RAG v produkci: chunking, retrieval a poctivé citace
Řez naslepo po 512 znacích zničí 4 z 32 odpovědí dřív, než je retriever uvidí. Oprava chunkeru posune rank 115 na 3.
Na této stránce
Tady je skutečná otázka od skutečného uživatele skutečného asistenta: moje eval sada má 20 položek, stačí to, abych skóre věřil. Korpus odpověď obsahuje — celou její sekci. Tady jsou čtyři fragmenty, které retriever skutečně vložil do prompt.
[1] d=0.578 ship — that set has been used for fitting, and its score stops being
unbiased. Measured on this belt: sweeping the threshold on the
validation set picks 0.196, and the model then scores F1 = 0.4122…
[2] d=0.602 ng when the model is confidently **wrong**. Evaluate both at a few
scores, for an example whose true label is 1: | score | p | …
[3] d=0.613 ard and watch both numbers: | | reward model's score | true quality
| length produced | … The reward went up by a factor of 2.5. The…
[4] d=0.617 | 0.6 | +0.97 | +1.00 | +0.27 | … The reward model is working
perfectly. It has faithfully learned the preferences it was shown…Tři ze čtyř začínají uprostřed slova. Dva jsou z jiné kapitoly o jiném tématu. A fragment, který na otázku odpovídá — ten obsahující Sedmnáct z dvaceti nedokáže odlišit 85% model od 65% modelu — se vrátil na rank 115.
Teď stejná otázka, stejný embedding model, stejná prompt šablona. Změnila se jedna věc: jak byly dokumenty rozřezány.
[1] d=0.594 [Classification, Cross-Entropy… > How many test examples do I need?]
Read it backwards, which is how you will use it: ±5 points needs
about 200 examples. ±2 points needs about 1,230…
[2] d=0.598 [Classification, Cross-Entropy… > Three splits, and the leak…]
Why three splits and not two? Because the moment you use a set of
examples to *choose* anything…
[3] d=0.600 [Classification, Cross-Entropy… > How many test examples do I need?]
The honest reading of 17/20 is *somewhere between 64 % and 95 %*.
…Seventeen out of twenty cannot distinguish an 85 % model from a 65 % one.
[4] d=0.605 [Classification, Cross-Entropy… > How many test examples do I need?]
Suppose you score a model on 20 examples and it gets 17 right. You
report 85 %. …Wilson 95% CI : [0.6396, 0.9476]Z rank 115 na rank 3. Nikdo nesáhl na model, prompt, práh ani počet slotů. Tato kapitola je o této mezeře a o čtyřech dalších místech, kde vám retrieval systém potichu lže.
Zobrazit podrobnosti
Co tato kapitola potřebuje z předchozích kapitol a jediné místo, kde mění jazyk.
- Kapitola 1 definovala skalární součin a L2 normu. Sekce o prahu níže je právě o těchto dvou věcech a o ničem jiném.
- Kapitola 8 oddělila embedding tabulku jazykového modelu od retrieval embedding modelu trénovaného kontrastivně na dvojicích, měřila kosinovou podobnost a skončila slibem, že kapitola 19 dojde ke konkrétní hranici. Tady ten slib přichází na řadu. Nic z toho se neopakuje.
- Kapitola 4 sestavila Wilsonův interval; kapitola 15 sestavila evaluační harness. Každá tabulka níže nese první z nich a vznikla pomocí druhého.
- Kapitola 16 ocenila context window. Prompt sestavený na konci této kapitoly stojí 591 tokenů a právě o tento rozpočet fragmenty soutěží.
Všechno tady je TypeScript, stejně jako od kapitoly 14, a tato kapitola je místem, kde si toto pravidlo vydělá na svou existenci: ingestion jsou fronty a úložiště, vyhledávání je síťové volání a sestavení prompt s citacemi je práce serveru. Měření je záměrně stejný kód se scoreboardem kolem — retriever skórovaný druhou implementací je číslo o softwaru, který nedodáváte, a kosinový práh níže je uvěřitelný jen proto, že ho sledujete při sweepu chunkerem, který poběží v produkci.
Korpus a co se počítá jako správná odpověď
Odkaz na sekci: Korpus a co se počítá jako správná odpověďVšechno níže je měřeno proti jednomu korpusu: prvním třinácti kapitolám tohoto kurzu — 13 dokumentů, 359 067 znaků, 127 sekcí, s odstraněným front matterem a bibliografiemi. Je to skutečný technický korpus s prózou, tabulkami, vzorci a code blocks a je to přesně ten typ obsahu, který lidé nahrají do knowledge base a pak si stěžují.
Ground truth tvoří 32 otázek, každá spárovaná s jehlou: krátkou doslovnou větou z korpusu, která na ni odpovídá. Každá jehla se v 359 067 znacích objevuje přesně jednou a žádná není nadpis sekce — na této kontrole záleží, protože chunker, který kopíruje nadpisy do každého chunk, by se jinak sám skóroval. Každá otázka je položena dvakrát, jednou v angličtině kurzu a jednou tak, jak by ji formuloval ticket podpory: 64 queries nad 32 ground truths.
Retrieval je správný, když vrácený chunk obsahuje celou jehlu vcelku. To je jediná definice, která odpovídá tomu, co generátor potřebuje: půl věty v prompt není odpověď, ale riziko.
Embedding model je all-MiniLM-L6-v2 — 384 dimenzí, mean-pooled a normalizovaný, kontrastivně trénovaný model měřený v kapitole 8. Indexace korpusu trvá 20,8 sekundy na CPU, 22 ms na chunk; embedding jedné query trvá 13 ms.
Chunking měřený šesti způsoby
Odkaz na sekci: Chunking měřený šesti způsobyŠest strategií ze tří nezávislých ingrediencí. Blind řeže každých 512 znaků bez ohledu na text. Boundaries nikdy neřeže uvnitř odstavce a na hranici věty se vrací jen tehdy, když je jeden odstavec přes rozpočet. Header před každý chunk přidá název dokumentu a cestu sekcí. Overlap zkopíruje posledních 64 znaků předchozího chunk do dalšího.
| strategy | chunks | answers destroyed | R@1 | R@4 | R@8 | R@20 | MRR |
|---|---|---|---|---|---|---|---|
| A blind 512 | 708 | 4 / 32 | 0.125 | 0.297 | 0.422 | 0.594 | 0.241 |
| B blind + overlap | 809 | 0 | 0.172 | 0.391 | 0.453 | 0.625 | 0.286 |
| C boundaries | 940 | 0 | 0.156 | 0.422 | 0.531 | 0.672 | 0.293 |
| D boundaries + overlap | 940 | 0 | 0.156 | 0.359 | 0.516 | 0.656 | 0.277 |
| E boundaries + header | 940 | 0 | 0.094 | 0.422 | 0.578 | 0.828 | 0.280 |
| F boundaries + header + overlap | 940 | 0 | 0.156 | 0.391 | 0.562 | 0.766 | 0.298 |
U 64 queries je 95% Wilsonův interval pro R@20 [0.471, 0.705] pro A a [0.718, 0.901] pro E — ty se nepřekrývají, ale většina ostatních sloupců ano a nepárovaná tabulka je nedokáže oddělit. Každá strategie odpovídá na stejné queries, takže poctivý test je párovaný: spočítat výhry a prohry každé strategie proti jiné a na neshodných párech spustit znaménkový test. Přežijí tři výsledky.
Blind chunking přímo zničí čtyři z dvaatřiceti odpovědí. Ne že je špatně zařadí — zničí je. Jehla přetíná hranici 512 znaků, takže žádný chunk v indexu ji neobsahuje a strop recall pro tyto queries je nula. Žádný reranker je neobnoví, žádný práh nepomůže, žádný větší model nepomůže. Nemůžete retrieval text, který nikde ve vašem indexu není v jednom kuse. To je jednoznačně nejméně reportované selhání v RAG, protože vypadá přesně jako špatný retriever.
Overlap opraví právě to a nic jiného. Každá strategie s overlap ztratí nula odpovědí, což je smysl overlap. Nezlepší ranking: B proti A na R@8 je +8/−6, p = 0.79; na R@20 je +9/−7, p = 0.80. Ještě hůř, přidání overlap nad header aktivně škodí — F proti E je +2/−6 na R@20 — a důvod je mechanický. Vektor chunk je průměr přes jeho tokeny, takže 64 znaků předchozího chunk táhne tento průměr směrem k tématu souseda. Overlap je pojištění proti rozdělené odpovědi, zaplacené přesností.
Contextual header kupuje retrieval. E proti A je +18/−3 na R@20, p = 0.0015. A ablation říká, že to nedělají boundaries: E proti C — stejné řezy, header je jediný rozdíl — je +12/−2, p = 0.0129. Přidání prefixu „Classification, Cross-Entropy, and How Not to Fool Yourself > How many test examples do I need?“ před odstavec říká embedding modelu, o čem odstavec je, což samotný odstavec často neříká. Je to resolver zájmen pro dokumenty.
To dává chunkeru jeho tvar a jedno pravidlo, které se snadno pokazí:
export interface Chunked {
/** What gets EMBEDDED: contextual header + this chunk's own content. */
text: string;
/** ONLY this chunk's own content: what is quoted back to the user. */
content: string;
section: string;
/** Character range in the document's canonical text. Sliceable. */
from: number;
to: number;
}
export function chunkDocument(doc: string, docTitle: string, target = 512): Chunked[] {
const out: Chunked[] = [];
const heads = [...doc.matchAll(/^## (.+)$/gm)].map((m) => ({ at: m.index!, title: m[1].trim() }));
const spans = heads.length
? heads.map((h, i) => ({ ...h, end: i + 1 < heads.length ? heads[i + 1].at : doc.length }))
: [{ at: 0, title: "", end: doc.length }];
for (const s of spans) {
const header = s.title ? `${docTitle} > ${s.title}` : docTitle;
const skip = /^## .+\n/.exec(doc.slice(s.at, s.end))?.[0].length ?? 0;
const body = doc.slice(s.at + skip, s.end);
const origin = s.at + skip;
// The offset is FOUND in the document, never accumulated: adding up
// lengths drifts by a character wherever a separator was normalised,
// and a citation anchor off by one points at the wrong line.
const emit = (from: number, to: number) => {
const raw = body.slice(from, to);
const lead = raw.length - raw.trimStart().length;
const content = raw.trim();
if (!content) return;
out.push({ text: `[${header}]\n${content}`, content, section: s.title,
from: origin + from + lead, to: origin + from + lead + content.length });
};
let open: [number, number] | null = null;
for (const m of body.matchAll(/[^\n]([^\n]|\n(?!\n))*/g)) { // paragraphs
const [pf, pt] = [m.index!, m.index! + m[0].length];
if (pt - pf > target) { // one huge paragraph
if (open) { emit(open[0], open[1]); open = null; }
let cur: [number, number] | null = null;
for (const sm of body.slice(pf, pt).matchAll(/[^.!?]*[.!?]*\s*/g)) {
if (!sm[0]) continue;
const [sf, st] = [pf + sm.index!, pf + sm.index! + sm[0].length];
if (cur && st - cur[0] > target) { emit(cur[0], cur[1]); cur = null; }
cur = cur ? [cur[0], st] : [sf, st];
}
if (cur) emit(cur[0], cur[1]);
continue;
}
if (open && pt - open[0] > target) { emit(open[0], open[1]); open = null; }
open = open ? [open[0], pt] : [pf, pt];
}
if (open) emit(open[0], open[1]);
}
return out;
}Dva texty, ne jeden. text je to, co se embedduje, včetně header. content jsou jen vlastní slova tohoto chunk a právě ta se citují zpět uživateli. Citujte text a citace ukáže header, který v dokumentu na daném místě není — a s overlap také opakovaný konec patřící předchozímu fragmentu. Pak zobrazuje text, který není tam, kde tvrdí, že je, což je horší než neukázat nic.
Header není zadarmo. Napříč 940 chunks stojí 24 213 ze 114 275 embedded tokenů indexu: 21,2 % toho, za co platíte při embedding, je header, který jste sami napsali. Také tlačí chunks proti oknu encoderu. all-MiniLM-L6-v2 přijímá 256 word-pieces; strategie E má 17 chunks nad touto hranicí a F jich má 28, každý z nich tiše oříznutý bez jakéhokoli varování. Vaše efektivní velikost chunk není číslo ve vaší konfiguraci — je to menší hodnota z ní a okna vašeho encoderu.
Dvacet řádků BM25, které každý přeskočí
Odkaz na sekci: Dvacet řádků BM25, které každý přeskočíDense retrieval má jednu systematickou slabinu a není nenápadná: páruje význam, takže je lhostejný k tomu, který přesný řetězec jste napsali. Číslo dílu, chybový kód, zkratka, příjmení — nic z toho nemá užitečný význam k embedding a nejbližším sousedem chybového kódu je každý jiný chybový kód ve vašem korpusu.
Klasická odpověď je starší než celé toto téma a zabere dvacet řádků. BM25 skóruje dokument podle toho, jak často se v něm objevují termy query, tlumí každý term s rostoucí frekvencí a penalizuje dlouhé dokumenty, které shromáždí shody prostě svou délkou.1 Term přispívá
kde je počet výskytů termu v dokumentu, jeho délka, průměrná délka a a dvě konvenční konstanty — nastavuje, jak rychle opakování přestane pomáhat, jak tvrdě se trestá délka.
const toks = (s: string) => s.toLowerCase().match(/[a-z0-9]+/g) ?? [];
export class BM25 {
private tf: Map<string, number>[] = [];
private len: number[] = [];
private idf = new Map<string, number>();
private avg = 0;
private k1: number; private b: number;
constructor(docs: string[], k1 = 1.2, b = 0.75) {
this.k1 = k1; this.b = b;
const df = new Map<string, number>();
for (const d of docs) {
const t = new Map<string, number>(); const ws = toks(d);
for (const w of ws) t.set(w, (t.get(w) ?? 0) + 1);
for (const w of t.keys()) df.set(w, (df.get(w) ?? 0) + 1);
this.tf.push(t); this.len.push(ws.length);
}
this.avg = this.len.reduce((a, b) => a + b, 0) / this.len.length;
const N = docs.length;
for (const [w, n] of df) this.idf.set(w, Math.log(1 + (N - n + 0.5) / (n + 0.5)));
}
scores(query: string): number[] {
const q = toks(query);
return this.tf.map((tf, i) => {
const L = this.len[i]; let s = 0;
for (const w of q) {
const f = tf.get(w); if (!f) continue;
s += (this.idf.get(w) ?? 0) * (f * (this.k1 + 1)) /
(f + this.k1 * (1 - this.b + (this.b * L) / this.avg));
}
return s;
});
}
}Nad 940 chunks to skóruje query za 1,14 ms bez jakéhokoli indexu mimo dvě hash mapy. A není to muzejní exponát:
| retriever | R@1 | R@4 | R@8 | MRR | cost per query |
|---|---|---|---|---|---|
| dense (cosine) | 0.094 | 0.422 | 0.578 | 0.280 | 13 ms na embedding + 0,3 ms na scan |
| lexical (BM25) | 0.219 | 0.375 | 0.469 | 0.313 | 1,14 ms |
| hybrid (RRF) | 0.203 | 0.484 | 0.609 | 0.346 | obojí |
| hybrid + cross-encoder | 0.312 | 0.578 | 0.703 | 0.447 | + 569 ms |
BM25 na tomto korpusu více než zdvojnásobuje top-1 přesnost dense retrieveru a u rank 8 s ním výrazně prohrává. Selhávají na různých queries, což je celý argument pro spouštění obou.
Jejich fúze je jediné místo, kde je zjevný přístup špatně. Kosinové vzdálenosti a BM25 skóre nejsou na stejné škále, nejsou stejně omezené a jejich normalizace per query způsobí, že váha závisí na tom, jak dobrý náhodou byl nejlepší zásah. Reciprocal rank fusion skóre zahodí a ponechá jen ranks:2
/** Reciprocal rank fusion: ranks, not scores. Nothing to calibrate. */
export function rrf(lists: number[][], k = 60): number[] {
const acc = new Map<number, number>();
for (const list of lists)
list.forEach((id, r) => acc.set(id, (acc.get(id) ?? 0) + 1 / (k + r + 1)));
return [...acc.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
}A tady je poctivé čtení tabulky důležitější než tabulka. Hybrid poráží BM25 na R@4 o +10/−3, p = 0.09. Poráží dense o +10/−6, p = 0.45. Na tomto korpusu, s 64 queries, hybrid retrieval není odlišitelný od dense retrieval. Je lepší v bodových odhadech i v každém sloupci recall a důkazy nedosahují významnosti. Téměř každý blog post o hybrid search na internetu reportuje tabulku jako tu výše a žádný interval; tohle říká interval.
Bi-encoder, cross-encoder a kde skutečně vzniká zlepšení
Odkaz na sekci: Bi-encoder, cross-encoder a kde skutečně vzniká zlepšeníVšechno doposud je bi-encoder: query projde modelem sama, každý chunk jím prošel sám před měsíci a dvojice se nikdy nepotká jinak než jako skalární součin. To umožňuje index — embed jednou, znovu používat navždy — a zároveň je to strop. Model se nikdy nedívá na query a chunk společně.
Cross-encoder dělá přesně to: vezme dvojici jako jeden vstup a vrátí skóre relevance. Nic nelze předpočítat, takže nemůže rankovat index — ale může rerankovat shortlist. Reranking hybrid top 25 pomocí ms-marco-MiniLM-L-6-v2 posune R@1 z 0.094 (dense) na 0.312 a MRR z 0.280 na 0.447: největší jednotlivé zlepšení v této kapitole a jediné, které se dotýká vrcholu seznamu místo jeho ocasu.
Stojí 569 ms na query na CPU oproti 1,14 ms pro BM25 a 0,3 ms pro vektorový scan. Zhruba dvoutisícinásobek retrieval nákladů pro dvacet pět dokumentů. To je celý trade-off bi-encoder/cross-encoder v jednom čísle a důvod, proč má architektura vždy stejný tvar: levný retriever s širokým recall, potom drahý scorer na shortlistu, který si můžete dovolit. ColBERT sedí mezi nimi, předpočítává vektory per token a dělá late interaction, která je levnější než cross-encoder a ostřejší než skalární součin.3
L2, kosinus a práh, který jste si nevysloužili
Odkaz na sekci: L2, kosinus a práh, který jste si nevysloužiliVektorové databáze reportují vzdálenosti a volba vzdálenosti je konfigurační možnost. Na normalizovaných vektorech je volba kosmetická a identitu stojí za to jednou provést, protože všechno po ní závisí na tom, že vektory jsou opravdu jednotkové. Pro :
takže kosinová vzdálenost je přesně . To je skalární součin a norma z kapitoly 1, vyplacené v hotovosti. Ověřeno na dvou skutečných vektorech chunk z indexu výše a potom přes 40 000 dvojic:
||a|| = 1.000000 ||b|| = 1.000000
L2 = 0.795183 L2^2/2 = 0.316158 1 - cos = 0.316158 diff = 7.66e-08
max |L2^2/2 - (1 - cos)| over 200 x 200 pairs = 8.3e-07Přesné až na floating-point šum — a jen proto, že vektory jsou normalizované. Přeskočte normalizaci a identita je nepravdivá, váš práh nic neznamená a vzdálenost reportovaná dokumentem závisí na tom, jak dlouhý byl jeho text.
Teď číslo, které nikdo neodvozuje. Retriever vždy něco vrátí: seřadí celý index a předá vám vrchol seznamu, ať už odpověď v korpusu je, nebo není. Práh je jediná část systému, která může říct ne — a k jeho nastavení potřebujete queries, které by neměly vrátit nic. Tady je třicet: dvacet jedna o věcech, které tento korpus skutečně nepokrývá — streaming, rate limits, prompt caching, JSON schémata, agent smyčky, vektorové databáze, prompt injection, generování obrázků — a devět o paelle, pasech a reklamačních pravidlech. Proti stejnému indexu:
| top-1 cosine distance | |
|---|---|
| in-domain queries, všech 64 | průměr 0.445, rozsah 0.270 – 0.721 |
| in-domain, top-1 skutečně správně | průměr 0.370 |
| in-domain, top-1 špatně | průměr 0.452 |
| out-of-domain, všech 30 | průměr 0.699, rozsah 0.497 – 0.867 |
Distribuce se oddělují a překrývají. Nejhorší in-domain query je dál od své odpovědi (0.721) než nejlepší out-of-domain query od irelevantního odstavce (0.497), takže žádný práh nedá oboje správně. Sweep přes skutečnou bránu — ponechat nejvýše čtyři chunks a pouze ty pod řezem:
| threshold | in-domain answered | of which the answer was in | out-of-domain answered |
|---|---|---|---|
| 0.400 | 17 / 64 | 6 | 0 / 30 |
| 0.450 | 38 / 64 | 13 | 0 / 30 |
| 0.500 | 50 / 64 | 19 | 1 / 30 |
| 0.525 | 52 / 64 | 20 | 2 / 30 |
| 0.550 | 55 / 64 | 21 | 3 / 30 |
| 0.600 | 60 / 64 | 25 | 5 / 30 |
| 0.675 | 62 / 64 | 27 | 10 / 30 |
| 0.800 | 64 / 64 | 27 | 26 / 30 |
| none | 64 / 64 | 27 | 30 / 30 |
Poslední sloupec čtěte jako blafy. Bez prahu asistent vytvoří sebejistou, dobře citovanou odpověď na „jak obnovím svůj španělský pas“ z korpusu o backpropagation, třicetkrát ze třiceti. Při 0.675 to udělá desetkrát ze třiceti. Při 0.525 to udělá dvakrát a vzdá se dvanácti otázek, které odpovědět mohl.
Tento trade-off je produktové rozhodnutí a jeho správný konec závisí na tom, kolik vás stojí špatná odpověď. Co není vyjednatelné, je samotná existence posledního sloupce. Pokud jste nikdy neměřili svůj retriever proti otázkám, které by měl odmítnout, nemáte práh — máte číslo.
Dva z deseti blafů při 0.675 ukazují dva způsoby, jak to selhává.
query: "how much does prompt caching save on a long conversation"
[1] d=0.497 13-inference-optimization > Prefill and decode are two different machines
[2] d=0.532 13-inference-optimization > The cache is also the bill
query: "what is the capital of france"
[1] d=0.671 12-reasoning > The model does not think. It computes for longer.
"…it is why 'think step by step' does nothing for what is the capital of France."První je těsný omyl: korpus detailně vysvětluje KV cache, query se týká prompt cache, slova jsou stejná slova a 0.497 je blíž než většina správných in-domain retrieval v celém experimentu. Embedding neví, že dvě cache se stejným názvem jsou různé stroje. Druhý je doslovná shoda bez odpovědi: korpus obsahuje přesnou frázi „jaké je hlavní město Francie“, použitou jako příklad otázky, která nepotřebuje reasoning. Retriever má pravdu; odpověď tam není. Jakýkoli systém, který čte „našel jsem něco podobného“ jako „našel jsem odpověď“, na tomto důkazu prohlásí Paříž — nebo, ještě hůř, ji neprohlásí.
Proč citaci nepíše model
Odkaz na sekci: Proč citaci nepíše modelModel nemá samostatnou schopnost pro fakta. Vytvořit pravdivou větu a vytvořit věrohodnou větu je tatáž operace — predikce dalšího tokenu z kapitoly 8 — a nic v této operaci neoznačuje, která je která. Analýza z roku 2025, která to přerámovala, tvrdí, že trénovací a evaluační pipeline aktivně odměňuje hádání: benchmarky skórují binární přesností a nedávají žádný kredit za zdržení se odpovědi, takže model, který odpovídá vždy, překoná identický model, který řekne „nevím“, když neví, a post-training podle toho optimalizuje.6 Halucinace v tomto čtení není tajemná vada. Je to to, co dostanete, když známkujete test s výběrem odpovědí bez penalizace za špatnou odpověď.
Sledujte její tvar. Na dotaz na osm článků o contrastive sentence embeddings, s identifikátory, Qwen2.5-0.5B-Instruct vytvořil osm řádků v perfektním formátu. Všech osm identifikátorů je dobře utvořených. Všech osm vede na skutečné články na arXiv. Nula z osmi je článek, za který se vydává.
claimed arXiv:1907.06432 - Contrastive Sentence Embeddings for Text Retrieval
actual A Neural Turing~Machine for Conditional Transition Graph Modeling
claimed arXiv:1809.08669 - Contrastive Learning of Sentence Representations…
actual Collapsing Superstring Conjecture
claimed arXiv:1807.08669 - Contrastive Learning of Sentence Representations…
actual Automatic Speech Recognition for Humanitarian Applications in SomaliJe to malý model a míra je jeho vlastní; frontier model si vymýšlí mnohem méně. Mechanismus se zobecňuje a právě on je důvodem následujícího pravidla. Validátor kontrolující „existuje tento identifikátor“ pustí všech osm a uživatel, který na jeden klikne, skončí na skutečné stránce ze skutečného archivu bez možnosti poznat, že mapování bylo vymyšlené. Selhání není v identifikátoru ani ve formátu. Je v asociaci — přesně v tom, co jazykový model produkuje podle věrohodnosti.
Takže: model píše [1] a [2] a nikdy nepíše odkaz. Čísla odkazují na fragmenty, které server retrieved, a server — který přesně ví, z jakého dokumentu a jakých offsetů každé číslo pochází — potom připojí dokument, popisek a URL. Model nemá co vymyslet, protože se ho nikdy neptáte na jedinou věc, kterou by si vymyslel.
export function buildContext(question: string, hits: Scored[]) {
const citations: Citation[] = hits.map((h, i) => ({
index: i + 1,
documentId: h.chunk.documentId,
documentName: h.chunk.documentName,
locatorLabel: label(h.chunk),
fragment: `#char=${h.chunk.locator.flow.from},${h.chunk.locator.flow.to}`,
quote: h.chunk.content, // the OWN content, never `text`
cosineDistance: h.cosineDistance,
}));
const blocks = citations
.map((c) => `[${c.index}] ${c.documentName} - ${c.locatorLabel}\n${c.quote}`)
.join("\n\n");
const prompt =
`Answer using ONLY the numbered sources below. Cite every claim as [n].\n` +
`If the sources do not contain the answer, say so and stop.\n\n` +
`SOURCES\n${blocks}\n\nQUESTION\n${question}`;
return { prompt, citations };
}Spusťte to na úvodní otázce a čtyři chunks se stanou prompt o 591 tokenů a tabulkou, kterou model nikdy nevidí:
[1] 04-classification How many test examples do I need? #char=28215,28701 d=0.594
[2] 04-classification Three splits, and the leak… #char=20329,20839 d=0.598
[3] 04-classification How many test examples do I need? #char=25873,26272 d=0.600
[4] 04-classification How many test examples do I need? #char=25554,25871 d=0.605Lokátor je část, kterou lidé přeskočí a pak ji nemohou dodat později. #char=25873,26272 je rozsah v kanonickém textu dokumentu; pro PDF je ekvivalent #page=12, pro audio nebo video #t=132.4,158.9, pro tabulku list a A1 range. Tyto dvě věci nejsou výmysly — #page= je PDF Open Parameters a #t= je W3C Media Fragments, které prohlížeče nativně respektují u video a audio prvků. Citace bez lokátoru je název dokumentu a název dokumentu není citace; je to návrh, aby se šel uživatel podívat.
A když nic neprojde prahem, pipeline se k modelu vůbec nedostane:
NO ANSWER: nothing under cosine distance 0.675 for "what is the offside rule in football"
NO ANSWER: nothing under cosine distance 0.675 for "how do i renew my spanish passport"
NO ANSWER: nothing under cosine distance 0.675 for "how do i build an agent loop with tools"To je levnější a spolehlivější odmítnutí než jakákoli instrukce v system prompt, protože jde o porovnání dvou čísel, ne o žádost na pravděpodobnostní systém.
Evaluujte retriever odděleně od generátoru
Odkaz na sekci: Evaluujte retriever odděleně od generátoruKaždé měření v této kapitole skóruje retriever a ani jednou nežádá model, aby napsal odpověď. Je to záměrné a je to část, kterou většina týmů přeskočí.
RAG systém má dva režimy selhání, které zvenčí vypadají totožně. Retriever pasáž nenašel; nebo ji našel a generátor ji ignoroval, odporoval jí nebo ji smíchal s něčím, čemu už věřil. Skórujte jen finální odpověď a ty dvě věci jsou nerozlišitelné, takže ladíte prompts proti problému, který žije ve vašem chunkeru. Recall@k, MRR a počet zničených odpovědí nepotřebují žádné generation volání, jsou dost levné na spuštění při každém deploy a jsou harness z kapitoly 15 s jinou skórovací funkcí — stejný request, deadline, concurrency a tally, nad fixní sadou otázek místo živé konverzace.
Reportujte je s intervaly. Aritmetika z kapitoly 4 platí beze změny: při 64 queries nese recall 0.5 95% Wilsonův interval zhruba ±0.12, takže strategie o čtyři body před jinou vám neřekla nic. Použijte párovaný test vždy, když obě strategie odpovídají na stejné otázky, což tady dělají vždy — právě to proměnilo „E vypadá lépe než A“ na p = 0.0015.
A poslední poctivost: RAG snižuje halucinace a neodstraňuje je. Vložení správné pasáže do prompt model nenutí ji použít a literatura to říká od původního článku.7 Dvě věci to v produkci zhoršují. Dlouhé contexts degradují — model nachází informace na začátku a konci dlouhého prompt spolehlivěji než uprostřed, takže dvacet chunks místo čtyř může snížit přesnost a zároveň zvýšit účet, což je efekt měřený v kapitole 24. A retrieval může být správný a přesto nedostatečný, jak ukázaly dvě cache výše. SelfCheckGPT označuje tvrzení, která nepřežijí resampling;8 Self-RAG trénuje model, aby emitoval vlastní retrieve-and-critique tokeny;9 TruthfulQA udělala tento režim selhání vůbec čitelným.10 Nic tuto mezeru nezavírá a systém, který prezentuje retrieved text jako důkaz, si spletl ozdrojované s pravdivým.
Polovina systému, která běží před jakoukoli query
Odkaz na sekci: Polovina systému, která běží před jakoukoli queryRetriever je viditelná část pipeline, jejíž selhání se všechna dějí dříve, ve tmě. Tři z nich se opakují.
Extrakce je místo, kde obsah umírá. PDF není text; jsou to instrukce ke kreslení. Dvousloupcové layouty se proplétají, tabulky se mění ve slovní polévku, záhlaví stránek se opakují do každého chunk a naskenovaná stránka nemá žádný text, dokud jí OCR nějaký nedá, i s confidence. Všechno měřené výše předpokládalo, že extractor odvedl svou práci; v produkci ji často neodvede a symptom se objeví jako špatný retrieval o tři vrstvy dál.
Index je orazítkovaný modelem, který ho postavil. Embeddings ze dvou modelů nejsou porovnatelné — ne „méně přesné“, ale neporovnatelné, protože jsou to body v různých prostorech. Změňte embedding model a každý vektor ve storu je odpad, dokud se nepřestaví. Proto se název modelu, počet dimenzí, verze pipeline a verze extractoru zapisují vedle každého dokumentu při indexaci. Bez nich v den upgradu nepoznáte, které dokumenty jsou zastaralé a které aktuální, a napůl migrovaný index vrací sebejistý nesmysl bez chyby kdekoli.
Jeden rozbitý dokument nesmí rozbít složku a čítače musí počítat, co se stalo. Dokument, jehož extrakce selže, skončí ve stavu failed se svým důvodem, viditelný a opakovatelný, zatímco dalších devadesát devět zůstane prohledatelných; a počet indexovaných chunks zapisuje server, když skončí, ne klient, když uploaduje. Složka, která hlásí 400 fragmentů a drží 40, je lež, která vyplave až jako nezodpověditelná otázka.
Kam to vede dál
Odkaz na sekci: Kam to vede dálSystém v této kapitole odpovídá na otázky, jejichž odpovědi jsou napsané. Retrievuje je, rankuje je, odmítá, když nemůže, a cituje, kde hledal. To je většina toho, co lidé chtějí od asistenta nad vlastními dokumenty, a je to omezené jedním konkrétním způsobem: retrieval může vrátit jen to, co někdo napsal.
Zbývá tedy druhá polovina. Něco z toho, co chcete, aby model dělal, vůbec není fakt v dokumentu — formát, který musí udržet, tón, taxonomie se čtyřmi sty štítky, způsob rozhodování žijící v deseti tisících minulých příkladech a v žádném odstavci nikde. Retrieval to dodat nemůže, protože není co retrievovat; delší prompt jen zaplatí účet z kapitoly 16 za popis skill místo skill.
Kapitola 20 je právě toto rozhodnutí — fine-tune, retrieve nebo prompt — a její zjištění je, že rozhodnutí je ekonomické dřív, než je technické: všechny tři možnosti jsou oceněny end to end na stejné otázce a crossover je počet tokenů. Otázka, která ji otevírá, je ta, na kterou tato kapitola odpovědět neumí. Ne kde je odpověď napsaná, ale co dělat, když nikdy napsaná nebyla.
Zdroje a metoda
Odkaz na sekci: Zdroje a metodaVšechno měřené v této kapitole použilo jeden korpus a jeden nástroj a obojí je reprodukovatelné. Korpus tvoří kapitoly 1 až 13 tohoto kurzu tak, jak stály 7. září 2026 — 13 dokumentů, 359 067 znaků, 127 sekcí, front matter a bibliografie odstraněny. Tyto kapitoly se dál upravují, takže použití stejného pravidla dnes napočítá o několik tisíc znaků víc: počet sekcí je beze změny a stejně tak každý závěr níže, ale celkový počet znaků je snapshot a je tak označen. Ground truth je 32 otázek, každá spárovaná s doslovnou větou, která se v korpusu vyskytuje přesně jednou a nikdy není nadpis sekce, položená ve dvou formulacích pro 64 queries. Retrieval embeddings jsou sentence-transformers/all-MiniLM-L6-v2 (384 dimenzí, mean-pooled, L2-normalizované, 256-token window); reranking je cross-encoder/ms-marco-MiniLM-L-6-v2 nad top 25; generation příklad je Qwen/Qwen2.5-0.5B-Instruct s greedy decoding. Všechna měření času jsou single-threaded CPU. K vytvoření této kapitoly nebylo zavoláno žádné placené API, což je také důvod, proč je každá latence zde lokální a je tak označena.
Chunker ukázaný v TypeScriptu je chunker, který byl měřen: Python nástroj implementující stejné pravidlo a ts/chunk.ts byly porovnány chunk po chunk přes celý korpus a shodují se na všech 940 chunks, textech i offsets. Intervaly jsou Wilsonovy na 95 %; párovaná porovnání jsou oboustranné přesné znaménkové testy na neshodných párech.
Všech čtrnáct identifikátorů citovaných výše bylo 7. září 2026 ověřeno proti arXiv API a zkontrolováno název po názvu — což se vzhledem k těm osmi, které nebyly správně, zdálo jako to nejmenší, co mohla právě tato kapitola udělat.
Reference
Odkaz na sekci: Reference-
Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval 3(4), pp. 333–389 (2009). Zdroj saturační funkce a dvou konstant použitých výše a místo, kde si přečíst, proč vůbec existuje. ↩
-
Cormack, G. V., Clarke, C. L. A. and Büttcher, S. Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR 2009. je jejich a smyslem metody je, že nepotřebuje žádnou kalibraci mezi škálami skóre, které slučuje. ↩
-
Khattab, O. and Zaharia, M. ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. arXiv:2004.12832 (2020). Střední cesta mezi skalárním součinem a cross-encoder. Reimers, N. and Gurevych, I., Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, arXiv:1908.10084 (2019), je bi-encoder, na kterém je postaven index této kapitoly a který byl měřen v kapitole 8. ↩
-
Malkov, Yu. A. and Yashunin, D. A. Efficient and Robust Approximate Nearest Neighbor Search using Hierarchical Navigable Small World Graphs. arXiv:1603.09320 (2016). Grafový index za většinou dnes prodávaných vektorových databází. ↩
-
Johnson, J., Douze, M. and Jégou, H. Billion-scale Similarity Search with GPUs. arXiv:1702.08734 (2017). FAISS a referenční implementace IVF měřeného v boxu výše. ↩
-
Kalai, A. T., Nachum, O., Vempala, S. S. and Zhang, E. Why Language Models Hallucinate. arXiv:2509.04664 (2025). Argument, že halucinace vzniká z hodnocení binární přesností, které nikdy neodměňuje zdržení se odpovědi, a proto je dřív evaluační problém než problém modelování. ↩
-
Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S. and Kiela, D. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. arXiv:2005.11401 (2020). Článek, který vzor pojmenoval, a ten, který si přečíst, chcete-li vědět, co opravuje a co ne. Guu et al., REALM: Retrieval-Augmented Language Model Pre-Training, arXiv:2002.08909 (2020), je souběžná práce, která trénuje retriever společně s modelem místo toho, aby ho připojila zvenku; Karpukhin et al., Dense Passage Retrieval for Open-Domain Question Answering, arXiv:2004.04906 (2020), je původ dvouencoderového dense retrieveru používaného v celé této kapitole; a Izacard and Grave, Leveraging Passage Retrieval with Generative Models for Open Domain Question Answering, arXiv:2007.01282 (2020), je fusion-in-decoder uspořádání pro krmení mnoha pasáží jednomu generátoru. Gao et al., Retrieval-Augmented Generation for Large Language Models: A Survey, arXiv:2312.10997 (2023), je mapa všeho, co přišlo potom, včetně HyDE (Gao et al., Precise Zero-Shot Dense Retrieval without Relevance Labels, arXiv:2212.10496, 2022), který embedduje hypotetickou odpověď místo otázky. ↩
-
Manakul, P., Liusie, A. and Gales, M. J. F. SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models. arXiv:2303.08896 (2023). Detekce resamplingem, bez přístupu k interním částem modelu a bez externí knowledge base. ↩
-
Asai, A., Wu, Z., Wang, Y., Sil, A. and Hajishirzi, H. Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. arXiv:2310.11511 (2023). Trénování modelu, aby rozhodoval, kdy retrievovat, místo retrieval na každém turn. ↩
-
Lin, S., Hilton, J. and Evans, O. TruthfulQA: Measuring How Models Mimic Human Falsehoods. arXiv:2109.07958 (2021). Benchmark postavený z otázek, kde se věrohodná odpověď a pravdivá odpověď liší, což je celá obtíž v jedné větě. ↩