RAG in Production: Chunking, Retrieval und ehrliche Zitationen
Blindes Schneiden bei 512 Zeichen zerstört Antworten. Besseres Chunking bringt Rang 115 auf 3 – bevor reranking hilft.
Auf dieser Seite
Hier ist eine echte Frage eines echten Nutzers eines echten assistant: my eval set has 20 items, is that enough to trust the score. Der Korpus enthält die Antwort — einen ganzen Abschnitt davon. Hier sind die vier Fragmente, die der retriever tatsächlich in den prompt gelegt hat.
[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…Drei der vier beginnen mitten im Wort. Zwei stammen aus einem anderen Kapitel zu einem anderen Thema. Und das Fragment, das die Frage beantwortet — das mit Seventeen out of twenty cannot distinguish an 85 % model from a 65 % one — kam auf Rang 115 zurück.
Jetzt dieselbe Frage, dasselbe embedding model, dasselbe prompt-Template. Eine Sache hat sich geändert: wie die Dokumente geschnitten wurden.
[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]Von Rang 115 auf Rang 3. Niemand hat das Modell, den prompt, den Schwellenwert oder die Anzahl der Slots angefasst. Dieses Kapitel handelt von dieser Lücke — und von den vier anderen Stellen, an denen ein retrieval-System dich leise belügt.
Details anzeigen
Was dieses Kapitel aus früheren Kapiteln braucht, und die eine Stelle, an der es die Sprache ändert.
- Kapitel 1 definierte das Skalarprodukt und die L2-Norm. Der Schwellenwert-Abschnitt unten besteht aus genau diesen beiden Dingen, und nichts anderem.
- Kapitel 8 trennte die embedding-Tabelle eines Sprachmodells von einem retrieval embedding model, das kontrastiv auf Paaren trainiert wurde, maß cosine similarity und endete mit dem Versprechen, dass Kapitel 19 bei einem konkreten Cut-off ankommen würde. Dieses Versprechen wird hier eingelöst. Nichts davon wird wiederholt.
- Kapitel 4 baute das Wilson-Intervall; Kapitel 15 baute den Evaluations-harness. Jede Tabelle unten trägt das erste und wurde vom zweiten erzeugt.
- Kapitel 16 bezifferte den Preis der context window. Der prompt, der am Ende dieses Kapitels zusammengesetzt wird, kostet 591 tokens, und das ist das Budget, um das die Fragmente konkurrieren.
Alles hier ist TypeScript, wie seit Kapitel 14, und dieses Kapitel ist der Ort, an dem sich die Regel verdient macht: Ingestion sind Queues und Storage, Suche ist ein Netzwerkaufruf, und das Zusammenbauen eines prompt mit Zitationen ist Serverarbeit. Die Messung ist absichtlich derselbe Code mit einem Scoreboard darum herum — ein retriever, der von einer zweiten Implementierung bewertet wird, liefert eine Zahl über Software, die du nicht auslieferst, und der cosine-Schwellenwert unten ist nur glaubwürdig, weil du zusiehst, wie er von dem Chunker gesweept wird, der in Production laufen wird.
Der Korpus, und was als richtige Antwort zählt
Link zum Abschnitt: Der Korpus, und was als richtige Antwort zähltAlles unten wird gegen einen Korpus gemessen: die ersten dreizehn Kapitel dieses Kurses — 13 Dokumente, 359.067 Zeichen, 127 Abschnitte, mit entfernten Front Matter und Bibliografien. Es ist ein echter technischer Korpus, mit Prosa, Tabellen, Formeln und Codeblöcken, und genau die Art von Material, die Leute in eine knowledge base laden und sich dann darüber beschweren.
Die Ground Truth besteht aus 32 Fragen, jeweils gepaart mit einer Needle: einem kurzen wörtlichen Satz aus dem Korpus, der sie beantwortet. Jede Needle erscheint genau einmal in den 359.067 Zeichen, und keine ist eine Abschnittsüberschrift — diese Prüfung ist wichtig, weil ein Chunker, der Überschriften in jeden Chunk kopiert, sich sonst selbst hochbewerten würde. Jede Frage wird zweimal gestellt, einmal im Englisch des Kurses und einmal so, wie ein Supportticket sie formuliert: 64 Queries über 32 Ground Truths.
Ein retrieval ist korrekt, wenn ein zurückgegebener Chunk die Needle vollständig enthält. Das ist die einzige Definition, die zu dem passt, was der Generator braucht: ein halber Satz im prompt ist keine Antwort, sondern ein Risiko.
Das embedding model ist all-MiniLM-L6-v2 — 384 Dimensionen, mean-pooled und normalisiert, das kontrastiv trainierte Modell, das Kapitel 8 gemessen hat. Das Indexieren des Korpus dauert 20,8 Sekunden auf einer CPU, 22 ms pro Chunk; das embedding einer Query dauert 13 ms.
Chunking, auf sechs Arten gemessen
Link zum Abschnitt: Chunking, auf sechs Arten gemessenSechs Strategien aus drei unabhängigen Zutaten. Blind schneidet alle 512 Zeichen, ohne den Text anzusehen. Boundaries schneidet nie innerhalb eines Absatzes und fällt nur dann auf eine Satzgrenze zurück, wenn ein Absatz das Budget überschreitet. Header stellt jedem Chunk seinen Dokumenttitel und Abschnittspfad voran. Overlap kopiert die letzten 64 Zeichen des vorherigen Chunks in den nächsten.
| Strategie | Chunks | zerstörte Antworten | 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 |
Bei 64 Queries liegt das 95-%-Wilson-Intervall für R@20 bei A bei [0.471, 0.705] und bei E bei [0.718, 0.901] — diese überlappen nicht, aber die meisten anderen Spalten tun es, und eine ungepaarte Tabelle kann sie nicht trennen. Jede Strategie beantwortet dieselben Queries, also ist der ehrliche Test gepaart: Zähle die Gewinne und Verluste jeder Strategie gegenüber einer anderen und führe einen Vorzeichentest auf den diskordanten Paaren aus. Drei Ergebnisse überstehen ihn.
Blindes chunking zerstört vier der zweiunddreißig Antworten vollständig. Es rankt sie nicht schlecht — es zerstört sie. Die Needle liegt über einer 512-Zeichen-Grenze, sodass kein Chunk im Index sie enthält, und die Recall-Obergrenze für diese Queries ist null. Kein reranker holt sie zurück, kein Schwellenwert hilft, kein größeres Modell hilft. Du kannst keinen Text abrufen, der nirgendwo in deinem Index am Stück vorkommt. Das ist der am meisten unterschätzte Fehler in RAG, weil er genau wie ein schlechter retriever aussieht.
Overlap behebt das und sonst nichts. Jede Strategie mit overlap verliert null Antworten, genau dafür ist overlap da. Er verbessert das Ranking nicht: B gegen A bei R@8 ist +8/−6, p = 0.79; bei R@20 ist es +9/−7, p = 0.80. Schlimmer: overlap zusätzlich zum header schadet aktiv — F gegen E ist +2/−6 bei R@20 — und der Grund ist mechanisch. Der Vektor eines Chunks ist ein Mittelwert über seine tokens, also ziehen 64 Zeichen des vorherigen Chunks diesen Mittelwert in Richtung des Nachbarthemas. Overlap ist eine Versicherung gegen eine geteilte Antwort, bezahlt mit precision.
Der kontextuelle header kauft das retrieval. E gegen A ist +18/−3 bei R@20, p = 0.0015. Und die Ablation sagt, dass es nicht an den boundaries liegt: E gegen C — dieselben Schnitte, header als einziger Unterschied — ist +12/−2, p = 0.0129. Einem Absatz „Classification, Cross-Entropy, and How Not to Fool Yourself > How many test examples do I need?“ voranzustellen, sagt dem embedding model, worum es in dem Absatz geht, was der Absatz selbst oft nicht sagt. Es ist ein Pronomenauflöser für Dokumente.
Das gibt dem Chunker seine Form, und eine Regel, die leicht falsch zu machen ist:
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;
}Zwei Texte, nicht einer. text ist das, was eingebettet wird, header und alles. content sind nur die eigenen Wörter dieses Chunks, und genau das wird dem Nutzer zurückzitiert. Zitierst du text, zeigt die Zitation einen header, der an dieser Stelle nicht im Dokument steht — und, mit overlap, ein wiederholtes Ende, das zum vorherigen Fragment gehört. Sie zeigt dann Text an, der nicht dort steht, wo sie behauptet. Das ist schlimmer, als nichts zu zeigen.
Der header ist nicht gratis. Über die 940 Chunks hinweg kostet er 24.213 der 114.275 eingebetteten tokens des Index: 21,2 % dessen, wofür du beim embedding bezahlst, ist ein header, den du selbst geschrieben hast. Er drückt Chunks außerdem gegen das Fenster des Encoders. all-MiniLM-L6-v2 akzeptiert 256 Word-Pieces; Strategie E hat 17 Chunks über dieser Linie und F hat 28, jeder davon stillschweigend und ohne Warnung von irgendwem abgeschnitten. Deine effektive Chunk-Größe ist nicht die Zahl in deiner Config — sie ist das Minimum aus dieser Zahl und dem Fenster deines Encoders.
Zwanzig Zeilen BM25, die alle überspringen
Link zum Abschnitt: Zwanzig Zeilen BM25, die alle überspringenDense retrieval hat eine systematische Schwäche, und sie ist nicht subtil: Es matcht Bedeutung, also ist ihm egal, welche genaue Zeichenfolge du getippt hast. Eine Teilenummer, ein Fehlercode, ein Akronym, ein Nachname — nichts davon hat eine nützliche Bedeutung zum embedding, und der nächste Nachbar eines Fehlercodes ist jeder andere Fehlercode in deinem Korpus.
Die klassische Antwort ist älter als all das und braucht zwanzig Zeilen. BM25 bewertet ein Dokument danach, wie oft die Terme der Query darin erscheinen, dämpft jeden Term, wenn seine Häufigkeit steigt, und bestraft lange Dokumente, die allein durch ihre Länge Treffer ansammeln.1 Term trägt bei
wobei die Häufigkeit des Terms im Dokument ist, seine Länge, die durchschnittliche Länge, und und die zwei üblichen Konstanten — legt fest, wie schnell Wiederholung nicht mehr hilft, , wie stark Länge bestraft wird.
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;
});
}
}Über 940 Chunks bewertet das eine Query in 1,14 ms, ganz ohne Index außer zwei Hash Maps. Und es ist kein Museumsstück:
| retriever | R@1 | R@4 | R@8 | MRR | Kosten pro Query |
|---|---|---|---|---|---|
| dense (cosine) | 0.094 | 0.422 | 0.578 | 0.280 | 13 ms für embedding + 0,3 ms fürs Scannen |
| lexical (BM25) | 0.219 | 0.375 | 0.469 | 0.313 | 1,14 ms |
| hybrid (RRF) | 0.203 | 0.484 | 0.609 | 0.346 | beides |
| hybrid + cross-encoder | 0.312 | 0.578 | 0.703 | 0.447 | + 569 ms |
BM25 verdoppelt die Top-1-Accuracy des dense retriever auf diesem Korpus mehr als, und verliert bis Rang 8 deutlich gegen ihn. Sie scheitern an unterschiedlichen Queries, und genau das ist das ganze Argument dafür, beide laufen zu lassen.
Sie zu fusionieren ist die eine Stelle, an der der naheliegende Ansatz falsch ist. Cosine-Distanzen und BM25-Scores liegen nicht auf derselben Skala, sind nicht gleich begrenzt, und sie pro Query zu normalisieren lässt das Gewicht davon abhängen, wie gut der beste Treffer zufällig war. Reciprocal rank fusion wirft die Scores weg und behält nur die Ränge: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);
}Und hier ist die ehrliche Lesart der Tabelle wichtiger als die Tabelle. Hybrid schlägt BM25 bei R@4 mit +10/−3, p = 0.09. Es schlägt dense mit +10/−6, p = 0.45. Auf diesem Korpus, mit 64 Queries, ist hybrid retrieval nicht von dense retrieval zu unterscheiden. Es ist bei beiden Punktschätzungen und in jeder Recall-Spalte besser, aber die Evidenz erreicht keine Signifikanz. Fast jeder Hybrid-Search-Blogpost im Internet berichtet eine Tabelle wie die oben und kein Intervall; das ist, was das Intervall sagt.
Bi-encoder, cross-encoder, und wo der Lift wirklich liegt
Link zum Abschnitt: Bi-encoder, cross-encoder, und wo der Lift wirklich liegtAlles bisher ist ein bi-encoder: Die Query geht allein durch das Modell, jeder Chunk ging vor Monaten allein durch das Modell, und die beiden begegnen sich nie außer als Skalarprodukt. Genau das macht einen Index möglich — einmal embedden, für immer wiederverwenden — und es ist zugleich die Obergrenze. Das Modell sieht Query und Chunk nie zusammen.
Ein cross-encoder tut genau das: Er nimmt das Paar als eine Eingabe und gibt einen Relevanz-Score zurück. Nichts kann vorab berechnet werden, also kann er keinen Index ranken — aber er kann eine Shortlist reranken. Das reranking der hybrid Top 25 mit ms-marco-MiniLM-L-6-v2 verschiebt R@1 von 0.094 (dense) auf 0.312 und MRR von 0.280 auf 0.447: die größte einzelne Verbesserung in diesem Kapitel, und die einzige, die den Kopf der Liste statt des Tails betrifft.
Es kostet 569 ms pro Query auf einer CPU, gegenüber 1,14 ms für BM25 und 0,3 ms für den Vektor-Scan. Ungefähr zweitausendmal die retrieval-Kosten, für fünfundzwanzig Dokumente. Das ist der ganze bi-encoder/cross-encoder-Trade-off in einer Zahl, und deshalb hat die Architektur immer dieselbe Form: ein billiger retriever mit breitem Recall, dann ein teurer Scorer auf einer Shortlist, die du dir leisten kannst. ColBERT sitzt zwischen den beiden, berechnet Vektoren pro token vor und macht eine Late Interaction, die billiger als ein cross-encoder und schärfer als ein Skalarprodukt ist.3
L2, cosine, und ein Schwellenwert, den du dir nicht verdient hast
Link zum Abschnitt: L2, cosine, und ein Schwellenwert, den du dir nicht verdient hastVektordatenbanken melden Distanzen, und welche Distanz das ist, ist eine Konfigurationsoption. Auf normalisierten Vektoren ist die Wahl kosmetisch, und die Identität einmal herzuleiten lohnt sich, weil alles danach davon abhängt, dass die Vektoren wirklich Einheitsvektoren sind. Für :
also ist die cosine-Distanz exakt . Das ist Kapitel 1s Skalarprodukt und Norm, eingelöst. Geprüft an zwei echten Chunk-Vektoren aus dem Index oben und dann über 40.000 Paare:
||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-07Exakt bis auf Floating-Point-Rauschen — und nur, weil die Vektoren normalisiert sind. Überspringst du die Normalisierung, ist die Identität falsch, dein Schwellenwert bedeutet nichts, und die Distanz, die ein Dokument meldet, hängt davon ab, wie lang sein Text war.
Jetzt zu der Zahl, die niemand herleitet. Ein retriever gibt immer etwas zurück: Er sortiert den ganzen Index und reicht dir die Spitze der Liste, egal ob die Antwort irgendwo im Korpus steht oder nicht. Der Schwellenwert ist der einzige Teil des Systems, der nein sagen kann — und um einen zu setzen, brauchst du Queries, die nichts zurückbekommen sollten. Hier sind dreißig: einundzwanzig über Dinge, die dieser Korpus wirklich nicht abdeckt — Streaming, Rate Limits, prompt caching, JSON-Schemas, agent-Loops, Vektordatenbanken, prompt injection, Bilderzeugung — und neun über Paella, Reisepässe und Rückerstattungsrichtlinien. Gegen denselben Index:
| Top-1-cosine-Distanz | |
|---|---|
| In-domain-Queries, alle 64 | Mittelwert 0.445, Bereich 0.270 – 0.721 |
| In-domain, Top 1 tatsächlich korrekt | Mittelwert 0.370 |
| In-domain, Top 1 falsch | Mittelwert 0.452 |
| Out-of-domain, alle 30 | Mittelwert 0.699, Bereich 0.497 – 0.867 |
Die Verteilungen trennen sich, und sie überlappen. Die schlechteste In-domain-Query ist weiter von ihrer Antwort entfernt (0.721), als die beste Out-of-domain-Query von einem irrelevanten Absatz entfernt ist (0.497), also macht kein Schwellenwert beides richtig. Sweepen wir ihn über das echte Gate — höchstens vier Chunks behalten, und nur die unter dem Cut:
| Schwellenwert | In-domain beantwortet | davon mit Antwort enthalten | Out-of-domain beantwortet |
|---|---|---|---|
| 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 |
Lies die letzte Spalte als Bluffs. Ohne Schwellenwert produziert der assistant auf „how do I renew my Spanish passport“ aus einem Korpus über backpropagation eine selbstbewusste, sauber zitierte Antwort, dreißig von dreißig Mal. Bei 0.675 tut er es zehn von dreißig Mal. Bei 0.525 tut er es zweimal und gibt bei zwölf Fragen auf, die er hätte beantworten können.
Dieser Trade-off ist eine Produktentscheidung, und das richtige Ende davon hängt davon ab, was dich eine falsche Antwort kostet. Nicht verhandelbar ist, dass die letzte Spalte überhaupt existiert. Wenn du deinen retriever nie gegen Fragen gemessen hast, die er verweigern sollte, hast du keinen Schwellenwert — du hast eine Zahl.
Zwei der zehn Bluffs bei 0.675 zeigen die zwei Arten, wie das scheitert.
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."Der erste ist ein knapper Fehltreffer: Der Korpus erklärt den KV cache im Detail, die Query handelt vom prompt cache, die Wörter sind dieselben Wörter, und 0.497 ist näher als die meisten korrekten In-domain-retrievals im ganzen Experiment. Ein embedding weiß nicht, dass zwei Caches mit demselben Namen unterschiedliche Maschinen sind. Der zweite ist ein wörtlicher Treffer ohne Antwort: Der Korpus enthält die exakte Phrase „what is the capital of France“, verwendet als Beispiel für eine Frage, die kein Reasoning braucht. Der retriever hat recht; die Antwort ist nicht da. Jedes System, das „Ich habe etwas Ähnliches gefunden“ als „Ich habe die Antwort gefunden“ liest, wird auf dieser Evidenz Paris behaupten — oder, schlimmer, nicht.
Warum die Zitation nicht vom Modell geschrieben wird
Link zum Abschnitt: Warum die Zitation nicht vom Modell geschrieben wirdEin Modell hat keine separate Fähigkeit für Fakten. Einen wahren Satz zu produzieren und einen plausiblen zu produzieren sind dieselbe Operation — Kapitel 8s Next-token Prediction — und nichts in dieser Operation markiert, was was ist. Die Analyse von 2025, die das neu gerahmt hat, argumentiert, dass die Trainings- und Evaluationspipeline Raten aktiv belohnt: Benchmarks bewerten mit binärer Accuracy und geben keinen Credit fürs Enthalten, also schlägt ein Modell, das immer antwortet, ein identisches Modell, das „Ich weiß es nicht“ sagt, wenn es das nicht weiß; Post-Training optimiert entsprechend.6 Halluzination ist in dieser Lesart kein mysteriöser Defekt. Sie ist das, was du bekommst, wenn du eine Multiple-Choice-Prüfung ohne Abzug für falsche Antworten benotest.
Schau dir die Form an. Auf die Bitte nach acht Papers zu kontrastiven Sentence embeddings, mit Identifiers, produzierte Qwen2.5-0.5B-Instruct acht Zeilen in perfektem Format. Alle acht Identifiers sind wohlgeformt. Alle acht führen zu echten Papers auf arXiv. Null der acht sind das behauptete Paper.
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 SomaliDas ist ein kleines Modell, und die Rate ist seine eigene; ein Frontier-Modell erfindet weit weniger. Der Mechanismus verallgemeinert sich, und er ist der Grund für die folgende Regel. Ein Validator, der prüft „existiert dieser Identifier“, lässt alle acht durch, und ein Nutzer, der einen anklickt, landet auf einer echten Seite eines echten Archivs, ohne erkennen zu können, dass die Zuordnung erfunden wurde. Der Fehler liegt nicht im Identifier oder im Format. Er liegt in der Assoziation — genau dem Ding, das ein Sprachmodell über Plausibilität erzeugt.
Also: Das Modell schreibt [1] und [2], und nie den Link. Die Zahlen beziehen sich auf Fragmente, die der Server retrieved hat, und der Server — der exakt weiß, aus welchem Dokument und welchen Offsets jede Zahl stammt — hängt Dokument, Label und URL danach an. Es gibt nichts, was das Modell erfinden könnte, weil es nie nach genau dem einen Ding gefragt wird, das es erfinden würde.
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 };
}Lass es auf die Eingangsfrage laufen, und aus den vier Chunks werden ein 591-token-prompt und eine Tabelle, die das Modell nie sieht:
[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.605Der Locator ist der Teil, den Leute überspringen und später nicht mehr hinzufügen können. #char=25873,26272 ist ein Bereich im kanonischen Text des Dokuments; für ein PDF ist das Äquivalent #page=12, für Audio oder Video #t=132.4,158.9, für eine Tabelle ein Sheet und ein A1-Bereich. Diese beiden sind keine Erfindungen — #page= sind PDF Open Parameters und #t= sind W3C Media Fragments, nativ von Browsern auf Video- und Audioelementen unterstützt. Eine Zitation ohne Locator ist ein Dokumentname, und ein Dokumentname ist keine Zitation; er ist ein Vorschlag an den Nutzer, selbst suchen zu gehen.
Und wenn nichts den Schwellenwert passiert, erreicht die Pipeline das Modell gar nicht erst:
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"Das ist eine billigere und zuverlässigere Verweigerung als jede Anweisung in einem system prompt, weil es ein Vergleich zwischen zwei Zahlen ist statt einer Bitte an ein probabilistisches System.
Evaluiere den retriever getrennt vom Generator
Link zum Abschnitt: Evaluiere den retriever getrennt vom GeneratorJede Messung in diesem Kapitel bewertet den retriever und bittet nie ein Modell, eine Antwort zu schreiben. Das ist Absicht, und es ist der Teil, den die meisten Teams überspringen.
Ein RAG-System hat zwei Fehlermodi, die von außen identisch aussehen. Der retriever hat die Passage nicht gefunden; oder er hat sie gefunden und der Generator hat sie ignoriert, ihr widersprochen oder sie mit etwas vermischt, das er schon glaubte. Bewertest du nur die finale Antwort, sind die beiden ununterscheidbar, also optimierst du prompts gegen ein Problem, das in deinem Chunker lebt. Recall@k, MRR und die Anzahl zerstörter Antworten brauchen überhaupt keinen Generation-Call, sie sind billig genug, um bei jedem deploy zu laufen, und sie sind der harness aus Kapitel 15 mit einer anderen Scoring-Funktion — dieselbe Anfrage, Deadline, Nebenläufigkeit und Zählung, über ein fixes Fragenset statt einer Live-Konversation.
Berichte sie mit Intervallen. Kapitel 4s Arithmetik gilt unverändert: Bei 64 Queries trägt ein Recall von 0.5 ein 95-%-Wilson-Intervall von ungefähr ±0.12, also hat dir eine Strategie, die vier Punkte vor einer anderen liegt, nichts gesagt. Nutze den gepaarten Test, wann immer beide Strategien dieselben Fragen beantworten, was sie hier immer tun — das ist es, was „E sieht besser aus als A“ in p = 0.0015 verwandelt hat.
Und die letzte Ehrlichkeit: RAG reduziert Halluzination und entfernt sie nicht. Die richtige Passage in den prompt zu legen verpflichtet das Modell nicht, sie zu verwenden, und die Literatur sagt das seit dem ursprünglichen Paper.7 Zwei Dinge machen es in Production schlimmer. Lange Kontexte degradieren — ein Modell findet Informationen am Anfang und am Ende eines langen prompt zuverlässiger als in der Mitte, sodass zwanzig Chunks statt vier die Accuracy senken können, während sie die Rechnung erhöhen, ein Effekt, der in Kapitel 24 gemessen wird. Und retrieval kann richtig und trotzdem unzureichend sein, wie die zwei Caches oben gezeigt haben. SelfCheckGPT markiert Claims, die Resampling nicht überstehen;8 Self-RAG trainiert das Modell, seine eigenen Retrieve-and-Critique-tokens auszugeben;9 TruthfulQA machte den Fehlermodus überhaupt erst lesbar.10 Keines schließt die Lücke, und ein System, das retrieved Text als Beweis präsentiert, hat belegt mit wahr verwechselt.
Die Hälfte des Systems, die vor jeder Query läuft
Link zum Abschnitt: Die Hälfte des Systems, die vor jeder Query läuftEin retriever ist der sichtbare Teil einer Pipeline, deren Fehler alle früher passieren, im Dunkeln. Drei davon wiederholen sich.
Extraction ist der Ort, an dem der Inhalt stirbt. Ein PDF ist kein Text; es sind Zeichenanweisungen. Zweispaltige Layouts verschachteln sich, Tabellen werden zu Wortsuppe, Seitenköpfe wiederholen sich in jedem Chunk, und eine gescannte Seite hat überhaupt keinen Text, bis OCR ihr welchen gibt, mit einer Confidence. Alles oben Gemessene setzte voraus, dass der Extractor seinen Job gemacht hat; in Production tut er das oft nicht, und das Symptom erscheint drei Schichten weiter als schlechtes retrieval.
Der Index trägt den Stempel des Modells, das ihn gebaut hat. Embeddings aus zwei Modellen sind nicht vergleichbar — nicht „weniger genau“, sondern nicht vergleichbar, weil sie Punkte in unterschiedlichen Räumen sind. Änderst du das embedding model, ist jeder Vektor im Store Müll, bis er neu gebaut wird. Deshalb werden Modellname, Dimensionszahl, Pipeline-Version und Extractor-Version beim Indexieren neben jedes Dokument geschrieben. Ohne sie kannst du am Upgrade-Tag nicht sagen, welche Dokumente stale und welche aktuell sind, und ein halb migrierter Index gibt selbstbewussten Unsinn zurück, ohne irgendwo einen Fehler zu werfen.
Ein kaputtes Dokument darf nicht den Ordner kaputtmachen, und die Zähler müssen zählen, was passiert ist. Ein Dokument, dessen Extraction fehlschlägt, endet in einem failed-State mit seinem Grund, sichtbar und retryable, während die anderen neunundneunzig durchsuchbar bleiben; und die Anzahl der indexierten Chunks wird vom Server geschrieben, wenn er fertig ist, nicht vom Client deklariert, wenn er hochlädt. Ein Ordner, der 400 Fragmente meldet und 40 enthält, ist eine Lüge, die erst als unbeantwortbare Frage sichtbar wird.
Wohin es als Nächstes geht
Link zum Abschnitt: Wohin es als Nächstes gehtDas System in diesem Kapitel beantwortet Fragen, deren Antworten aufgeschrieben sind. Es retrieved sie, rankt sie, verweigert, wenn es nicht kann, und zitiert, wo es gesucht hat. Das ist das meiste von dem, was Menschen von einem assistant über ihre eigenen Dokumente wollen, und es ist auf eine spezifische Weise begrenzt: retrieval kann nur zurückgeben, was jemand geschrieben hat.
Damit bleibt die andere Hälfte. Manches, was ein Modell tun soll, ist überhaupt keine Tatsache in einem Dokument — ein Format, das es halten muss, ein Ton, eine Taxonomie mit vierhundert Labels, eine Entscheidungsweise, die in zehntausend vergangenen Beispielen lebt und in keinem Absatz irgendwo. Retrieval kann das nicht liefern, weil es nichts zu retrieven gibt; ein längerer prompt bezahlt nur Kapitel 16s Rechnung für die Beschreibung einer skill statt für die skill.
Kapitel 20 ist diese Entscheidung — fine-tune, retrieve oder prompt — und sein Befund ist, dass die Entscheidung wirtschaftlich ist, bevor sie technisch ist: Die drei werden end to end auf derselben Frage bepreist, und der Crossover ist eine token-Zahl. Die Frage, die es eröffnet, ist die, die dieses Kapitel nicht beantworten kann. Nicht wo steht die Antwort geschrieben, sondern was tust du, wenn sie es nie war.
Quellen und Methode
Link zum Abschnitt: Quellen und MethodeAlles in diesem Kapitel Gemessene verwendete einen Korpus und ein Instrument, und beide sind reproduzierbar. Der Korpus besteht aus den Kapiteln 1 bis 13 dieses Kurses, wie sie am 7. September 2026 standen — 13 Dokumente, 359.067 Zeichen, 127 Abschnitte, Front Matter und Bibliografien entfernt. Diese Kapitel werden weiter bearbeitet, also zählt dieselbe Regel heute ein paar tausend Zeichen mehr: Die Abschnittszahl ist unverändert und ebenso jede Schlussfolgerung unten, aber die Zeichenzahl ist ein Snapshot und als solcher gelabelt. Die Ground Truth besteht aus 32 Fragen, jeweils gepaart mit einem wörtlichen Satz, der genau einmal im Korpus vorkommt und nie eine Abschnittsüberschrift ist, gestellt in zwei Formulierungen für 64 Queries. Retrieval-embeddings sind sentence-transformers/all-MiniLM-L6-v2 (384 Dimensionen, mean-pooled, L2-normalisiert, 256-token-Fenster); reranking ist cross-encoder/ms-marco-MiniLM-L-6-v2 über die Top 25; das Generation-Beispiel ist Qwen/Qwen2.5-0.5B-Instruct mit greedy decoding. Alle Timings sind Single-Thread-CPU. Es wurde keine kostenpflichtige API aufgerufen, um dieses Kapitel zu erzeugen, weshalb auch jede Latenz hier lokal ist und entsprechend gelabelt wird.
Der in TypeScript gezeigte Chunker ist der Chunker, der gemessen wurde: Das Python-Instrument, das dieselbe Regel und ts/chunk.ts implementiert, wurde Chunk für Chunk über den ganzen Korpus mit ihm verglichen und stimmt bei allen 940 Chunks überein, Texte und Offsets eingeschlossen. Intervalle sind Wilson bei 95 %; gepaarte Vergleiche sind zweiseitige exakte Vorzeichentests auf den diskordanten Paaren.
Alle vierzehn oben zitierten Identifiers wurden am 7. September 2026 gegen die arXiv API aufgelöst und Titel für Titel geprüft — was angesichts der acht, die es nicht waren, das Mindeste schien, was dieses spezielle Kapitel tun konnte.
Referenzen
Link zum Abschnitt: Referenzen-
Robertson, S. and Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval 3(4), pp. 333–389 (2009). Die Quelle der Sättigungsfunktion und der zwei oben verwendeten Konstanten, und der Ort, an dem du nachlesen kannst, warum überhaupt existiert. ↩
-
Cormack, G. V., Clarke, C. L. A. and Büttcher, S. Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR 2009. Das ist ihres, und der Punkt der Methode ist, dass sie keine Kalibrierung zwischen den Score-Skalen braucht, die sie fusioniert. ↩
-
Khattab, O. and Zaharia, M. ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. arXiv:2004.12832 (2020). Der Mittelweg zwischen einem Skalarprodukt und einem cross-encoder. Reimers, N. and Gurevych, I., Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, arXiv:1908.10084 (2019), ist der bi-encoder, auf dem der Index dieses Kapitels gebaut ist und der in Kapitel 8 gemessen wurde. ↩
-
Malkov, Yu. A. and Yashunin, D. A. Efficient and Robust Approximate Nearest Neighbor Search using Hierarchical Navigable Small World Graphs. arXiv:1603.09320 (2016). Der Graphindex hinter den meisten Vektordatenbanken, die derzeit verkauft werden. ↩
-
Johnson, J., Douze, M. and Jégou, H. Billion-scale Similarity Search with GPUs. arXiv:1702.08734 (2017). FAISS, und die Referenzimplementierung des IVF, das in der Box oben gemessen wurde. ↩
-
Kalai, A. T., Nachum, O., Vempala, S. S. and Zhang, E. Why Language Models Hallucinate. arXiv:2509.04664 (2025). Das Argument, dass Halluzination durch Grading mit binärer Accuracy erzeugt wird, das Enthalten nie belohnt, und deshalb ein Evaluationsproblem ist, bevor es ein Modellierungsproblem ist. ↩
-
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). Das Paper, das das Muster benannt hat, und dasjenige, das du lesen solltest, um zu verstehen, was es behebt und was nicht. Guu et al., REALM: Retrieval-Augmented Language Model Pre-Training, arXiv:2002.08909 (2020), ist die zeitgleiche Arbeit, die den retriever gemeinsam mit dem Modell trainiert, statt ihn anzuflanschen; Karpukhin et al., Dense Passage Retrieval for Open-Domain Question Answering, arXiv:2004.04906 (2020), ist der Ursprung des two-encoder dense retriever, der in diesem Kapitel durchgehend verwendet wird; und Izacard and Grave, Leveraging Passage Retrieval with Generative Models for Open Domain Question Answering, arXiv:2007.01282 (2020), ist die Fusion-in-Decoder-Anordnung, um viele Passagen an einen Generator zu geben. Gao et al., Retrieval-Augmented Generation for Large Language Models: A Survey, arXiv:2312.10997 (2023), ist die Karte von allem, was danach kam, einschließlich HyDE (Gao et al., Precise Zero-Shot Dense Retrieval without Relevance Labels, arXiv:2212.10496, 2022), das eine hypothetische Antwort statt der Frage embeddet. ↩
-
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). Erkennung durch Resampling, ohne Zugriff auf die Interna des Modells und ohne externe 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). Das Modell zu trainieren, selbst zu entscheiden, wann es retrieven soll, statt bei jedem Turn zu retrieven. ↩
-
Lin, S., Hilton, J. and Evans, O. TruthfulQA: Measuring How Models Mimic Human Falsehoods. arXiv:2109.07958 (2021). Der Benchmark aus Fragen, bei denen die plausible Antwort und die wahre Antwort auseinanderfallen — die ganze Schwierigkeit in einem Satz. ↩