هذا هو الجزء 3 لسلسلة DocSummarizer:
هذا جزء من "الزمن-أدوات المboxed "نهجيM SK3 أعطي نفسي نافذة ثابتة لبناء شيء عمليMSC4 إنه يفرض القرارات وينتج البرمجة العاملة بدلاً من التصميمات النظرية
بدأ DocSummarizer بعرض كيف يمكنك يجب أن بناء ملخصات दस्तावेज مع LLMs - مقاربة الخطوط العريضة التي شرحتها في الجزء 1. معظم الدروس تبين لك كيف تضع النص في LLM وتأمل في الأفضل. أردت أن أريكم المعمار المناسب
لكن كما أفعل دائماً, أصبحت مهتماً بمكان المشكلة. بعد أربعة أيامM SK2 حفرت في البحثMSC4 كيف تتعامل الأنظمة الإنتاجية--- مع التقليد في الواقع-- ? ما الذي يجعل عملية الاستخراج تعمل بشكل جيد؟
لقد قمت بتطبيق نسخ من هذه المقاربات. ما بدأ كـ " هنا ' هو النمط الصحيح " أصبح مداخلات ONNX تعمل محلياًM SK4 البحث الهجين يجمع بين BMMSC5 مع الاستخراج الكثيبMST6 الأهمية الحدية المحدودة للتعددMSP7 ودمج التصنيف المتقابل لدمج الإشاراتMSL8
تحذير عادل: هذا هو " لقد ذهبت بعيداً " الغوص العميق. إذا أردت فقط أن تستخدم الأداةM SK4 أقرأ الجزء 2. إذا كنت تريد أن تفهم لماذا إنها تعمل كيف تتناسب الأجزاء مع بعضها.
هذا المقال يغطي
قبل الغوص في التفاصيل, هنا' كيف تتناسب الأجزاء مع بعضها البعض
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- دليل للصفحة, تحتاج إلى العثور على الفصول ذات الصلةM SK2 إختلال البحث عن كلمات رئيسية تقليديةMSC3
أنت بحاجة البحث الدلالي - تطابق مع المعنى, ليس مجرد كلمات
المداخلات تحل هذا عن طريق تحويل النص إلى محركات كثيفة (سلسلة من الأرقام) التي تلتقط المعنى الدلاليM SK2 المعاني المتقاربة = محركات متقاربةMSC4 على الرغم من الكلمات الدقيقة
هنا ' هو الحدس : تخيل مساحة 384- بعدية حيث كل قطعة من النص لديها موقع . Textes with similar meanings cluster together .
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
المشكلة: أحتاج إلى مداخلات تعمل على شبيهة الدلالية. تم تصميم بيرت الخام لواجبات التصنيفM SK2 ليس بحث شبيهية .
الحل: استخدام محولات الجملة نماذج - تم تدريبها بشكل خاص على مهام شبيهة باستخدام التعلم المتناقض . معمارية بيرت لكن جيد-مضبط بشكل مختلف.
نماذج مثل all-MiniLM-L6-v2 و bge-small-en-v1.5 تم تدريبها على مليارات زوجات نصية مثل :
الدرس يعلمهم : المعاني مماثلة = векторات قريبة | ( | شبيهة بكزيونية عالية |).
مرتبطة: إذا كنت تريد أن تفهم كيف تعمل نماذج المحولات على مستوى أعمق - بما في ذلك آليات الانتباه, البرمجةM SK3 العمارة البرمجيةMSC4 ولماذا تعمل التداخلات - شاهد مقالي على كيف تعمل ترجمة الآلة العصبية. إنها تغطي نفس مبادئ المحول من وجهة نظر الترجمة
التنفيذ: نأخذ طبقة خروجية النموذج' ونطبقها الجمع المتوسط - متوسط تداخل كل الرموز للحصول على вектор واحد لكل النص
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
أردت دمجات إلى "عمل فقط" عندما يقوم شخص ما بتشغيل الأداةM SK2 المقاربة القياسية :
هذا سيء. يريد المستخدمون docsummarizer -f doc.pdf, ليس دليل لإعداد خطوة 30-
أونكس ( تبادل شبكات عصبية مفتوحة ) هو نموذج مفتوح للنماذج الـ ML إستنتاج وقت الجري بدون Python.
ما أحصل عليه مع ONNX Runtime:
التجارة-off: أبطأ قليلاً من GPU PyTorchM SK2 ولكن أسرع بكثير من طلب المستخدمين أن يشتروا Python .
DocSummarizer يحتوي على العديد من نماذج الدمج, كل منها مع تجارة مختلفة-offsM SK2
| النماذج | الأبعاد | اعظمي الرموز | الحجم | ( | كميّة | ) | موضوعة الاستخدام | مSK7 | تتطلب التعليمات | المSK8 | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
AllMiniLmL6V2 |
384 ≥ | \256 ♪ | ~23 | MB | លំនាំដើម | MSC7 | سرعة | MSC8 | جودة جيدة | msc9 | لا | msc10 | |||
BgeSmallEnV15 |
384 | ||||||||||||||
GteSmall |
384 ≥ | ≤512 | ~34 MB | الغرض العام مسك7 لا مسك8 | |||||||||||
MultiQaMiniLm |
384 ≥ | \512 | ♪~23 | MB | optimized for Q |
ملاحظة: جميع مدخلات السجل تشير إلى WordPiece-مصدرات ONNX المتوافقة (استعمال vocab.txt). BPE/نماذج الـ"Unigram" لم يتم تشغيلها بعد
صيغة التعليمات BGE: بعض النماذج ( مثل BGE) تتطلب مسبقات لأداء مثاليM SK3 الصيغة الدقيقة تعتمد على النموذج
// 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), الذي يقسم الكلمات غير المعروفة إلى توكنات كلمات فرعيةM SK2 قد تستخدم النماذج الأخرى BPE
مهم: توكيزة النموذج يجب أن تتطابق مع توكيزر التدريب . سجلنا يتبع أي توكيسور يحتاجه كل نموذج يتم تنفيذها حاليا: WordPiece (via vocab.txt). BPE/دعم وحدة الرسم عن طريق tokenizer.json planned but not yet implemented - stick to the WordPiece models in the registry for now
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;
}
}
}
مثال توكيزة:
| الدخل | الرموز |
|---|---|
"embedding" |
["em", "##bed", "##ding"] |
"DocSummarizer" |
["doc", "##su", "##mm", "##ari", "##zer"] |
"the quick brown" |
["the", "quick", "brown"] |
بعد أن يقوم بريت بتحليل الرموز، نحصل على حالة مخفية لكل رموز.
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;
}
هنا'التدفق الكامل من النص إلى الدمج:
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
حتى مع 128K النوافذ السياقية, يمكنك ' عدم رمي الوثائق الهائلة في
بدلاً من إرسال كل شيء، أرسل فقط ما هو ملائم
لماذا يعمل هذا نرى في LLM 10KB من المحتويات ذات الصلة العالية بدلاً من 2MB من النص الأكثر أهمية.
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 يستخرج أجزاء فردية M SK1جمل,मदات القائمةMSC3كتلوف الرمزMska4 مع درجات الجدوىMske5
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
};
}
}
بدون MMR, البحث عن " كيف يعمل ال caching?" أعاد
النتائج الأعلى 3 جميعها تقول نفس الشئ
توازن MMR الأهمية (similarity to query) with التنوع (ماثل جداً بما هو موجود بالفعل-أشياء تم اختيارها
الصيغة: $$MMR = \lambda S\cdot \textM SK5sim}(sMSC7 queryMNK8 | | - \ (1 |- ♫\laMBda ) ♪\ cdot \\max_{sM SK1 \in Selected}
ما الذي تفعله penalizes candidates similar to already-selected segments. This prevents the summary from being 5 versions of the same paragraphM SK3
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
الصيغة:
$$MMR = \lambda SSK3cdot simM SK4s, centroidMSC6 | | - \ |(1 |_{sM SK1 \in Selected} simMSC4s, sMST6
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;
}
صادفت هذا عندما كنت أختبر :
السؤال: "ماذا'هي نقطة نهاية الAPI للاتصالM SK3
استعادت البحث الدلالي:
ما تفتقده: نقطة النهاية الفعلية للAPI مدفونة في أمثلة البرمجة: POST /api/v1/auth/login
لماذا: تم تدريب النماذج المدمجة على اللغة الطبيعية , ليس الرمز/URLsM SK3مصطلحات دقيقةMSC4 النقطة النهائية POST /api/v1/auth/login لا يتوافق 'من الناحية الدلالية "نقطة النهاية للتحقق من صحته " - إنه S'مرجع تقني حرفي SSK5
دمج طريقتين للبحث مع نقاط متماثلة
| نوع البحث | نقاط قوته | نقاط ضعفه ♫ | |||
|---|---|---|---|---|---|
| كثافة ( | الفهم الدلالي, المصطلحات المتماثلة | يمكن أن تضيع تطابق تامةM SK3 مصطلحات نادرة | |||
| المعزوفة (BM25) | تطابق مفاتيح الكلمة بالضبط, عبارات نادرة | لا فهم لغوي |
البحث الهجين يجمع كلاهما باستخدام دمج الرتبة المتقابلة (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 المعروضة موضحة . المستقيم kM SK2 هو معياري; التصنيف الفعلي يعتمد على المجموعة الكاملة للمرشحين
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();
}
}
BM25 M SK1Best Matching 25) is the classic information retrieval algorithmMSC3 It combines term frequency, inverse document frequenceMska5 and document length normalizationM Ska6
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;
}
}
عند تلخيص رواية، حصلت على نتائج مثل
"الشخصي يرتدي قبعة أزرقة . ولاحظ واتسون أن الطقس كان معتدلاً
هذه استخراجات دقيقة، ولكنها اللون (sceneM SK1setting details), not نقاط الرسم الأساسي.
التحدي: كيف تحدد الفرق بين
TF-IDF كيف أن المصطلح مركزي للوثيقة, ليس قيمة الحقيقة
منطقي:
هذا لا يتعلق بالحقيقة (إدعاءات متكررة يمكن أن تكون خاطئة,حقيقة نادرة يمكنها أن تكون صحيحة مركزية للوثيقة.
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, أنا' واجهت هذه المشاكل M SK2 وسوف تفعل أيضاًMSC3
خطأ توكينر → مداخلات غير معقولة: تحميل مصطلح WordPiece vocab لBPE-نموذج تدريبي ينتج векторات صالحةM SK2 تبدو ولكنها غير ذات معنى فيزيائياً . دائماً تتأكد من تطابق الرمز مع النموذج
التعصب الرئيسي -الموضوعي في واحد -درجات مركزية: بإستخدام مستند واحد مركزي بشكل منهجي إلى الأسفل- يصنف المواضيع الأقلية ( القيود M SK3 استثناءات , حالات الحافة MSC5 متعددةM SK6 تصحيح هذا ولكن تضيف تعقيداً
BM25 يتغلب على البحث الكثيب على عبارات نادرة: إذا كان سؤالك يحتوي على المصطلحات التقنية أو الأسماء الصحيحة ليس جيداً- ممثلة في نموذج الدمجM SK2 بيانات التدريب , تطابق اللغوي MSC4BMMSC5 سيتفوق البحث الدلاليMSc6 لهذا السبب يهم البحث الهجين MSc7
القمامة المطبوعة بصيغة أو سي آر في ملفات بي دي اس مسحت: التدوين جيد, لكن أخطاء الرسم البياني المركبة . إذا رأيت فوضى في الإقتباساتM SK3 قم بالتحقق من إخراج العلامات المنخفضة من التدوير أولا | - | يمكن للملخص |' | لا إصلاح مدخل القمامة |
منخفض - توصيل الغطاء يجب أن يضمن اللغة: إذا كنتم ' تشاهدون فقط 3% من الوثائق , عبارة مثل "M SK4" في نهاية المطاف "Mska5" أو "Mske6" في الختام "Msko7" غير صادقة "Msek8" يجب أن تقول النظام " في مقاطع العينة " ويتجنبون نهاية نهائية "SSK11"
هلوس الاقتباس: Small LLMs (1.5BM SK2B params) sometimes invent plausibleMST4sounding citationsM ST5 We validate by parsing the output for [chunk-N], التحقق من وجود N في الجسيمات المصدريّة, وتشير أو تصليح الادعاءات التي تشير إلى الجسيمين المفقودةM SK2 إذا رأيتم [chunk-999] من أجل 10-documentchunk, يصارع LLM مع المهمة
هذه ليست "مسك0" bugs مسك1" they'"re intrinsic tensions in the design space. Good production systems acknowledge and mitigate them"
عند معالجة مستندات كبيرة جدا, DocSummarizer لا يحاول ' أن يدمج كل شيء - إنه يستخدم الدلالية قبلM SK3 التصفية لتحديد الأجزاء reprezentativeMSC4 هذا يعني أن الخلاصة تعتمد على عينة
النظام يتعامل مع هذا بوضوح:
// 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}";
مهم: هذا هو ملخص الأدلة التي تم الحصول عليها, ليست ضماناً للغطية الكاملة - للوثيقة
العينة ليست عشوائية - it 's الدلالية. نستخدم مجموعة متعددة - لنتأكد من أن المواضيع الأقلية ليست excluded' excluedM SK3 يمكن أن يضيع عشوائيًا | 3% | جميع القيود و حالات الحافة |. | سيميّن ـ 3% | يحاول إلتقاط جزء واحد reprezentative من كل موضوع رئيسي ٬. | إنه ٬ ' ٬ لا يزال تغطية جزئية ,, ٬ ولكنه , ' ٬ هو تغطية متنوعة بشكل مقصود .
القياسات الملائمة مع مزجات موضوعية متعددة: في الـ"pre"ـ--المرشح يستخدم العديد من القناصات |(ـk"ـ -- يعنى -- التجمع على شكل عينة مقسمة "M SK5"- للتأكد من أن المواضيع الأقلية "MSC6"ـ لا يتم استبعادها بطريقة منهجية .- هذا يمنع ـ"ـ تحيز موضوعي رئيسي "Mska9"ـ حيث ينزل مركز واحد "Msek10"ـ إلى مستوى مهم "Msec11"ـ لكن "M Sec12"ـ محتويات نادرة مثل القيود "Msc13"ـ إستثناءات "M sec14"ـ أو إفتراضات "م Sec15"
من 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));
هذا بحث - مُعلم ( يتجنب الإنهيار الفردي | - | تخريب استرجاع الطلب |) | لكن عمليا ♪ - | يعمل في ثوان على ال CPU ♪
لماذا لا ندمج كل شيء
بالنسبة لوثيقة 500-صفحة (2,000+segments), تداخل كل شيء سيعمل ولكن ليس كافياً
العينات متعددة -anchor تعطيك أفضل من كلاهما : تغطية موضوعية واسعة مع حوسبة قابلة للتعامل
ما وصفته سابقاً هو "ماسك 1" ليس مجرد "ماسک 2" والحصول على "ماكس 3" و "ماك 4" بل هو "مسك 5" وهو نمط معين أسميه سحب السياق الخافت المقيد (CFCD). الإدراك
معظم المختصين يواصلون إضافة السياق. DocSummarizer يسحب إلى الأمام فقط ما يحافظ على الانتقاء المعرفي, عندها يسمح للنموذج بكتابة fluently داخل تلك الحدود.
هنا ' كيف يرسم خط أنابيب DocSummarizer إلى CFCD
| مبدأ CFCD | تطبيق DocSummarizer |
|---|---|
| الكشف عن اللعاب (fuzzyM SK1 | ملحقات, شبيهة مركزيةMSC4 TFMske5 مركزية IDF |
| التعزيز المعرفي | MMRM SK1 BM25, الإنصهار RRFMST3 أعلىMSC4الانتخاب |
| دليل القنابل | الجزء الذي تم الحصول عليه مكون من أرقام الاقتباس |
| الجيل القياسي | عملية التمثيل محصورة من خلال الأدلة المستخرجة |
لماذا هذا مهم: النموذج لا يقرر ' ما هو ذا صلة |' | - | خط الإستخراج |. | النموذج ينتج بسلاسة فقط ضمن الحدود التي قمنا بوضعها | ' | و هذا هو السبب في عمل نماذج محلية صغيرة | : | المثقوب تقوم بالرفع الثقيل |.
في الواقع، "كتابة القنابل" تبدو مثل هذا (من الناحية النظريةM SK3
{
"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"
}
}
ثم في عملية التحليل prompt:
لهذا السبب
CFCD هو نفس الانقسام الفيزيائي كما الغموض المقيد, MoM مقيدة, و توصيل الصورة - تقترح إحتمالية, يستمر التفاضل والتكامل - يتم تطبيقه على طول الزمنM SK3 محور الذاكرةMSC4
نماذج ONNX يمكن قياسها (دقة أقل) لصغر الحجم وأكثر استنتاجًا سريعًاM SK2 التجارة - تعني خسارة نوعية بسيطةMSC4
| النماذج | الدقة الكاملة | الكمية | الاختلافات في الجودة S | |||
|---|---|---|---|---|---|---|
| كل-MiniLMM SK2LMST3vMSC4 | 90MB | \23MB | ||||
| bge-small -enM SK3vMSC4 | 133MB | \34 | MB |
بالنسبة للوثائق الكبيرة , تجميع الحزم مهم جداً لأداء 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);
الوثائق الكبيرة جداً (novels, الوثائق القانونيةM SK2 تحتاج إلى معالجة خاصة للتجنب
// 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);
}
أداء حقيقي-عالمي على آلة مطورة تقليدية (ريزن 5600XM SK3 32GB RAMMスク5 لا GPUMSC6
| عمليات | 吞吐ة | ملاحظات | |||
|---|---|---|---|---|---|
| الدمج | ~150 أجزاء/sec | حجم المجموعة 64, كلها -MiniLM MSc6L MScs7v MSC8 مقدرة msc9 | |||
| إعادة البحث الكثيفة | <10ms | شبيهة الجسيمات على أجزاء | 500 | ||
| درجات BM25 | <5ms | في-مؤشر الذاكرة المعكسة | |||
| الإنصهار RRF | <2ms S | تجميع 3 التصنيفات M | |||
| النهاية-toM SK1end (25-page PDFMSC3 includes chunking, embedding, retrieval , LLM synthesis M SK6 |
بيئة الاختبار: Ryzen 5600X (6-core), 32GB RAMM SK5 no GPUMSC6 Embedding uses allMST7MiniLMMSP8LMSSK9vMS2 ♫(quantizedMSR12 |MSR13 token maxMSL14 ♪MSR15thread parallel batchingMSF16 Retrieval corpusMSV17 .MSF18 segmentsMSS19 your mileage will vary with different modelsMSSR20 hardwareMSSA21 and document complexityMSA22
محرك رئيسي هو دمج الإنتاجية (إختيار النماذج | + | طول الرموز |+ | حجم العينة | МSK3 | إستخراج و الإنصهار في الأساس مجانية ♫- | تستغرق الميلي ثانية | . | هذا يعزز | | الـ " | LLM الأخير | مSK7 | المبدأ | ماSK8 | عمل المعالجات المركزية الرخيصة | من ثمة ( | التداخل | بِ , | الاستخراج | و ) | أولاً | إلى , | العمل الـ LLM الثمني فقط على المحتوى المجهول |
التدرج: يعالج الاستخراج الهرمي 500+ مخطوطات الصفحات | ( | نوبل |, | دليل | МSK4 | من خلال المعالجة في مجموعات وحفظها فقط في الأعلى | - | K لكل مجموعة في الذاكرة |
DocSummarizer يوضح أن إمكانية NLP المتطورة لا تحتاج إلى APIs أو إعتمادات Python.
الملاحظات الرئيسية من بناء هذا الأداة:
هذا يخلص لسلسلة DocSummarizer
الجزء 1 يشرح لماذا نهج خط الأنابيب يتغلب على مكالمات LLM ساذجة. إنه يغطي أنماط الهندسة المعمارية (تشنجM SK2 التقليل الهرمي , تصديق الإقتباسات ) التي تجعل أي summarizer يعمل جيداًMSC5 الأداة قد تطورت منذ أن أُكتبت الجزء | 1 |, | لكن المبادئ مازالت صالحة
الجزء 2 هو مؤشرك السريع-إرشاد البداية .التركيب,مداراتM SK3نماذج templatesMSC4 حالات الاستخدام الشائعةـ. إذا أردت فقط أن تستخدم الأداةMST6 التيـMST7 هي كل ما تحتاجه
الجزء 3 (this article) is the deep dive for people who want to understand كيف إنه بالفعل يعمل: بيرت مقابل محولات الجملة, لماذا يهم ONNX , توكنالوجيا جوتchasM SK3 تجارة البحث الهجينةMSC4 الإختلافاتMNK5 وما ينكسر في الإنتاجMRK6
إذا كنت ستقوم ببناء خط أنابيب الخاص بك، ستقرأ كل الثلاثة، ستقراء ثلاثة، إذا كنت تستخدم فقط الأداة، ستقر الجزء، ستفهم الجزء، و ربما تتصفح الجزء،
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.