RAG di Production: Chunking, Retrieval, dan Sitasi yang Jujur
Potong buta di 512 karakter dan 4 dari 32 jawaban mati sebelum retriever melihatnya. Perbaiki chunker saja, rank 115 jadi 3.
Di halaman ini
Ini pertanyaan nyata dari pengguna nyata sebuah asisten nyata: eval set saya punya 20 item, apakah itu cukup untuk memercayai skornya. Corpus berisi jawabannya — satu bagian penuh. Berikut empat fragmen yang benar-benar dimasukkan retriever ke dalam 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…Tiga dari empat fragmen dimulai di tengah kata. Dua berasal dari bab lain tentang topik lain. Dan fragmen yang menjawab pertanyaan — yang berisi Seventeen out of twenty cannot distinguish an 85 % model from a 65 % one — kembali di rank 115.
Sekarang pertanyaan yang sama, embedding model yang sama, template prompt yang sama. Satu hal berubah: cara dokumen dipotong.
[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]Rank 115 menjadi rank 3. Tidak ada yang menyentuh model, prompt, threshold, atau jumlah slot. Bab ini membahas celah itu, dan empat tempat lain tempat sistem retrieval diam-diam berbohong kepadamu.
Tampilkan detail
Apa yang bab ini butuhkan dari bab-bab sebelumnya, dan satu tempat ia mengubah bahasa.
- Bab 1 mendefinisikan dot product dan norma L2. Bagian threshold di bawah adalah dua hal itu, dan tidak lebih.
- Bab 8 memisahkan embedding table milik language model dari model embedding retrieval yang dilatih secara contrastive pada pasangan, mengukur cosine similarity, dan berakhir dengan janji bahwa Bab 19 akan sampai pada cut-off yang konkret. Janji itu ditepati di sini. Tidak ada yang diulang.
- Bab 4 membangun interval Wilson; Bab 15 membangun evaluation harness. Setiap tabel di bawah membawa yang pertama dan diproduksi oleh yang kedua.
- Bab 16 memberi harga pada context window. Prompt yang dirakit di akhir bab ini memakan 591 token, dan itulah anggaran yang diperebutkan fragmen-fragmen.
Semua di sini adalah TypeScript, seperti sejak Bab 14, dan bab ini adalah tempat aturan itu membuktikan dirinya: ingestion adalah antrean dan storage, search adalah network call, dan merakit prompt dengan sitasi adalah pekerjaan server. Pengukurannya sengaja berupa kode yang sama dengan papan skor di sekelilingnya — retriever yang dinilai oleh implementasi kedua adalah angka tentang software yang tidak kamu kirim, dan cosine threshold di bawah hanya bisa dipercaya karena kamu melihatnya disapu oleh chunker yang akan berjalan di production.
Corpus, dan apa yang dihitung sebagai jawaban benar
Tautan ke bagian: Corpus, dan apa yang dihitung sebagai jawaban benarSemua di bawah diukur terhadap satu corpus: tiga belas bab pertama kursus ini — 13 dokumen, 359.067 karakter, 127 bagian, dengan front matter dan bibliografi dihapus. Ini corpus teknis nyata, berisi prosa, tabel, rumus, dan code block, dan persis jenis hal yang dimuat orang ke knowledge base lalu mereka keluhkan.
Ground truth-nya adalah 32 pertanyaan, masing-masing dipasangkan dengan needle: satu kalimat pendek verbatim dari corpus yang menjawabnya. Setiap needle muncul tepat sekali dalam 359.067 karakter, dan tidak ada yang merupakan heading bagian — pemeriksaan itu penting, karena chunker yang menyalin heading ke setiap chunk kalau tidak akan memberi skor untuk dirinya sendiri. Setiap pertanyaan diajukan dua kali, sekali dalam bahasa Inggris kursus dan sekali seperti cara tiket support merumuskannya: 64 query atas 32 ground truth.
Sebuah retrieval benar ketika chunk yang dikembalikan berisi needle secara utuh. Itulah satu-satunya definisi yang cocok dengan kebutuhan generator: setengah kalimat dalam prompt bukan jawaban, melainkan bahaya.
Embedding model-nya adalah all-MiniLM-L6-v2 — 384 dimensi, mean-pooled dan dinormalisasi, model yang dilatih contrastive yang diukur Bab 8. Mengindeks corpus memakan 20,8 detik di CPU, 22 ms per chunk; membuat embedding satu query memakan 13 ms.
Chunking, diukur enam cara
Tautan ke bagian: Chunking, diukur enam caraEnam strategi dari tiga bahan independen. Blind memotong setiap 512 karakter tanpa melihat teks. Boundaries tidak pernah memotong di dalam paragraf, dengan fallback ke batas kalimat hanya ketika satu paragraf melebihi anggaran. Header memberi prefiks pada setiap chunk dengan judul dokumen dan jalur bagian. Overlap menyalin 64 karakter terakhir dari chunk sebelumnya ke chunk berikutnya.
| 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 |
Dengan 64 query, interval Wilson 95 % pada R@20 adalah [0.471, 0.705] untuk A dan [0.718, 0.901] untuk E — keduanya tidak overlap, tetapi sebagian besar kolom lain overlap, dan tabel tak berpasangan tidak bisa memisahkannya. Setiap strategi menjawab query yang sama, jadi tes yang jujur adalah berpasangan: hitung kemenangan dan kekalahan setiap strategi terhadap strategi lain lalu jalankan sign test pada pasangan yang berbeda. Tiga hasil bertahan.
Blind chunking menghancurkan empat dari tiga puluh dua jawaban secara langsung. Bukan memberi rank buruk — menghancurkannya. Needle melintasi batas 512 karakter, jadi tidak ada chunk dalam index yang memuatnya, dan recall ceiling untuk query tersebut adalah nol. Tidak ada reranker yang memulihkannya, tidak ada threshold yang membantu, tidak ada model lebih besar yang membantu. Kamu tidak bisa mengambil teks yang tidak ada utuh di mana pun dalam index-mu. Ini adalah kegagalan RAG yang paling jarang dilaporkan, karena tampilannya persis seperti retriever yang buruk.
Overlap memperbaiki itu dan tidak ada yang lain. Setiap strategi dengan overlap kehilangan nol jawaban, dan memang itulah gunanya overlap. Ia tidak memperbaiki ranking: B melawan A pada R@8 adalah +8/−6, p = 0.79; pada R@20 adalah +9/−7, p = 0.80. Lebih buruk, menambahkan overlap di atas header justru merugikan — F melawan E adalah +2/−6 pada R@20 — dan alasannya mekanis. Vektor sebuah chunk adalah mean atas token-nya, jadi 64 karakter dari chunk sebelumnya menarik mean itu ke arah topik tetangganya. Overlap adalah asuransi terhadap jawaban yang terbelah, dibayar dengan precision.
Header kontekstual-lah yang membeli retrieval. E melawan A adalah +18/−3 pada R@20, p = 0.0015. Dan ablation mengatakan boundaries bukan penyebabnya: E melawan C — potongan yang sama, header satu-satunya perbedaan — adalah +12/−2, p = 0.0129. Memberi prefiks "Classification, Cross-Entropy, and How Not to Fool Yourself > How many test examples do I need?" pada sebuah paragraf memberi tahu embedding model tentang topik paragraf itu, sesuatu yang sering tidak dikatakan paragrafnya sendiri. Ia adalah resolver pronomina untuk dokumen.
Itu memberi bentuk pada chunker, dan satu aturan yang mudah keliru:
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;
}Dua teks, bukan satu. text adalah yang di-embed, header dan semuanya. content hanyalah kata-kata milik chunk ini sendiri, dan itulah yang dikutip kembali kepada pengguna. Kutip text dan sitasi menampilkan header yang tidak ada di dokumen pada titik itu — dan, dengan overlap, ekor berulang milik fragmen sebelumnya. Ia lalu menampilkan teks yang tidak berada di tempat yang dikatakannya, yang lebih buruk daripada tidak menampilkan apa pun.
Header tidak gratis. Di 940 chunk, ia memakan 24.213 dari 114.275 token embedding index: 21,2 % dari biaya embedding yang kamu bayar adalah header yang kamu tulis sendiri. Ia juga mendorong chunk ke batas window encoder. all-MiniLM-L6-v2 menerima 256 word-piece; strategi E memiliki 17 chunk di atas garis itu dan F punya 28, semuanya diam-diam terpotong tanpa peringatan dari apa pun. Ukuran chunk efektifmu bukan angka dalam config — melainkan yang lebih kecil antara angka itu dan window encoder-mu.
Dua puluh baris BM25, yang dilewati semua orang
Tautan ke bagian: Dua puluh baris BM25, yang dilewati semua orangDense retrieval punya satu kelemahan sistematis dan itu tidak halus: ia mencocokkan makna, jadi ia acuh terhadap tepat string mana yang kamu ketik. Nomor komponen, kode error, akronim, nama belakang — tidak ada yang punya makna berguna untuk di-embed, dan nearest neighbour dari sebuah kode error adalah setiap kode error lain dalam corpus-mu.
Jawaban klasiknya lebih tua dari semua ini dan butuh dua puluh baris. BM25 memberi skor dokumen berdasarkan seberapa sering term query muncul di dalamnya, meredam setiap term saat frekuensinya naik dan menghukum dokumen panjang yang mengumpulkan match hanya karena panjangnya.1 Term berkontribusi
di mana adalah jumlah term dalam dokumen, panjangnya, panjang rata-rata, dan serta dua konstanta konvensional — mengatur seberapa cepat pengulangan berhenti membantu, seberapa keras panjang dihukum.
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;
});
}
}Di 940 chunk, itu memberi skor sebuah query dalam 1,14 ms tanpa index sama sekali selain dua hash map. Dan ini bukan benda museum:
| retriever | R@1 | R@4 | R@8 | MRR | cost per query |
|---|---|---|---|---|---|
| dense (cosine) | 0.094 | 0.422 | 0.578 | 0.280 | 13 ms untuk embed + 0,3 ms scan |
| lexical (BM25) | 0.219 | 0.375 | 0.469 | 0.313 | 1,14 ms |
| hybrid (RRF) | 0.203 | 0.484 | 0.609 | 0.346 | keduanya |
| hybrid + cross-encoder | 0.312 | 0.578 | 0.703 | 0.447 | + 569 ms |
BM25 lebih dari menggandakan akurasi top-1 dense retriever pada corpus ini, dan kalah telak darinya pada rank 8. Keduanya gagal pada query yang berbeda, dan itulah seluruh argumen untuk menjalankan keduanya.
Menggabungkannya adalah satu tempat pendekatan yang tampak jelas justru salah. Cosine distance dan skor BM25 tidak berada pada skala yang sama, tidak dibatasi dengan cara yang sama, dan menormalisasikannya per query membuat bobot bergantung pada seberapa bagus hit terbaik kebetulan. Reciprocal rank fusion membuang skor dan hanya mempertahankan rank: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);
}Dan di sini pembacaan tabel yang jujur lebih penting daripada tabelnya. Hybrid mengalahkan BM25 pada R@4 dengan +10/−3, p = 0.09. Ia mengalahkan dense dengan +10/−6, p = 0.45. Pada corpus ini, dengan 64 query, hybrid retrieval tidak dapat dibedakan dari dense retrieval. Ia lebih baik pada point estimate dan setiap kolom recall, dan buktinya belum mencapai signifikansi. Hampir setiap blog post hybrid-search di internet melaporkan tabel seperti di atas tanpa interval; inilah yang dikatakan intervalnya.
Bi-encoder, cross-encoder, dan di mana lift sebenarnya berada
Tautan ke bagian: Bi-encoder, cross-encoder, dan di mana lift sebenarnya beradaSemua sejauh ini adalah bi-encoder: query masuk ke model sendirian, setiap chunk masuk ke model sendirian berbulan-bulan lalu, dan keduanya tidak pernah bertemu kecuali sebagai dot product. Itulah yang membuat index mungkin — embed sekali, pakai ulang selamanya — dan itu juga plafonnya. Model tidak pernah melihat query dan chunk bersama-sama.
Sebuah cross-encoder melakukan persis itu: ia menerima pasangan sebagai satu input dan mengembalikan skor relevansi. Tidak ada yang bisa diprecompute, jadi ia tidak bisa memberi rank pada index — tetapi ia bisa reranking shortlist. Reranking hybrid top 25 dengan ms-marco-MiniLM-L-6-v2 memindahkan R@1 dari 0.094 (dense) ke 0.312 dan MRR dari 0.280 ke 0.447: peningkatan tunggal terbesar di bab ini, dan satu-satunya yang menyentuh puncak daftar, bukan ekornya.
Biayanya 569 ms per query di CPU, dibandingkan 1,14 ms untuk BM25 dan 0,3 ms untuk vector scan. Kira-kira dua ribu kali biaya retrieval, untuk dua puluh lima dokumen. Itulah seluruh trade-off bi-encoder/cross-encoder dalam satu angka, dan itulah sebabnya arsitekturnya selalu berbentuk sama: retriever murah dengan recall lebar, lalu scorer mahal pada shortlist yang mampu kamu bayar. ColBERT berada di antara keduanya, memprecompute vektor per-token dan melakukan late interaction yang lebih murah daripada cross-encoder dan lebih tajam daripada dot product.3
L2, cosine, dan threshold yang belum kamu pantas dapatkan
Tautan ke bagian: L2, cosine, dan threshold yang belum kamu pantas dapatkanVector database melaporkan distance, dan distance mana yang dipakai adalah opsi konfigurasi. Pada vektor yang dinormalisasi, pilihannya kosmetik, dan identitas ini layak dilakukan sekali karena semua setelahnya bergantung pada vektor yang benar-benar unit. Untuk :
jadi cosine distance tepat sama dengan . Itu dot product dan norm dari Bab 1, diuangkan. Dicek pada dua vektor chunk nyata dari index di atas, lalu pada 40.000 pasangan:
||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-07Tepat sampai noise floating-point — dan hanya karena vektornya dinormalisasi. Lewati normalisasi dan identitasnya salah, threshold-mu tidak berarti apa-apa, dan distance yang dilaporkan sebuah dokumen bergantung pada seberapa panjang teksnya.
Sekarang angka yang tidak diturunkan siapa pun. Retriever selalu mengembalikan sesuatu: ia mengurutkan seluruh index dan menyerahkan bagian atas daftar, entah jawabannya ada di corpus atau tidak. Threshold adalah satu-satunya bagian sistem yang bisa berkata tidak — dan untuk menetapkannya kamu membutuhkan query yang seharusnya tidak mendapat apa pun kembali. Berikut tiga puluh: dua puluh satu tentang hal-hal yang memang tidak dicakup corpus ini — streaming, rate limit, prompt caching, JSON schema, agent loop, vector database, prompt injection, image generation — dan sembilan tentang paella, paspor, dan kebijakan refund. Terhadap index yang sama:
| top-1 cosine distance | |
|---|---|
| query in-domain, semua 64 | mean 0.445, range 0.270 – 0.721 |
| in-domain, top-1 benar-benar benar | mean 0.370 |
| in-domain, top-1 salah | mean 0.452 |
| out-of-domain, semua 30 | mean 0.699, range 0.497 – 0.867 |
Distribusinya terpisah, dan overlap. Query in-domain terburuk lebih jauh dari jawabannya (0.721) daripada query out-of-domain terbaik dari paragraf yang tidak relevan (0.497), jadi tidak ada threshold yang membuat keduanya benar. Menyapunya di gate nyata — simpan paling banyak empat chunk, dan hanya yang berada di bawah cut:
| 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 |
Baca kolom terakhir sebagai gertakan. Tanpa threshold, asisten menghasilkan jawaban percaya diri dengan sitasi rapi untuk "bagaimana cara memperbarui paspor Spanyol saya" dari corpus tentang backpropagation, tiga puluh kali dari tiga puluh. Pada 0.675 ia melakukannya sepuluh kali dari tiga puluh. Pada 0.525 ia melakukannya dua kali, dan menyerah pada dua belas pertanyaan yang bisa ia jawab.
Trade-off itu adalah keputusan produk, dan ujung yang benar bergantung pada berapa biaya jawaban salah bagimu. Yang tidak bisa dinegosiasikan adalah keberadaan kolom terakhir sama sekali. Jika kamu belum pernah mengukur retriever-mu terhadap pertanyaan yang seharusnya ia tolak, kamu tidak punya threshold — kamu punya angka.
Dua dari sepuluh gertakan pada 0.675 menunjukkan dua cara kegagalan ini terjadi.
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."Yang pertama adalah near miss: corpus menjelaskan KV cache secara detail, query membahas prompt cache, kata-katanya sama, dan 0.497 lebih dekat daripada kebanyakan retrieval in-domain yang benar di seluruh eksperimen. Embedding tidak tahu bahwa dua cache dengan nama sama adalah mesin berbeda. Yang kedua adalah literal match tanpa jawaban: corpus berisi frasa persis "what is the capital of France", digunakan sebagai contoh pertanyaan yang tidak membutuhkan penalaran. Retriever benar; jawabannya tidak ada. Sistem apa pun yang membaca "Saya menemukan sesuatu yang mirip" sebagai "Saya menemukan jawabannya" akan menyatakan Paris berdasarkan bukti itu — atau, lebih buruk, tidak akan.
Mengapa sitasi tidak ditulis oleh model
Tautan ke bagian: Mengapa sitasi tidak ditulis oleh modelModel tidak punya fakultas terpisah untuk fakta. Menghasilkan kalimat benar dan menghasilkan kalimat masuk akal adalah operasi yang sama — next-token prediction dari Bab 8 — dan tidak ada dalam operasi itu yang menandai mana yang mana. Analisis 2025 yang membingkai ulang hal ini berargumen bahwa pipeline training dan evaluasi secara aktif memberi imbalan pada tebakan: benchmark menilai dengan akurasi biner dan tidak memberi kredit untuk abstention, jadi model yang selalu menjawab mengungguli model identik yang berkata "Saya tidak tahu" ketika ia tidak tahu, dan post-training mengoptimalkannya sesuai itu.6 Hallucination dalam pembacaan ini bukan cacat misterius. Itu yang kamu dapat ketika menilai ujian pilihan ganda tanpa penalti untuk jawaban salah.
Perhatikan bentuknya. Diminta delapan paper tentang contrastive sentence embeddings, dengan identifier, Qwen2.5-0.5B-Instruct menghasilkan delapan baris dalam format sempurna. Semua delapan identifier well-formed. Semua delapan mengarah ke paper nyata di arXiv. Nol dari delapan adalah paper yang diklaim.
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 SomaliIni model kecil dan rate-nya miliknya sendiri; frontier model menciptakan jauh lebih sedikit. Mekanismenya berlaku umum, dan itulah alasan aturan berikut. Validator yang memeriksa "apakah identifier ini ada" meloloskan semua delapan, dan pengguna yang mengklik salah satunya mendarat di halaman nyata dari arsip nyata tanpa cara mengetahui bahwa mapping-nya diciptakan. Kegagalannya bukan pada identifier atau format. Kegagalannya ada pada asosiasi — persis hal yang diproduksi language model melalui plausibility.
Jadi: model menulis [1] dan [2], dan tidak pernah menulis link. Angka-angka merujuk pada fragmen yang diambil server, dan server — yang tahu persis dokumen mana dan offset mana asal setiap angka — menempelkan dokumen, label, dan URL setelahnya. Tidak ada yang bisa diciptakan model karena ia tidak pernah diminta untuk satu hal yang akan ia ciptakan.
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 };
}Jalankan pada pertanyaan pembuka dan empat chunk menjadi prompt 591-token serta tabel yang tidak pernah dilihat model:
[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.605Locator adalah bagian yang dilewati orang lalu tidak bisa ditambahkan nanti. #char=25873,26272 adalah range dalam teks canonical dokumen; untuk PDF padanannya adalah #page=12, untuk audio atau video #t=132.4,158.9, untuk spreadsheet sebuah sheet dan range A1. Dua itu bukan ciptaan — #page= adalah PDF Open Parameters dan #t= adalah W3C Media Fragments, dihormati secara native oleh browser pada elemen video dan audio. Sitasi tanpa locator adalah nama dokumen, dan nama dokumen bukan sitasi; itu saran agar pengguna pergi mencari.
Dan ketika tidak ada yang melewati threshold, pipeline tidak pernah mencapai model sama sekali:
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"Itu penolakan yang lebih murah dan lebih andal daripada instruksi apa pun dalam system prompt, karena ia adalah perbandingan antara dua angka, bukan permintaan kepada sistem probabilistik.
Evaluasi retriever terpisah dari generator
Tautan ke bagian: Evaluasi retriever terpisah dari generatorSetiap pengukuran di bab ini menilai retriever dan tidak sekali pun meminta model menulis jawaban. Itu disengaja, dan itulah bagian yang dilewati sebagian besar tim.
Sistem RAG memiliki dua mode kegagalan yang terlihat identik dari luar. Retriever tidak menemukan passage; atau ia menemukannya dan generator mengabaikannya, membantahnya, atau mencampurnya dengan sesuatu yang sudah ia percayai. Nilai hanya jawaban akhir dan keduanya tidak bisa dibedakan, jadi kamu men-tune prompt terhadap masalah yang hidup di chunker-mu. Recall@k, MRR, dan hitungan jawaban-yang-dihancurkan tidak membutuhkan generation call sama sekali, cukup murah untuk dijalankan pada setiap deploy, dan merupakan harness dari Bab 15 dengan fungsi scoring berbeda — request, deadline, concurrency, dan tally yang sama, di atas question set tetap alih-alih percakapan live.
Laporkan dengan interval. Aritmetika Bab 4 berlaku tanpa perubahan: pada 64 query, recall 0.5 membawa interval Wilson 95 % sekitar ±0.12, jadi strategi yang unggul empat poin atas strategi lain belum memberimu apa pun. Gunakan paired test ketika kedua strategi menjawab pertanyaan yang sama, yang selalu terjadi di sini — itulah yang mengubah "E terlihat lebih baik daripada A" menjadi p = 0.0015.
Dan kejujuran terakhir: RAG mengurangi hallucination dan tidak menghilangkannya. Menaruh passage yang benar dalam prompt tidak mewajibkan model menggunakannya, dan literatur telah mengatakan begitu sejak paper asli.7 Dua hal membuatnya lebih buruk di production. Context panjang menurun — model menemukan informasi di awal dan akhir prompt panjang lebih andal daripada di tengah, jadi dua puluh chunk alih-alih empat bisa menurunkan akurasi sambil menaikkan tagihan, efek yang diukur di Bab 24. Dan retrieval bisa benar tetapi tetap tidak cukup, seperti ditunjukkan dua cache di atas. SelfCheckGPT menandai klaim yang tidak bertahan dari resampling;8 Self-RAG melatih model untuk mengeluarkan token retrieve-and-critique-nya sendiri;9 TruthfulQA membuat mode kegagalan itu terbaca sejak awal.10 Tidak ada yang menutup celah, dan sistem yang menyajikan retrieved text sebagai bukti telah mencampuradukkan bersumber dengan benar.
Separuh sistem yang berjalan sebelum query apa pun
Tautan ke bagian: Separuh sistem yang berjalan sebelum query apa punRetriever adalah bagian terlihat dari pipeline yang kegagalannya semua terjadi lebih awal, dalam gelap. Tiga di antaranya berulang.
Extraction adalah tempat konten mati. PDF bukan teks; ia adalah instruksi menggambar. Layout dua kolom saling menyisip, tabel menjadi sup kata, header halaman berulang ke setiap chunk, dan halaman hasil scan tidak punya teks sama sekali sampai OCR memberinya teks, dengan confidence. Semua yang diukur di atas mengasumsikan extractor melakukan pekerjaannya; di production sering kali tidak, dan gejalanya muncul sebagai retrieval buruk tiga lapis jauhnya.
Index dicap dengan model yang membangunnya. Embeddings dari dua model tidak bisa dibandingkan — bukan "kurang akurat", melainkan tidak comparable, karena mereka adalah titik di ruang yang berbeda. Ubah embedding model dan setiap vektor di store menjadi sampah sampai dibangun ulang. Jadi nama model, jumlah dimensi, versi pipeline, dan versi extractor ditulis di samping setiap dokumen pada waktu index. Tanpa itu, pada hari upgrade, kamu tidak bisa tahu dokumen mana yang stale dan mana yang current, dan index setengah migrasi mengembalikan omong kosong percaya diri tanpa error di mana pun.
Satu dokumen rusak tidak boleh merusak folder, dan counter harus menghitung apa yang terjadi. Dokumen yang gagal extraction berakhir dalam state failed dengan alasannya, terlihat dan bisa di-retry, sementara sembilan puluh sembilan lainnya tetap searchable; dan jumlah chunk yang di-index ditulis oleh server saat selesai, bukan dideklarasikan oleh client saat upload. Folder yang melaporkan 400 fragmen dan berisi 40 adalah kebohongan yang baru muncul sebagai pertanyaan yang tak terjawab.
Ke mana ini berlanjut
Tautan ke bagian: Ke mana ini berlanjutSistem dalam bab ini menjawab pertanyaan yang jawabannya tertulis. Ia mengambilnya, memberi rank, menolak ketika tidak bisa, dan menyitir tempat ia mencari. Itulah sebagian besar yang orang inginkan dari asisten atas dokumen mereka sendiri, dan ia dibatasi dengan satu cara spesifik: retrieval hanya bisa mengembalikan apa yang pernah ditulis seseorang.
Yang tersisa adalah separuh lainnya. Sebagian dari yang kamu ingin model lakukan sama sekali bukan fakta dalam dokumen — format yang harus ia pegang, tone, taxonomy dengan empat ratus label, cara memutuskan yang hidup dalam sepuluh ribu contoh masa lalu dan tidak ada dalam paragraf mana pun. Retrieval tidak bisa mengantarkan itu, karena tidak ada yang bisa diambil; prompt yang lebih panjang hanya membayar tagihan Bab 16 untuk deskripsi sebuah skill, bukan skill-nya.
Bab 20 adalah keputusan itu — fine-tune, retrieve, atau prompt — dan temuannya adalah keputusan tersebut ekonomis sebelum teknis: ketiganya diberi harga end to end pada pertanyaan yang sama, dan crossover-nya adalah jumlah token. Pertanyaan yang membukanya adalah pertanyaan yang tidak bisa dijawab bab ini. Bukan di mana jawabannya tertulis, melainkan apa yang kamu lakukan ketika jawabannya tidak pernah tertulis.
Sources and method
Tautan ke bagian: Sources and methodSemua yang diukur di bab ini memakai satu corpus dan satu instrumen, dan keduanya reproducible. Corpus-nya adalah bab 1 sampai 13 kursus ini sebagaimana adanya pada 7 September 2026 — 13 dokumen, 359.067 karakter, 127 bagian, front matter dan bibliografi dihapus. Bab-bab itu terus diedit, jadi menerapkan aturan yang sama hari ini menghasilkan beberapa ribu karakter lebih banyak: jumlah bagian tidak berubah dan begitu pula setiap kesimpulan di bawah, tetapi total karakter adalah snapshot dan diberi label seperti itu. Ground truth-nya adalah 32 pertanyaan, masing-masing dipasangkan dengan kalimat verbatim yang muncul tepat sekali dalam corpus dan tidak pernah menjadi heading bagian, diajukan dalam dua phrasing untuk 64 query. Retrieval embeddings adalah sentence-transformers/all-MiniLM-L6-v2 (384 dimensi, mean-pooled, L2-normalised, window 256-token); reranking adalah cross-encoder/ms-marco-MiniLM-L-6-v2 atas top 25; contoh generation adalah Qwen/Qwen2.5-0.5B-Instruct dengan greedy decoding. Semua timing adalah CPU single-threaded. Tidak ada API berbayar yang dipanggil untuk memproduksi bab ini, yang juga menjadi alasan setiap latency di sini bersifat lokal dan dilabeli demikian.
Chunker yang ditampilkan dalam TypeScript adalah chunker yang diukur: instrumen Python yang mengimplementasikan aturan yang sama dan ts/chunk.ts dibandingkan chunk demi chunk di seluruh corpus dan sepakat pada semua 940 chunk, teks, dan offset. Interval adalah Wilson pada 95 %; perbandingan berpasangan adalah exact sign test dua sisi pada pasangan discordant.
Keempat belas identifier yang disitir di atas diselesaikan terhadap arXiv API dan dicek judul demi judul pada 7 September 2026 — yang, mengingat delapan yang tidak, terasa seperti hal paling minimal yang bisa dilakukan bab khusus ini.
Referensi
Tautan ke bagian: Referensi-
Robertson, S. dan Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval 3(4), hlm. 333–389 (2009). Sumber fungsi saturasi dan dua konstanta yang dipakai di atas, dan tempat membaca mengapa ada sama sekali. ↩
-
Cormack, G. V., Clarke, C. L. A. dan Büttcher, S. Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR 2009. adalah milik mereka, dan inti metodenya adalah tidak membutuhkan kalibrasi antara skala skor yang digabungkannya. ↩
-
Khattab, O. dan Zaharia, M. ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. arXiv:2004.12832 (2020). Jalan tengah antara dot product dan cross-encoder. Reimers, N. dan Gurevych, I., Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, arXiv:1908.10084 (2019), adalah bi-encoder yang menjadi dasar index bab ini dan diukur di Bab 8. ↩
-
Malkov, Yu. A. dan Yashunin, D. A. Efficient and Robust Approximate Nearest Neighbor Search using Hierarchical Navigable Small World Graphs. arXiv:1603.09320 (2016). Graph index di balik sebagian besar vector database yang dijual saat ini. ↩
-
Johnson, J., Douze, M. dan Jégou, H. Billion-scale Similarity Search with GPUs. arXiv:1702.08734 (2017). FAISS, dan implementasi referensi IVF yang diukur dalam kotak di atas. ↩
-
Kalai, A. T., Nachum, O., Vempala, S. S. dan Zhang, E. Why Language Models Hallucinate. arXiv:2509.04664 (2025). Argumen bahwa hallucination diproduksi oleh penilaian akurasi biner yang tidak pernah memberi imbalan untuk abstention, sehingga ia adalah masalah evaluasi sebelum menjadi masalah modeling. ↩
-
Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S. dan Kiela, D. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. arXiv:2005.11401 (2020). Paper yang menamai pola ini dan yang perlu dibaca untuk mengetahui apa yang ia perbaiki dan tidak. Guu et al., REALM: Retrieval-Augmented Language Model Pre-Training, arXiv:2002.08909 (2020), adalah karya sezaman yang melatih retriever bersama model, bukan menempelkannya kemudian; Karpukhin et al., Dense Passage Retrieval for Open-Domain Question Answering, arXiv:2004.04906 (2020), adalah asal two-encoder dense retriever yang dipakai sepanjang bab ini; dan Izacard dan Grave, Leveraging Passage Retrieval with Generative Models for Open Domain Question Answering, arXiv:2007.01282 (2020), adalah susunan fusion-in-decoder untuk memasukkan banyak passage ke satu generator. Gao et al., Retrieval-Augmented Generation for Large Language Models: A Survey, arXiv:2312.10997 (2023), adalah peta semua yang datang setelahnya, termasuk HyDE (Gao et al., Precise Zero-Shot Dense Retrieval without Relevance Labels, arXiv:2212.10496, 2022), yang meng-embed jawaban hipotetis alih-alih pertanyaan. ↩
-
Manakul, P., Liusie, A. dan Gales, M. J. F. SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models. arXiv:2303.08896 (2023). Deteksi dengan resampling, tanpa akses ke internal model dan tanpa external knowledge base. ↩
-
Asai, A., Wu, Z., Wang, Y., Sil, A. dan Hajishirzi, H. Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. arXiv:2310.11511 (2023). Melatih model untuk memutuskan kapan harus retrieve, alih-alih retrieve pada setiap turn. ↩
-
Lin, S., Hilton, J. dan Evans, O. TruthfulQA: Measuring How Models Mimic Human Falsehoods. arXiv:2109.07958 (2021). Benchmark yang dibangun dari pertanyaan ketika jawaban yang plausible dan jawaban yang benar berbeda, yang merangkum seluruh kesulitannya dalam satu kalimat. ↩