这是 部分#3 调制解压缩器序列@: @
这是我的“" Time” -Boxed Tool@"}方法 @:给自己一个固定的窗口 来构建功能性#.}它迫使我们做出决定,并生成工作代码而不是理论设计_.}
DocSummarizer 一开始是为了展示你是如何 应: 多数教程都展示了如何将文字插入到LLM中, 希望有最佳的 & . @ @ 我希望能够显示适当的架构@:} gropting~,}嵌入,recurry_,}引用校验=.}
但当我总是做,时, 我对太空问题很感兴趣 四天后 “.” "I," I''d 挖进研究的“.”中
我运用了这些方法的版本 ._@here}'s 开始,正确的模式"成为ONX嵌入器 运行在本地{,BM25}混合搜索结合
公正警告@ : @ 这是# " @ I wrong too far@ MS K2 @ 深潜@ I.} 如果您想要使用此工具, @ MPK4 @ read part * 2. @ 如果你想理解的话读取部分 为什么 它的工作和 如何 继续读着.
本篇文章涵盖 @:
在钻入具体细节前 这里 ' * 碎片是如何组合在一起的* MS K2 *
flowchart TB
subgraph Input["Document Input"]
DOC[/"Document<br/>(PDF, MD, URL)"/]
end
subgraph Parse["Parsing Layer"]
DOCLING["Docling<br/>(PDF/DOCX)"]
MARKDIG["Markdig<br/>(Markdown)"]
end
subgraph Extract["Extraction Layer"]
CHUNK["Document Chunker"]
SEGMENT["Segment Extractor"]
end
subgraph Embed["Embedding Layer"]
ONNX["ONNX Runtime<br/>(Sentence Transformers)"]
OLLAMA_EMB["Ollama<br/>(Optional)"]
end
subgraph Store["Vector Storage"]
QDRANT["Qdrant<br/>(Vector DB)"]
MEMORY["In-Memory<br/>(Small Docs)"]
end
subgraph Retrieve["Retrieval Layer"]
DENSE["Dense Search<br/>(Semantic)"]
BM25["BM25<br/>(Lexical)"]
RRF["RRF Fusion"]
end
subgraph Synthesize["Synthesis Layer"]
OLLAMA["Ollama LLM<br/>(Local)"]
TEMPLATES["Summary Templates"]
end
subgraph Output["Output"]
SUMMARY[/"Summary with<br/>Citations [chunk-N]"/]
end
DOC --> DOCLING & MARKDIG
DOCLING & MARKDIG --> CHUNK & SEGMENT
CHUNK --> ONNX & OLLAMA_EMB
SEGMENT --> ONNX
ONNX & OLLAMA_EMB --> QDRANT & MEMORY
QDRANT & MEMORY --> DENSE
CHUNK --> BM25
DENSE & BM25 --> RRF
RRF --> OLLAMA
OLLAMA --> TEMPLATES
TEMPLATES --> SUMMARY
当总结“ 500-” 页面手动手册时, 您需要找到相关的区域@ MS K2 @ 传统关键字搜索失败@ MPK3}
你需要 语义搜索 - 校对:Portnoy
嵌入式通过将文字转换成密度矢量@(arrays of number @)}来解决这个问题,它能捕捉语义意为#.}类似的意思 *=}类似的矢量{,}不论措辞是否精确.}
这里“ '” 是直觉@ MS K1 @ 想象一个“ I384-” 维空间, 每个文本都有位置@ MPK3+Texts 具有相似含义的组群 # MSC4}
graph LR
subgraph "Embedding Space (simplified to 2D)"
A["🚗 car"]
B["🚙 automobile"]
C["🏎️ vehicle"]
D["🍎 apple"]
E["🍊 orange"]
F["🍌 fruit"]
end
A -.->|"close"| B
B -.->|"close"| C
A -.->|"close"| C
D -.->|"close"| E
E -.->|"close"| F
D -.->|"close"| F
A -.-|"far"| D
问题:我需要嵌入为语义相似性工作的嵌入@.Raw BERT是为分类任务设计的 @,不相近搜索_.
解决方案: 使用 句子变压器 “-+”模式专门通过对比性学习就相似任务进行了专门培训;“.+”基于《公约》, BERT 建筑 以不同方式调整
模型,如 all-MiniLM-L6-v2 和 bge-small-en-v1.5 培训数十亿对文本,
训练教他们类似的意义 近端矢量 (高连结
相关: @ @ MSk0} 如果您想要了解变压器模型如何在更深的级别上工作 #- 包括关注机制, encoder@-decoder 架构,, 为什么嵌入工作 {- 见我的文章 神经机器翻译如何工作它从翻译角度涵盖了相同的变压器概念 @.
执行 执行情况: 我们使用模型'#%s 输出层并应用 平均共用 - 平均所有标记的嵌入以获得整个文本的单一矢量@.
flowchart LR
subgraph Input
TEXT["The quick brown fox"]
end
subgraph Tokenization
CLS["[CLS]"]
T1["the"]
T2["quick"]
T3["brown"]
T4["fox"]
SEP["[SEP]"]
end
subgraph "BERT Encoder"
direction TB
L1["Layer 1: Self-Attention"]
L2["Layer 2: Self-Attention"]
L3["..."]
L6["Layer 6: Self-Attention"]
end
subgraph Output
E1["E[CLS]"]
E2["E[the]"]
E3["E[quick]"]
E4["E[brown]"]
E5["E[fox]"]
E6["E[SEP]"]
end
subgraph Pooling
MEAN["Mean Pool<br/>(with attention mask)"]
VEC["384-dim Vector"]
end
TEXT --> CLS & T1 & T2 & T3 & T4 & SEP
CLS & T1 & T2 & T3 & T4 & SEP --> L1
L1 --> L2 --> L3 --> L6
L6 --> E1 & E2 & E3 & E4 & E5 & E6
E1 & E2 & E3 & E4 & E5 & E6 --> MEAN
MEAN --> VEC
我想嵌入到“"” 只要工作 “"” 当有人运行工具时
用户想要的垃圾@. @% docsummarizer -f doc.pdf@ ,_ 不是30-} 一步设置指南 * .
ONNX {(} 开放神经网络交换} )} 是 ML 模型的公开格式@.} 杀手特性:} 没有 Python 的运行时间推断.
我得到了什么与ONNX 运行时间=:
与 GPU PyTorrch @,相比, 贸易速度稍慢, 但比让用户安装 Python@.要快得多
DocSummarizer 包含多个嵌入模型@, @ 每一个都具有不同的交易}#-_ offs:}
| Q 型号为 @ | Q 尺寸@ | Z Max Tokens * | Q大小 @MS K4QQAnnatized#}( | Q)使用大小写@MS K7Q 需要指令 MSKO8 |
|---|---|---|---|---|
AllMiniLmL6V2 @ |
@ 384 @ @ I | @ *256# @ * | } @ *MSSK6# 默认@- @ fast_ MS K8# 优质 @ MASK9 @ no # | # |
BgeSmallEnV15 |
+#~34MB # | #更好的技术工具#MS K7_是 | # | |
GteSmall -=YTET -伊甸园字幕组=- 翻译: |
||||
MultiQaMiniLm @ |
@ 384 @ @ @ I | @ @ *512 @ * | @ * MB# MS K6 @ 优化为@&A @ A_ | @ no MSC9} @ |
注注注注注释注注说明附注注注,注注 注注的注 注 注 注注说明注注附注注 注的注注 注注说注注的附注注说明,注 注,注说明的注说明说明注 注说明注说明 注注 注附注注的注释注 注 注 注注释注说明注释注的说明注,说明注的 注 注说明说明说明 注 注的说明说明,说明说明的附注 注注附注说明注附注的注附注 注 注注释说明注注释的注注释 注注注释注释注,的注, 注注,注释注附注附注注注释说明的说明 注说明 注的注释 注 注附注说明说明注释说明说明附注 注说明的注释注释注释说明 注注释注释 注说明注释注释的说明注释 注注释 注的附注注释注注释,注注释附注注附注注释的 注注脚注注注:}所有注册条目点均与 WordPiece-}匹配的 OnNX 出口 {(} 使用 vocab.txt尚未支持Unigram模型“.”
BGE 指令格式@ :_ 某些型号@ (} 类似 BGE)} 需要前缀来优化性能@ I.} 确切格式取决于模型=:}
// Query embedding (what the user asks)
var queryText = "Represent this sentence for searching relevant passages: " + userQuery;
var queryEmbedding = await EmbedAsync(queryText);
// Passage embedding (document chunks)
// Some BGE variants prefix passages, others don't - check model documentation
var passageEmbedding = await EmbedAsync(chunkText);
登记册跟踪哪些模型需要通过 RequiresInstruction 和 QueryInstruction 字段@.}在使用基于指令的模型时,总是以检索质量为基准{-} .
示范登记册如何运作?
public static class OnnxModelRegistry
{
public static EmbeddingModelInfo GetEmbeddingModel(OnnxEmbeddingModel model, bool quantized = true)
{
return model switch
{
OnnxEmbeddingModel.AllMiniLmL6V2 => new EmbeddingModelInfo
{
Name = "all-MiniLM-L6-v2",
HuggingFaceRepo = "Xenova/all-MiniLM-L6-v2",
ModelFile = quantized ? "onnx/model_quantized.onnx" : "onnx/model.onnx",
VocabFile = "vocab.txt",
EmbeddingDimension = 384,
MaxSequenceLength = 256,
SizeBytes = quantized ? 23_000_000 : 90_000_000,
RequiresInstruction = false
},
// ... other models
};
}
}
不同的模型使用不同的方式 all-MiniLM-L6-v2 模型使用 WordPiece 代号@( @ 类似 BERT),}, 它将未知的单词拆分为子名符號@ ._ 其他模式可以使用 BPE {(}\ _Byte MS K4} Pair Encoding}) 或 Unigram messignizers *.}
重要: 模擬代碼器必須符合 訓練的代號機}{. 我們的登記軌道 每個型號都要求. 目前正在实施@ :_ WordPiece (维亚 vocab.txt*).BPE/Unigram支持通过 tokenizer.json 正在计划但尚未实施 @- 粘贴在登记册中的 WordPiece 模型
public class BertTokenizer
{
private readonly Dictionary<string, int> _vocab;
private const int ClsTokenId = 101; // [CLS] - start of sequence
private const int SepTokenId = 102; // [SEP] - end of sequence
private const int PadTokenId = 0; // [PAD] - padding
private const int UnkTokenId = 100; // [UNK] - unknown token
public BertTokenizer(string vocabPath)
{
// Load vocabulary: word -> token ID
_vocab = File.ReadAllLines(vocabPath)
.Select((word, index) => (word, index))
.ToDictionary(x => x.word, x => x.index);
}
public (long[] InputIds, long[] AttentionMask, long[] TokenTypeIds)
Encode(string text, int maxLength)
{
// Split text into words, then apply WordPiece to each word
var words = text.ToLowerInvariant()
.Split(new[] { ' ', '\t', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var tokens = words.SelectMany(WordPieceTokenize).ToList();
// Truncate to fit [CLS] and [SEP] tokens
if (tokens.Count > maxLength - 2)
tokens = tokens.Take(maxLength - 2).ToList();
// Build input: [CLS] + tokens + [SEP] + [PAD]...
var inputIds = new List<long> { ClsTokenId };
inputIds.AddRange(tokens.Select(t => (long)GetTokenId(t)));
inputIds.Add(SepTokenId);
// Pad to maxLength
var padCount = maxLength - inputIds.Count;
inputIds.AddRange(Enumerable.Repeat((long)PadTokenId, padCount));
// Attention mask: 1 for real tokens, 0 for padding
var attentionMask = inputIds.Select(id => id != PadTokenId ? 1L : 0L).ToArray();
// Token type IDs: all zeros for single sentence
var tokenTypeIds = new long[maxLength];
return (inputIds.ToArray(), attentionMask, tokenTypeIds);
}
private IEnumerable<string> WordPieceTokenize(string word)
{
// If the whole word is in vocabulary, return it
if (_vocab.ContainsKey(word))
{
yield return word;
yield break;
}
// Otherwise, split into subwords with "##" prefix
int start = 0;
while (start < word.Length)
{
int end = word.Length;
string? curSubstr = null;
while (start < end)
{
var substr = word[start..end];
if (start > 0) substr = "##" + substr; // Continuation marker
if (_vocab.ContainsKey(substr))
{
curSubstr = substr;
break;
}
end--;
}
if (curSubstr == null)
{
yield return "[UNK]";
yield break;
}
yield return curSubstr;
start = end;
}
}
}
示例符号@: @%
| 输入 * | * Tokens MS K2 |
|---|---|
"embedding" |
["em", "##bed", "##ding"] |
"DocSummarizer" |
["doc", "##su", "##mm", "##ari", "##zer"] |
"the quick brown" |
["the", "quick", "brown"] |
在 BERT 处理标记后, 我们得到每个标记的隐藏状态@ . @ 平均集合平均值这些\ MS K2}(但仅代表实际符号) @ (not padding @ MPK4}
private static float[] MeanPool(Tensor<float> hiddenStates, long[] attentionMask, int hiddenSize)
{
// Assumes last_hidden_state shape: [batch=1, seq_len, hidden_size]
// Note: Many sentence-transformer models export a pooled output directly,
// but we use mean pooling for consistency across all ONNX exports.
var result = new float[hiddenSize];
var dims = hiddenStates.Dimensions.ToArray();
var seqLen = (int)dims[1];
// Count real tokens (not padding)
float maskSum = attentionMask.Count(x => x == 1);
if (maskSum == 0) maskSum = 1; // Avoid division by zero
// Average each dimension, weighted by attention mask
for (int h = 0; h < hiddenSize; h++)
{
float sum = 0;
for (int s = 0; s < seqLen; s++)
{
if (attentionMask[s] == 1)
sum += hiddenStates[0, s, h];
}
result[h] = sum / maskSum;
}
// L2 normalize for cosine similarity
float norm = MathF.Sqrt(result.Sum(x => x * x));
if (norm > 0)
{
for (int i = 0; i < result.Length; i++)
result[i] /= norm;
}
return result;
}
这里“ '” 表示从文本到嵌入的完整流程@ MS K1 @
public class OnnxEmbeddingService : IEmbeddingService, IDisposable
{
private InferenceSession? _session;
private BertTokenizer? _tokenizer;
public async Task<float[]> EmbedAsync(string text, CancellationToken ct = default)
{
await InitializeAsync(ct); // Downloads model if needed
// Prepend instruction for models that need it (like BGE)
if (_modelInfo.RequiresInstruction)
text = _modelInfo.QueryInstruction + text;
// Tokenize
var (inputIds, attentionMask, tokenTypeIds) =
_tokenizer.Encode(text, _maxSequenceLength);
// Create ONNX tensors
var inputIdsTensor = new DenseTensor<long>(inputIds, new[] { 1, inputIds.Length });
var attentionMaskTensor = new DenseTensor<long>(attentionMask, new[] { 1, attentionMask.Length });
var tokenTypeIdsTensor = new DenseTensor<long>(tokenTypeIds, new[] { 1, tokenTypeIds.Length });
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("input_ids", inputIdsTensor),
NamedOnnxValue.CreateFromTensor("attention_mask", attentionMaskTensor),
NamedOnnxValue.CreateFromTensor("token_type_ids", tokenTypeIdsTensor)
};
// Run inference
using var results = _session.Run(inputs);
// Get hidden states output
var output = results.First(r => r.Name == "last_hidden_state");
var outputTensor = output.AsTensor<float>();
// Mean pooling with attention mask
return MeanPool(outputTensor, attentionMask, _modelInfo.EmbeddingDimension);
}
}
这种天真的做法失败了 :
var text = File.ReadAllText("500-page-manual.txt"); // 2MB of text
var summary = await llm.GenerateAsync($"Summarize: {text}"); // ❌ Doesn't fit in context
即使使用“128”K 上下文窗口@,}您也可以 @'}只将大文档在“MS K3”中丢弃
而不是发送所有信息@, @ 仅发送相关内容@ '}
为何如此成功? LLM认为“10QQKB”内容高度相关,
flowchart LR
subgraph "Without RAG"
DOC1[/"500-page PDF"/]
LLM1["LLM<br/>(32K context)"]
OUT1["❌ Truncated or<br/>Hallucinated"]
end
subgraph "With RAG"
DOC2[/"500-page PDF"/]
CHUNKS["100 Chunks"]
VDB["Vector DB"]
QUERY["Query"]
TOP["Top 10 Chunks"]
LLM2["LLM"]
OUT2["✅ Grounded<br/>Summary"]
end
DOC1 --> LLM1 --> OUT1
DOC2 --> CHUNKS --> VDB
QUERY --> VDB --> TOP --> LLM2 --> OUT2
DocSummarizer 支持基于文档结构的多个块化策略@: @%
public class DocumentChunker
{
public List<DocumentChunk> ChunkByHeadings(string markdown, int maxHeadingLevel = 2)
{
var chunks = new List<DocumentChunk>();
var lines = markdown.Split('\n');
var currentChunk = new StringBuilder();
var currentHeading = "";
var headingLevel = 0;
var order = 0;
foreach (var line in lines)
{
// Detect heading (# to ######)
var headingMatch = Regex.Match(line, @"^(#{1,6})\s+(.+)$");
if (headingMatch.Success &&
headingMatch.Groups[1].Length <= maxHeadingLevel)
{
// Flush current chunk
if (currentChunk.Length > 0)
{
chunks.Add(new DocumentChunk(
Order: order++,
Heading: currentHeading,
HeadingLevel: headingLevel,
Content: currentChunk.ToString().Trim(),
Hash: ComputeHash(currentChunk.ToString())
));
}
// Start new chunk
currentHeading = headingMatch.Groups[2].Value;
headingLevel = headingMatch.Groups[1].Length;
currentChunk.Clear();
}
else
{
currentChunk.AppendLine(line);
}
}
// Don't forget the last chunk
if (currentChunk.Length > 0)
{
chunks.Add(new DocumentChunk(
Order: order,
Heading: currentHeading,
HeadingLevel: headingLevel,
Content: currentChunk.ToString().Trim(),
Hash: ComputeHash(currentChunk.ToString())
));
}
return chunks;
}
}
对于较长的文档来说 @,} DocSummarizer 提取单个片段 ( sentences,}列出项目\ MPK3\ 代码块_)\ 带有突出评分\ :}
public class SegmentExtractor
{
public async Task<ExtractionResult> ExtractAsync(string docId, string markdown)
{
// 1. Parse into typed segments
var segments = ParseToSegments(docId, markdown);
// 2. Generate embeddings
await GenerateEmbeddingsAsync(segments);
// 3. Calculate document centroid (average embedding)
var centroid = CalculateCentroid(segments);
// 4. Score by salience using MMR (Maximal Marginal Relevance)
ComputeSalienceScores(segments, centroid);
return new ExtractionResult
{
AllSegments = segments,
TopBySalience = segments.OrderByDescending(s => s.SalienceScore).Take(50).ToList(),
Centroid = centroid
};
}
}
没有产妇死亡率“,”检索“"”如何进行抓取工作 ?“MS K2”返回“MSC3”
最上面的“3”结果都说同样的事情 “.” I'"正在重复浪费上下文窗口@.}
MMR 余额 关联性 “(”与“查询”相似 多样性 “(”与已经存在的“-”不同,
公式=: \(MY MS K1 @\lambda@\}(笑) (笑)_\ text{sim}}(sK7s')\)
它能做什么 : 类似已选中的区段@.}这样可以避免摘要成为同一段落的 @5}版本。
flowchart TB
subgraph "MMR Selection"
S1["Segment 1<br/>Score: 0.95"]
S2["Segment 2<br/>Score: 0.90"]
S3["Segment 3<br/>Score: 0.88"]
S4["Segment 4<br/>Score: 0.85"]
end
subgraph "Selected"
SEL1["✓ Seg 1<br/>(highest)"]
SEL2["✓ Seg 3<br/>(most diverse)"]
SEL3["✓ Seg 4"]
end
S1 -->|"Select"| SEL1
S2 -->|"Skip - too similar to Seg 1"| X["❌"]
S3 -->|"Select"| SEL2
S4 -->|"Select"| SEL3
公式=:
\(MY MS K1 I\lambda \cdott MS4s, CDNQ})MS K7~(1{- MSC10LAMBDA) MPK12cdott MS13max_@{ @sós@'} @ \ @in 选中的 @simK3 @sI( @s, @sQ')\)#
private List<Segment> SelectSentencesMMR(
List<Segment> segments,
float[] centroid,
int targetCount)
{
var selected = new List<Segment>();
var candidates = new HashSet<Segment>(segments.Where(s => s.Embedding != null));
// Pre-calculate centroid similarities
foreach (var segment in candidates)
{
segment.Score = CosineSimilarity(segment.Embedding!, centroid)
* segment.PositionWeight;
}
while (selected.Count < targetCount && candidates.Count > 0)
{
Segment? best = null;
double bestScore = double.MinValue;
foreach (var candidate in candidates)
{
// Relevance: similarity to centroid
var relevance = candidate.Score;
// Diversity: max similarity to already selected
double maxSimToSelected = 0;
foreach (var sel in selected)
{
var sim = CosineSimilarity(candidate.Embedding!, sel.Embedding!);
maxSimToSelected = Math.Max(maxSimToSelected, sim);
}
// MMR score: balance relevance and diversity
var mmrScore = _config.Lambda * relevance
- (1 - _config.Lambda) * maxSimToSelected;
if (mmrScore > bestScore)
{
bestScore = mmrScore;
best = candidate;
}
}
if (best != null)
{
selected.Add(best);
candidates.Remove(best);
}
}
return selected;
}
我在测试:时碰到了这个
查询@: @ @ " @ whatsize_ '}是认证的 API 端点@?" @
语义搜索返回@: @%
错过了什么在代码示例中埋藏的实际API终点@: POST /api/v1/auth/login
为什么以自然语言对嵌入模型进行培训 POST /api/v1/auth/login @ '_ t语义匹配@ MS K1} 校对端点 @ MPK2 @ * -* it@ I'} 一个字面技术参引Q.}
结合两种检索方法及互补优势
| +搜索类型 @ | +强点 @MS K2+弱点@ | } |
|---|---|---|
| {\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}高音 “ | 语义理解”“,”同名词 @ | 可以错过精确的匹配@, @稀有条件 @MS K4} |
| 粗略的“(”BM25) @ | _ 精确关键字匹配@ ,} 稀有的用词 @ I | _ 没有语义理解 * |
混合搜索组合,使用对等平整拼凑 (RRF):
flowchart TB
QUERY["Query: 'authentication security'"]
subgraph Dense["Dense Search (Semantic)"]
D1["1. OAuth 2.0 implementation"]
D2["2. User login flow"]
D3["3. Password hashing"]
end
subgraph Sparse["BM25 Search (Lexical)"]
S1["1. Authentication middleware"]
S2["2. Security headers"]
S3["3. OAuth 2.0 implementation"]
end
subgraph RRF["RRF Fusion (Illustrative)"]
R1["OAuth 2.0 implementation<br/>RRF = 1/(60+1) + 1/(60+3) ≈ 0.032"]
R2["Authentication middleware<br/>RRF = (not in dense) + 1/(60+1) ≈ 0.016"]
R3["User login flow<br/>RRF = 1/(60+2) + (not in BM25) ≈ 0.016"]
end
QUERY --> Dense & Sparse
Dense --> RRF
Sparse --> RRF
注注注注注释注注说明附注注注,注注 注注的注 注 注 注注说明注注附注注 注的注注 注注说注注的附注注说明,注 注,注说明的注说明说明注 注说明注说明 注注 注附注注的注释注 注 注 注注释注说明注释注的说明注,说明注的 注 注说明说明说明 注 注的说明说明,说明说明的附注 注注附注说明注附注的注附注 注 注注释说明注注释的注注释 注注注释注释注,的注, 注注,注释注附注附注注注释说明的说明 注说明 注的注释 注 注附注说明说明注释说明说明附注 注说明的注释注释注释说明 注注释注释 注说明注释注释的说明注释 注注释 注的附注注释注注释,注注释附注注附注注释的 注注脚注注注@:_RRF计分显示为示例@.}恒定的 k=60}是标准的标准{;}实际排名取决于全部候选集=.}
public static class HybridRRF
{
/// <summary>
/// Reciprocal Rank Fusion: combine multiple rankings into one.
///
/// Formula: RRF(d) = Σ 1/(k + rank_i(d))
///
/// Where k = 60 (standard constant to prevent division by small numbers)
/// </summary>
public static List<Segment> Fuse(
List<Segment> segments,
string query,
BM25Scorer bm25,
int k = 60,
int topK = 20)
{
// Rank by dense similarity
var byDense = segments
.Where(s => s.Embedding != null)
.OrderByDescending(s => s.QuerySimilarity)
.ToList();
// Rank by BM25 (scorer is built over the same ordered segment list)
var bm25Scores = segments
.Select((s, i) => (segment: s, score: bm25.Score(i, query)))
.OrderByDescending(x => x.score)
.Select(x => x.segment)
.ToList();
// Rank by salience (pre-computed importance)
var bySalience = segments
.OrderByDescending(s => s.SalienceScore)
.ToList();
// Compute RRF scores
var rrfScores = new Dictionary<Segment, double>();
void AddRRFScore(List<Segment> ranking)
{
for (int i = 0; i < ranking.Count; i++)
{
var segment = ranking[i];
var rrfContribution = 1.0 / (k + i + 1); // 1-based rank
if (!rrfScores.TryAdd(segment, rrfContribution))
rrfScores[segment] += rrfContribution;
}
}
AddRRFScore(byDense);
AddRRFScore(bm25Scores);
AddRRFScore(bySalience);
// Return top-K by fused score
return rrfScores
.OrderByDescending(kv => kv.Value)
.Take(topK)
.Select(kv => kv.Key)
.ToList();
}
}
BM=25}(Best 匹配{25)}是经典的信息检索算法.}它结合了术语频率@,}反向文档频度>,}和文件长度正常化_:}
public class BM25Scorer
{
private const double K1 = 1.5; // Term frequency saturation
private const double B = 0.75; // Length normalization factor
public double Score(int docIndex, string query)
{
var queryTerms = Tokenize(query);
var docTermFreq = _docTermFreqs[docIndex];
var docLength = _docLengths[docIndex];
double score = 0;
foreach (var term in queryTerms.Distinct())
{
if (!docTermFreq.TryGetValue(term, out var tf)) continue;
if (!_docFreqs.TryGetValue(term, out var df)) continue;
// IDF with smoothing
var idf = Math.Log((_corpusSize - df + 0.5) / (df + 0.5) + 1);
// BM25 TF component with length normalization
var tfNorm = (tf * (K1 + 1)) /
(tf + K1 * (1 - B + B * docLength / _avgDocLength));
score += idf * tfNorm;
}
return score;
}
}
当总结一本小说,时,我得到的结果就像"MS K1"一样。
华生注意到气候温和,
这些是精确的提取@, @ but they'_re 颜色颜色 @( @sceene@-# 设置细节@MS K2}不是 核心绘图点.
挑战挑战: 你如何辨别:的区别?
TF -IDF ( 定期频率 @- 反文文件频度@)估计数 一个术语对文件的核心有多重要@,_#不是它的真相价值@.}
逻辑=:
这与真相无关, @(a 重复的说法可能是虚假的@,}一个罕见的事实可能是真的 文件的中心地位.
flowchart LR
subgraph "TF-IDF Classification"
CLAIM["Claim text"]
TERMS["Extract terms"]
TFIDF["Compute TF-IDF"]
CLASS["Classify"]
end
subgraph "Term Types"
COMMON["High DF (>50%)<br/>→ Core content"]
MODERATE["Medium DF (20-50%)<br/>→ Supporting detail"]
RARE["Low DF (<20%)<br/>→ Incidental colour"]
end
CLAIM --> TERMS --> TFIDF --> CLASS
CLASS --> COMMON & MODERATE & RARE
public class TextAnalysisService
{
private readonly Dictionary<string, int> _documentFrequency = new();
private int _totalDocuments;
public void BuildTfIdfIndex(IEnumerable<string> documents)
{
_documentFrequency.Clear();
_totalDocuments = 0;
foreach (var doc in documents)
{
_totalDocuments++;
var terms = Tokenize(doc).Distinct();
foreach (var term in terms)
{
_documentFrequency.TryGetValue(term, out var count);
_documentFrequency[term] = count + 1;
}
}
}
/// <summary>
/// Classify term centrality (not epistemic truth):
/// - High DF (>50%): appears across most chunks = core content
/// - Medium DF (20-50%): supporting detail
/// - Low DF (<20%): rare = likely incidental ("colour")
///
/// Note: This estimates centrality, not factuality. A repeated
/// claim can be false; a rare fact can be true.
/// </summary>
public ClaimType ClassifyTermImportance(string term)
{
var df = _documentFrequency.GetValueOrDefault(term.ToLowerInvariant(), 0);
if (_totalDocuments == 0 || df == 0)
return ClaimType.Colour;
var documentRatio = (double)df / _totalDocuments;
// High centrality = appears widely
if (documentRatio > 0.5)
return ClaimType.Core;
// Medium centrality = supporting themes
if (documentRatio > 0.2)
return ClaimType.Supporting;
// Low centrality = incidental detail
return ClaimType.Colour;
}
}
Docsummarizer ' 生产管道BertRagSummarizer“)”结合所有这些概念“:”
public class BertRagSummarizer
{
/// <summary>
/// Full pipeline: Extract → Retrieve → Synthesize
///
/// Key properties:
/// - LLM only at synthesis (no LLM-in-the-loop evaluation)
/// - Deterministic extraction (reproducible, debuggable)
/// - Validated citations (every claim traceable to source segment)
/// - Scales to any document size
/// - Cost-optimal (cheap CPU work first, expensive LLM last)
/// </summary>
public async Task<DocumentSummary> SummarizeAsync(
string docId,
string markdown,
string? focusQuery = null)
{
// === Phase 1: Extract ===
// Parse document → segments with embeddings + salience scores
var extraction = await _extractor.ExtractAsync(docId, markdown);
// === Phase 2: Retrieve ===
// Hybrid search: Dense + BM25 + Salience via RRF
var retrieved = await RetrieveAsync(extraction, focusQuery);
// === Phase 3: Synthesize ===
// LLM generates fluent summary from retrieved segments
var summary = await SynthesizeAsync(docId, retrieved, extraction, focusQuery);
return summary;
}
}
当建造和使用 DocSummarizer @, @ I'# 遇到这些问题时,
收缩器不匹配 @ → @ 无稽的嵌入: 載入 WordPiece vocab 的 BPE- 訓練型態產生有效的{-但表面上沒有意義的矢量{MS K3 總要驗證符號是否與模擬' 培訓機程}.
单字母中的主数@ - @ title address“: ” 使用一个文档的中央机器人系统下移“-” 来修补这个功能,但又增加了复杂度“MS K7”
BM25 以稀有条件比密集搜索强: 如果您的查询包含技术术语或适当的名词, 在嵌入模型中代表的@' 培训数据 @, 词汇匹配 *(BM25) 将超过语义搜索@MS K6 这就是为什么混合搜索事项.
扫描PDFs中的OCR垃圾-*}校对:Portnoy
Lowó- 覆盖摘要必须使用对冲语言: 如果您只看到“'}”文档中的 “3% {,}”字句,比如“MS K4}{最后一刻}"}或“MSSK6}BARBAR_结论"}这是不诚实的>.}这个系统必须说‘"』样本区域 @"} 并避免最终结尾@.}
引用幻觉: 小LLM {(1.5}B-3}B params)}有时发明了可信的}- [chunk-N]“,}校验 N ” 在源块中存在, [chunk-999] 您的LLM 正在挣扎着要完成的任务@. @%
这些是'#tbugs @-}它们都是空间设计中固有的紧张 ~.}好的生产系统确认并减轻了这些紧张
当处理非常大的文档时 @ , docSummarizer does\ MS K1 t尝试嵌入所有@ - 它使用语义预写@ MPK3过滤程序来选择具有代表性的区段@ OMK4} 这意味着该摘要基于一个样本\ ,}而不是完整的文档=.。
系统可透明地处理此操作@ : @
// If coverage is low (<5%), prepend disclaimer and use cautious language
if (coverage < 0.05)
{
var disclaimer = $"WARNING: Summary (sampled ~{coverage:P1} of document)";
summary = $"{disclaimer}\n\n{CleanAndHedge(summary)}";
}
// Append coverage footer to every summary
var footer = $"\n\n---\nCoverage: {coverage:P1} ({scope})\nConfidence: {confidence}";
重要: 这是 已检索证据摘要当我们说“"+sampled @3%",”时,
抽样是'}不是随机的 # - @ it@' @%s 语义3%可能错过所有限制和边缘情况@.#Semantic @Z3%试图捕捉每个主要主题的代表部分@MS K7}It'}仍然部分覆盖*,,但是它故意使'}部分覆盖=.
具有多个专题锚点的适应性抽样: @ preQ-filter 使用多个锁定@ MS K2kK3}-mskK5}用來確保少數議題是't 被系统性地排除.} 這防止了「 MSKO8}主題偏差}{" 一個單核機關鍵-KKK10}(重要嘅K11butMS12rare) 的內容, 如限制♪, 例外}MS K14}或結論MSHC15}
从 SegmentExtractor.cs:
// Multi-anchor approach prevents single-centroid bias
var topicAnchors = ComputeTopicAnchors(embeddedSample, k: 5);
// Score by max similarity to ANY anchor (catches minority topics)
var score = topicAnchors.Max(anchor => CosineSimilarity(segment.Embedding, anchor));
这是研究 - 知情的 @( 避免单-\Qquery回想起崩溃@)}但实用的 MPK4Q 运行在 CPU+. 上几秒钟
为什么不把所有东西都嵌入进去呢?
@500-_page document 文档中的“%sK1}部分@),}嵌入每件事物都有效,但“'”是最佳的吗?
多- anchor 采样给了您最好的两个\MS K1 广泛主题覆盖,可移动计算@ .
I'' 以上我所描述的是 @ '_ 不只是 @ I"retrieval}"} @ *-} it'{ 我称之为特定模式 受限制的模糊环境拖拉 (CFCD洞察力和洞察能力
大多数总结器继续添加上下文@ . @ docSummarizer 向前拖动 然后让模型在这些边框中流畅地书写 .
这里请看DocSummarizer 通往 CFCD=:的管道图如何绘制
-=YTET -伊甸园字幕组=- 翻译: |--------------|------------------------------| | 色素检测 (Fuzzy)|嵌入式MS K3近似机器人},THK5IDF中心 | 促进确定性 |+M MMZMPK1+BM25,+RRFUD+,}顶尖的-K选择=|# | 锁定分类账 @|_ 检索到的片段组配有引证识别码@|} | 受制约的一代人 “| 综合合成,由检索到的证据所约束”“|
为什么这重要“:”这个模型决定了“'”与什么有关,“MS K3” 回收管道是‘.’ 。“模式”只在我们“'”所设置的范围内流畅生成。 这就是为什么小本地型号在“:”上工作的原因。“锚”做重升“MSKO8”
@"_anchor clactor\ "}在实际操作中看起来像这个#(}(概念上)~):}
{
"coverage": "3.2% semantic sample",
"anchors": [
{ "id": "chunk-12", "text": "Reset requires holding button 10s", "salience": 0.92 },
{ "id": "chunk-45", "text": "Factory reset clears all settings", "salience": 0.88 }
],
"constraints": {
"terms": { "factory reset": "restore factory settings" },
"hedging": "sampled 3% - avoid definitive conclusions"
}
}
然后在合成中 提示:
这就是为什么:
CFCD是和CFD一样的哲学分裂 受限制的模糊, 限制的模糊MOM,和 图像摘要器 “-”概率建议“,” 确定论长期存在 在时间轴中应用的“MS K2” “/”\“memory 轴@.”
ONNX 模型可以以较小大小和更快的推断量来量化 “(”降低精确度“)”
| “ | ”模型“ | ”全精度 | “量化”、“ | ”质量差异= |
|---|---|---|---|---|
| 所有的-MINIL-L6-v}2 MS K5 NSK6MB □ | MSKO8MU | MK10MSC11 | ||
| @ 133_MB# | } @ I34MB$ MS K9 ~0.3% @ @ MPK11 @ |
对于大文档来说,批量嵌入对性能至关重要 @.ONNX 运行时间@'s InferenceSession 通常可以安全地横贯线条共享@, @% 但性能取决于会话配置 @:}#
public async Task<float[][]> EmbedBatchAsync(IEnumerable<string> texts, CancellationToken ct)
{
var textList = texts.ToList();
var results = new float[textList.Count][];
// InferenceSession is safe to share for inference in most cases
// Tune SessionOptions.IntraOpNumThreads and InterOpNumThreads for your workload
var maxParallel = Math.Min(Environment.ProcessorCount, 8);
await Parallel.ForEachAsync(
textList.Select((text, index) => (text, index)),
new ParallelOptions { MaxDegreeOfParallelism = maxParallel },
async (item, token) =>
{
results[item.index] = await EmbedSingleAsync(item.text, token);
});
return results;
}
业绩提示@ :_ 配置 SessionOptions 创建会话时\ :\
var sessionOptions = new SessionOptions
{
IntraOpNumThreads = 4, // Threads within a single operation
InterOpNumThreads = 2 // Threads across operations
};
var session = new InferenceSession(modelPath, sessionOptions);
需要特殊处理才能避免 OOM @:}
// For documents > MaxSegmentsToEmbed, use hierarchical extraction
if (segments.Count > _config.MaxSegmentsToEmbed)
{
// Process in batches, keeping only top-K per batch
// Then re-rank globally
return await ExtractHierarchicalAsync(segments);
}
在典型的开发机“(Ryzen @5600X,”上,
| “ | ”操作“ | ”传输“MS K2”,注意“MSC3” | |
|---|---|---|---|
| 嵌入 64, * 所有-MiniL-L6-vMS K8QQQ* | |||
| 密集检索 @ | @ @ <10#s@ | _Constine 相近于 @MS K3}部分 @ | { |
| BM25评分 | MS K1 ms I | 输入- 模拟倒转索引 | |
| RRF 聚联 @ | @ @ <2#s @ I | #组合 @MS K3#排名@ | } @ |
| 尾端的-@to @ - end @(25-_ page PDF)}% | {~15-20} | 包括填块 MS K3 嵌入, 检索MSKO5LM 合成MS K6 |
测试环境32GBRAM,没有GPUMPK6嵌入式使用所有的-MiniL-L 6-v}(QQQAX,MS K13 象征性最大),@8-全线平行批发}MSC16Restrieval champage您的里程因不同的模型而不同
主驱动程序正在嵌入传输流量 -}他们只用毫秒=MSK4=.}这强化了“"LYM lastQZK7”原则 :}低廉的CPU工作({emedding, recredition@)_Birst @,}只有过滤内容才使用高价LLM}♪.}
缩缩逐批处理,每批仅保留上方的-QK以保存记忆@. @
DocSummarizer 表明,先进的NLP能力需要云性API或皮松依赖性*.}OnX嵌入运行时间和Ollama的一代=,}你可以建造完整的RAG输油管
构建此工具的关键洞见@: @
此结论是 DocSummarizer 系列“. ” 这里 “'”三部分是如何结合的?
部分#1 解释 为什么 管道处理方法比天真的LLM调用@.#它覆盖了结构模式 @(_,} 级降,}\ 引证校验=)}让任何文件摘要员工作顺利
部分#2 您的快速“ -” 启动指南@ MS K1 @ 安裝@ I, @ model @ ,} 模板@ MPK4 @ 通用案例@ MOSK5 @ 如果您想要使用此工具 ,# , 该工具是' @ 所有您需要的工具 @ OMK8 @
部分#3 @(_#这篇文章@)}是想要理解的人的深水潜水 如何 实际上它的工作原理是“:BERT 与句子变压器+,为什么ONX重要? , 象征性化得到,混合搜索交易@-offs,,生产中断什么?”
如果您正在建立自己的管道... ,... ...读取所有三个#....
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.