RAG tuotannossa: chunking, haku ja rehelliset viittaukset
Sokea 512 merkin leikkaus tuhoaa 4/32 vastausta ennen retrieveriä. Pelkkä chunker-korjaus nostaa sijan 115 sijaan 3.
Tällä sivulla
Tässä on oikea kysymys oikealta käyttäjältä oikealle assistentille: eval-joukossani on 20 kohtaa, riittääkö se pisteytykseen luottamiseen. Korpus sisältää vastauksen — kokonaisen osion siitä. Tässä ovat neljä fragmenttia, jotka retriever oikeasti laittoi promptiin.
[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…Kolme neljästä alkaa keskeltä sanaa. Kaksi on eri luvusta eri aiheesta. Ja fragmentti, joka vastaa kysymykseen — se, jossa lukee Seitsemäntoista kahdestakymmenestä ei erota 85 % mallia 65 % mallista — palasi sijalla 115.
Nyt sama kysymys, sama embedding model, sama prompt template. Yksi asia muuttui: miten dokumentit leikattiin.
[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]Sijalta 115 sijalle 3. Kukaan ei koskenut malliin, promptiin, kynnykseen tai paikkojen määrään. Tämä luku käsittelee tuota kuilua ja neljää muuta kohtaa, joissa hakujärjestelmä valehtelee sinulle hiljaa.
Näytä lisätiedot
Mitä tämä luku tarvitsee aiemmista luvuista, ja yksi kohta, jossa kieli muuttuu.
- Luku 1 määritteli pistetulon ja L2-normin. Alla oleva kynnysosio on nuo kaksi, eikä mitään muuta.
- Luku 8 erotti kielimallin embedding-taulukon retrieval embedding modelista, joka on koulutettu kontrastiivisesti pareilla, mittasi kosinisamankaltaisuutta ja päättyi lupaukseen, että luku 19 päätyisi konkreettiseen katkaisurajaan. Se lupaus lunastetaan tässä. Mitään siitä ei toisteta.
- Luku 4 rakensi Wilsonin välin; luku 15 rakensi arviointi-harnessin. Jokainen alla oleva taulukko kantaa ensimmäisen ja tuotettiin toisella.
- Luku 16 hinnoitteli context windowin. Tämän luvun lopussa koottu prompt maksaa 591 tokenia, ja se on budjetti, josta fragmentit kilpailevat.
Kaikki tässä on TypeScriptiä, kuten luvusta 14 lähtien, ja tämä luku on se, jossa sääntö ansaitsee paikkansa: ingestointi on jonoja ja tallennusta, haku on verkkokutsu, ja promptin kokoaminen viitteineen on palvelimen työ. Mittaus on tarkoituksella samaa koodia tulostaulun ympäröimänä — retriever, jonka pisteyttää toinen toteutus, on luku ohjelmistosta, jota et julkaise, ja alla oleva kosinikynnys on uskottava vain siksi, että näet sen pyyhkäistävän sillä chunkerilla, joka ajetaan tuotannossa.
Korpus ja mikä lasketaan oikeaksi vastaukseksi
Linkki osioon: Korpus ja mikä lasketaan oikeaksi vastaukseksiKaikki alla mitataan yhtä korpusta vasten: tämän kurssin ensimmäiset kolmetoista lukua — 13 dokumenttia, 359 067 merkkiä, 127 osiota, front matter ja bibliografiat poistettuina. Se on oikea tekninen korpus, jossa on proosaa, taulukoita, kaavoja ja koodilohkoja, ja juuri sellainen asia, jonka ihmiset lataavat knowledge baseen ja josta he sitten valittavat.
Ground truth on 32 kysymystä, joista jokaiseen on yhdistetty needle: lyhyt sanasta sanaan korpuksesta otettu lause, joka vastaa siihen. Jokainen needle esiintyy 359 067 merkissä tasan kerran, eikä yksikään ole osion otsikko — tuo tarkistus on tärkeä, koska otsikot jokaiseen chunkiin kopioiva chunker pisteyttäisi muuten itseään. Jokainen kysymys kysytään kahdesti, kerran kurssin englannilla ja kerran niin kuin tukipyyntö sen muotoilisi: 64 kyselyä yli 32 ground truthin.
Haku on oikea, kun palautettu chunk sisältää needlen kokonaisena. Se on ainoa määritelmä, joka vastaa sitä, mitä generaattori tarvitsee: puolikas lause promptissa ei ole vastaus, vaan vaara.
Embedding model on all-MiniLM-L6-v2 — 384 ulottuvuutta, mean-pooled ja normalisoitu, kontrastiivisesti koulutettu malli, jonka luku 8 mittasi. Korpuksen indeksointi vie CPU:lla 20,8 sekuntia, 22 ms per chunk; yhden kyselyn embedding kestää 13 ms.
Chunking mitattuna kuudella tavalla
Linkki osioon: Chunking mitattuna kuudella tavallaKuusi strategiaa kolmesta riippumattomasta ainesosasta. Blind leikkaa 512 merkin välein katsomatta tekstiä. Boundaries ei koskaan leikkaa kappaleen sisältä, vaan palaa lauserajaan vain, kun yksi kappale ylittää budjetin. Header lisää jokaisen chunkin alkuun sen dokumentin otsikon ja osiopolun. Overlap kopioi edellisen chunkin viimeiset 64 merkkiä seuraavaan.
| strategia | chunkit | tuhotut vastaukset | 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 |
64 kyselyllä R@20:n 95 % Wilsonin väli on A:lle [0,471, 0,705] ja E:lle [0,718, 0,901] — ne eivät mene päällekkäin, mutta useimmat muut sarakkeet menevät, eikä parittamaton taulukko voi erottaa niitä. Jokainen strategia vastaa samoihin kyselyihin, joten rehellinen testi on paritettu: laske jokaisen strategian voitot ja tappiot toista vastaan ja aja merkkitesti erimielisille pareille. Kolme tulosta selviää siitä.
Blind chunking tuhoaa neljä kolmestakymmenestäkahdesta vastauksesta kokonaan. Ei sijoita niitä huonosti — tuhoaa ne. Needle osuu 512 merkin rajan yli, joten yksikään indeksin chunk ei sisällä sitä, ja recallin katto näille kyselyille on nolla. Mikään reranker ei pelasta niitä, mikään kynnys ei auta, mikään suurempi malli ei auta. Et voi hakea tekstiä, joka ei ole yhtenä kappaleena missään indeksissäsi. Tämä on RAGin yksittäinen aliraportoiduin vika, koska se näyttää täsmälleen huonolta retrieveriltä.
Overlap korjaa sen eikä mitään muuta. Jokainen overlapia käyttävä strategia menettää nolla vastausta, mikä on overlapin tarkoitus. Se ei paranna sijoitusta: B vastaan A R@8:ssa on +8/−6, p = 0,79; R@20:ssa +9/−7, p = 0,80. Pahempaa on, että overlapin lisääminen headerin päälle aktiivisesti haittaa — F vastaan E on +2/−6 R@20:ssa — ja syy on mekaaninen. Chunkin vektori on keskiarvo sen tokeneista, joten 64 merkkiä edellisestä chunkista vetää tuota keskiarvoa kohti naapurin aihetta. Overlap on vakuutus jakautunutta vastausta vastaan, ja se maksetaan precisionissa.
Kontekstuaalinen header ostaa haun. E vastaan A on +18/−3 R@20:ssa, p = 0,0015. Ja ablaatio sanoo, etteivät rajat tee sitä: E vastaan C — samat leikkaukset, header ainoana erona — on +12/−2, p = 0,0129. Lisäämällä kappaleen alkuun ”Classification, Cross-Entropy, and How Not to Fool Yourself > How many test examples do I need?” kerrot embedding modelille, mistä kappaleessa on kyse, mitä kappale itse ei usein sano. Se on dokumenttien pronominien ratkaisija.
Tästä chunker saa muotonsa, ja yhden säännön, jonka voi helposti ymmärtää väärin:
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;
}Kaksi tekstiä, ei yksi. text on se, joka embedataan, header kaikkineen. content on vain tämän chunkin omat sanat, ja se lainataan takaisin käyttäjälle. Lainaa text, ja viite näyttää headerin, jota ei ole dokumentissa siinä kohdassa — ja overlapin kanssa toistetun hännän, joka kuuluu edelliseen fragmenttiin. Se näyttää silloin tekstiä, joka ei ole siellä missä se väittää olevansa, mikä on pahempaa kuin olla näyttämättä mitään.
Header ei ole ilmainen. 940 chunkissa se maksaa 24 213 indeksin 114 275 embedatusta tokenista: 21,2 % siitä, mistä maksat embeddingissä, on itse kirjoittamasi header. Se myös työntää chunkeja kohti encoderin ikkunaa. all-MiniLM-L6-v2 hyväksyy 256 word-pieceä; strategialla E on 17 chunkia tuon rajan yli ja F:llä 28, jokainen hiljaisesti katkaistuna ilman varoitusta mistään. Todellinen chunk-kokosi ei ole konfiguraatiossasi oleva luku — se on sen ja encoderisi ikkunan pienempi arvo.
Kaksikymmentä riviä BM25:tä, jonka kaikki ohittavat
Linkki osioon: Kaksikymmentä riviä BM25:tä, jonka kaikki ohittavatDense retrievalillä on yksi järjestelmällinen heikkous, eikä se ole hienovarainen: se vastaa merkitystä, joten sitä ei kiinnosta täsmälleen mikä merkkijono kirjoitit. Osanumero, virhekoodi, akronyymi, sukunimi — millään niistä ei ole hyödyllistä merkitystä embedattavaksi, ja virhekoodin lähin naapuri on jokainen muu virhekoodi korpuksessasi.
Klassinen vastaus on tätä kaikkea vanhempi ja vie kaksikymmentä riviä. BM25 pisteyttää dokumentin sen mukaan, kuinka usein kyselyn termit esiintyvät siinä, vaimentaen jokaista termiä sen frekvenssin kasvaessa ja rankaisten pitkiä dokumentteja, jotka keräävät osumia pelkällä pituudellaan.1 Termi antaa kontribuution
missä on termin määrä dokumentissa, sen pituus, keskipituus ja sekä kaksi perinteistä vakiota — määrittää, kuinka nopeasti toisto lakkaa auttamasta, kuinka kovasti pituutta rangaistaan.
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;
});
}
}940 chunkin yli se pisteyttää kyselyn 1,14 ms:ssa ilman mitään indeksiä kahden hash mapin lisäksi. Eikä se ole museoesine:
| retriever | R@1 | R@4 | R@8 | MRR | kustannus per kysely |
|---|---|---|---|---|---|
| dense (kosini) | 0,094 | 0,422 | 0,578 | 0,280 | 13 ms embeddingiin + 0,3 ms skannaukseen |
| lexical (BM25) | 0,219 | 0,375 | 0,469 | 0,313 | 1,14 ms |
| hybrid (RRF) | 0,203 | 0,484 | 0,609 | 0,346 | molemmat |
| hybrid + cross-encoder | 0,312 | 0,578 | 0,703 | 0,447 | + 569 ms |
BM25 yli kaksinkertaistaa dense retrieverin top-1-tarkkuuden tässä korpuksessa ja häviää sille pahasti sijaan 8 mennessä. Ne epäonnistuvat eri kyselyissä, mikä on koko perustelu molempien ajamiselle.
Niiden yhdistäminen on se yksi paikka, jossa ilmeinen lähestymistapa on väärä. Kosinietäisyydet ja BM25-pisteet eivät ole samalla asteikolla, eivät ole samalla tavalla rajattuja, ja niiden normalisointi kyselykohtaisesti tekee painosta riippuvaisen siitä, kuinka hyvä paras osuma sattui olemaan. Reciprocal rank fusion heittää pisteet pois ja säilyttää vain sijat: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);
}Ja tässä taulukon rehellinen lukeminen on tärkeämpää kuin taulukko. Hybrid voittaa BM25:n R@4:ssä +10/−3, p = 0,09. Se voittaa densen +10/−6, p = 0,45. Tässä korpuksessa, 64 kyselyllä, hybrid retrieval ei ole erotettavissa dense retrievalistä. Se on parempi molemmissa piste-estimaateissa ja jokaisessa recall-sarakkeessa, mutta näyttö ei saavuta merkitsevyyttä. Lähes jokainen hybrid-search-blogipostaus internetissä raportoi yllä olevan kaltaisen taulukon ilman väliä; näin väli puhuu.
Bi-encoder, cross-encoder ja mistä nosto oikeasti tulee
Linkki osioon: Bi-encoder, cross-encoder ja mistä nosto oikeasti tuleeKaikki tähän asti on bi-encoderia: kysely kulkee mallin läpi yksin, jokainen chunk kulki sen läpi yksin kuukausia sitten, eivätkä nämä kaksi koskaan kohtaa muuten kuin pistetulona. Siksi indeksi on mahdollinen — embed kerran, käytä ikuisesti uudelleen — ja se on myös katto. Malli ei koskaan katso kyselyä ja chunkia yhdessä.
Cross-encoder tekee täsmälleen sen: se ottaa parin yhtenä syötteenä ja palauttaa relevanssipisteen. Mitään ei voi esilaskentaa, joten se ei voi järjestää indeksiä — mutta se voi rerankata shortlistin. Hybrid top 25:n reranking mallilla ms-marco-MiniLM-L-6-v2 siirtää R@1:n arvosta 0,094 (dense) arvoon 0,312 ja MRR:n arvosta 0,280 arvoon 0,447: tämän luvun suurin yksittäinen parannus ja ainoa, joka koskee listan kärkeä eikä häntää.
Se maksaa CPU:lla 569 ms per kysely, kun BM25 maksaa 1,14 ms ja vektoriskannaus 0,3 ms. Noin kaksituhatta kertaa retrieval-kustannus kahdellekymmenelleviidelle dokumentille. Se on koko bi-encoder/cross-encoder-vaihtokauppa yhtenä lukuna, ja siksi arkkitehtuuri on aina saman muotoinen: halpa retriever laajalla recallilla, sitten kallis pisteyttäjä shortlistille, johon sinulla on varaa. ColBERT istuu näiden välissä, esilaskien per-token-vektoreita ja tehden late interactionin, joka on halvempi kuin cross-encoder ja terävämpi kuin pistetulo.3
L2, kosini ja kynnys, jota et ole ansainnut
Linkki osioon: L2, kosini ja kynnys, jota et ole ansainnutVektoritietokannat raportoivat etäisyyksiä, ja etäisyyden tyyppi on konfiguraatiovalinta. Normalisoiduilla vektoreilla valinta on kosmeettinen, ja identiteetti kannattaa tehdä kerran, koska kaikki sen jälkeen riippuu siitä, että vektorit ovat oikeasti yksikkövektoreita. Kun :
joten kosinietäisyys on täsmälleen . Tämä on luvun 1 pistetulo ja normi lunastettuna. Tarkistettuna kahdella oikealla chunk-vektorilla yllä olevasta indeksistä ja sitten yli 40 000 parilla:
||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-07Täsmällinen liukulukuvirheeseen asti — ja vain siksi, että vektorit on normalisoitu. Ohita normalisointi, ja identiteetti on epätosi, kynnyksesi ei tarkoita mitään, ja dokumentin raportoima etäisyys riippuu siitä, kuinka pitkä sen teksti oli.
Nyt luku, jota kukaan ei johda. Retriever palauttaa aina jotain: se lajittelee koko indeksin ja antaa sinulle listan kärjen riippumatta siitä, onko vastausta missään korpuksessa. Kynnys on järjestelmän ainoa osa, joka voi sanoa ei — ja sen asettamiseen tarvitset kyselyitä, joiden pitäisi palauttaa ei mitään. Tässä on kolmekymmentä: kaksikymmentäyksi asioista, joita tämä korpus ei aidosti kata — streaming, nopeusrajat, prompt caching, JSON-schemat, agent-silmukat, vektoritietokannat, prompt injection, kuvagenerointi — ja yhdeksän paellasta, passeista ja palautuskäytännöistä. Samaa indeksiä vasten:
| top-1-kosinietäisyys | |
|---|---|
| in-domain-kyselyt, kaikki 64 | keskiarvo 0,445, vaihteluväli 0,270–0,721 |
| in-domain, top-1 oikeasti oikein | keskiarvo 0,370 |
| in-domain, top-1 väärin | keskiarvo 0,452 |
| out-of-domain, kaikki 30 | keskiarvo 0,699, vaihteluväli 0,497–0,867 |
Jakaumat erottuvat, ja ne menevät päällekkäin. Huonoin in-domain-kysely on kauempana vastauksestaan (0,721) kuin paras out-of-domain-kysely on epärelevantista kappaleesta (0,497), joten mikään kynnys ei saa molempia oikein. Pyyhkäistynä oikean portin yli — pidä enintään neljä chunkia, ja vain ne, jotka ovat rajan alla:
| kynnys | in-domain vastattu | joista vastaus oli mukana | out-of-domain vastattu |
|---|---|---|---|
| 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 |
| ei mitään | 64 / 64 | 27 | 30 / 30 |
Lue viimeinen sarake bluffeina. Ilman kynnystä assistentti tuottaa itsevarman, hyvin viitatun vastauksen kysymykseen ”miten uusisin Espanjan passini” korpuksesta, joka käsittelee backpropagationia, kolmekymmentä kertaa kolmestakymmenestä. Arvolla 0,675 se tekee sen kymmenen kertaa kolmestakymmenestä. Arvolla 0,525 se tekee sen kahdesti ja luovuttaa kahdessatoista kysymyksessä, joihin se olisi voinut vastata.
Tuo vaihtokauppa on tuotepäätös, ja sen oikea pää riippuu siitä, mitä väärä vastaus maksaa sinulle. Se, mistä ei neuvotella, on viimeisen sarakkeen olemassaolo. Jos et ole koskaan mitannut retrieveriäsi kysymyksiä vasten, joista sen pitäisi kieltäytyä, sinulla ei ole kynnystä — sinulla on numero.
Kaksi kymmenestä bluffista arvolla 0,675 näyttää kaksi tapaa, joilla tämä epäonnistuu.
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."Ensimmäinen on läheltä piti: korpus selittää KV cachea yksityiskohtaisesti, kysely koskee prompt cachea, sanat ovat samoja sanoja, ja 0,497 on lähempänä kuin useimmat koko kokeen oikeat in-domain-haut. Embedding ei tiedä, että kaksi samannimistä cachea ovat eri koneita. Toinen on kirjaimellinen osuma ilman vastausta: korpus sisältää täsmällisen fraasin ”mikä on Ranskan pääkaupunki”, käytettynä esimerkkinä kysymyksestä, joka ei vaadi päättelyä. Retriever on oikeassa; vastaus ei ole siellä. Mikä tahansa järjestelmä, joka lukee ”löysin jotain samanlaista” muotoon ”löysin vastauksen”, väittää tuon näytön perusteella Pariisin — tai pahempaa, ei väitä.
Miksi viitettä ei kirjoita malli
Linkki osioon: Miksi viitettä ei kirjoita malliMallilla ei ole erillistä faktakykyä. Toden lauseen tuottaminen ja uskottavan lauseen tuottaminen ovat sama operaatio — luvun 8 next-token prediction — eikä mikään siinä operaatiossa merkitse, kumpi on kumpi. Vuoden 2025 analyysi, joka kehysti tämän uudelleen, väittää, että koulutus- ja arviointiputki aktiivisesti palkitsee arvaamisesta: benchmarkit pisteyttävät binäärisellä tarkkuudella eivätkä anna pisteitä pidättäytymisestä, joten malli, joka vastaa aina, saa paremman tuloksen kuin identtinen malli, joka sanoo ”en tiedä” silloin kun ei tiedä, ja post-training optimoi sen mukaisesti.6 Hallusinaatio ei tällä luennalla ole mystinen vika. Se on se, mitä saat, kun arvostelet monivalintakokeen ilman rangaistusta väärästä vastauksesta.
Katso sen muotoa. Kun mallilta pyydettiin kahdeksaa artikkelia kontrastiivisista sentence embeddingeistä tunnisteineen, Qwen2.5-0.5B-Instruct tuotti kahdeksan riviä täydellisessä formaatissa. Kaikki kahdeksan tunnistetta ovat hyvin muodostettuja. Kaikki kahdeksan ratkeavat oikeiksi arXiv-artikkeleiksi. Nolla kahdeksasta on väitetty artikkeli.
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 SomaliTämä on pieni malli, ja osuus on sen oma; frontier-malli keksii paljon vähemmän. Mekanismi yleistyy, ja se on syy seuraavaan sääntöön. Validaattori, joka tarkistaa ”onko tämä tunniste olemassa”, päästää kaikki kahdeksan läpi, ja käyttäjä, joka klikkaa yhtä, päätyy oikealle sivulle oikeassa arkistossa ilman mitään keinoa huomata, että kytkentä keksittiin. Vika ei ole tunnisteessa tai formaatissa. Se on assosiaatiossa — juuri siinä asiassa, jonka kielimalli tuottaa uskottavuuden perusteella.
Siispä: malli kirjoittaa [1] ja [2], eikä koskaan kirjoita linkkiä. Numerot viittaavat palvelimen hakemiin fragmentteihin, ja palvelin — joka tietää täsmälleen mistä dokumentista ja mistä offseteista jokainen numero tuli — liittää dokumentin, labelin ja URL:n jälkeenpäin. Mallilla ei ole mitään keksittävää, koska siltä ei koskaan pyydetä sitä yhtä asiaa, jonka se keksisi.
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 };
}Aja se avauskysymyksellä, ja neljä chunkia muuttuvat 591 tokenin promptiksi ja taulukoksi, jota malli ei koskaan näe:
[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 on osa, jonka ihmiset ohittavat eivätkä sitten voi lisätä myöhemmin. #char=25873,26272 on alue dokumentin kanonisessa tekstissä; PDF:lle vastaava on #page=12, audiolle tai videolle #t=132.4,158.9, laskentataulukolle sheet ja A1-alue. Nämä kaksi eivät ole keksintöjä — #page= on PDF Open Parameters ja #t= on W3C Media Fragments, joita selaimet kunnioittavat natiivisti video- ja audioelementeissä. Viite ilman locatoria on dokumentin nimi, eikä dokumentin nimi ole viite; se on ehdotus, että käyttäjä menisi katsomaan.
Ja kun mikään ei läpäise kynnystä, putki ei koskaan saavuta mallia lainkaan:
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"Se on halvempi ja luotettavampi kieltäytyminen kuin mikään ohje system promptissa, koska se on kahden luvun vertailu eikä pyyntö probabilistiselle järjestelmälle.
Arvioi retriever erillään generaattorista
Linkki osioon: Arvioi retriever erillään generaattoristaJokainen tämän luvun mittaus pisteyttää retrieverin eikä koskaan pyydä mallia kirjoittamaan vastausta. Se on tarkoituksellista, ja juuri sen useimmat tiimit ohittavat.
RAG-järjestelmällä on kaksi vikatilaa, jotka näyttävät ulkopuolelta identtisiltä. Retriever ei löytänyt kohtaa; tai se löysi sen ja generaattori sivuutti sen, kiisti sen tai sekoitti sen johonkin, johon se jo uskoi. Pisteytä vain lopullinen vastaus, ja näitä kahta ei voi erottaa, joten virität prompteja ongelmaan, joka elää chunkerissasi. Recall@k, MRR ja tuhottujen vastausten määrä eivät tarvitse generointikutsua lainkaan, ne ovat tarpeeksi halpoja ajettavaksi jokaisessa deployssa, ja ne ovat luvun 15 harness eri pisteytysfunktiolla — sama request, deadline, concurrency ja laskuri, kiinteän kysymysjoukon yli live-keskustelun sijaan.
Raportoi ne väleillä. Luvun 4 aritmetiikka pätee muuttumatta: 64 kyselyllä recall 0,5 kantaa 95 % Wilsonin välinä noin ±0,12, joten strategia, joka on neljä pistettä toista edellä, ei ole kertonut sinulle mitään. Käytä paritettua testiä aina, kun molemmat strategiat vastaavat samoihin kysymyksiin, kuten ne täällä aina tekevät — juuri se muutti ”E näyttää paremmalta kuin A” muotoon p = 0,0015.
Ja viimeinen rehellisyys: RAG vähentää hallusinaatiota eikä poista sitä. Oikean kohdan laittaminen promptiin ei velvoita mallia käyttämään sitä, ja kirjallisuus on sanonut niin alkuperäisestä paperista lähtien.7 Kaksi asiaa pahentaa sitä tuotannossa. Pitkät kontekstit heikkenevät — malli löytää tiedon pitkän promptin alusta ja lopusta luotettavammin kuin keskeltä, joten kaksikymmentä chunkia neljän sijaan voi laskea tarkkuutta samalla kun lasku kasvaa, vaikutus joka mitataan luvussa 24. Ja haku voi olla oikea ja silti riittämätön, kuten kaksi cachea yllä näyttivät. SelfCheckGPT merkitsee väitteet, jotka eivät selviä uudelleennäytteistyksestä;8 Self-RAG kouluttaa mallin tuottamaan omat retrieve-and-critique-tokeninsa;9 TruthfulQA teki vikatilasta alun perin luettavan.10 Mikään ei sulje kuilua, ja järjestelmä, joka esittää haetun tekstin todisteena, on sekoittanut lähteistetyn ja toden.
Järjestelmän puolikas, joka ajetaan ennen yhtäkään kyselyä
Linkki osioon: Järjestelmän puolikas, joka ajetaan ennen yhtäkään kyselyäRetriever on näkyvä osa putkea, jonka viat tapahtuvat kaikki aiemmin, pimeässä. Kolme niistä toistuu.
Extraction on paikka, jossa sisältö kuolee. PDF ei ole tekstiä; se on piirto-ohjeita. Kaksipalstaiset asettelut lomittuvat, taulukot muuttuvat sanasopaksi, sivuotsikot toistuvat jokaiseen chunkiin, eikä skannatulla sivulla ole tekstiä lainkaan ennen kuin OCR antaa sille sellaista, confidence-arvon kanssa. Kaikki yllä mitattu oletti, että extractor teki työnsä; tuotannossa niin ei usein ole, ja oire näkyy huonona hakuna kolme kerrosta myöhemmin.
Indeksiin leimataan malli, joka rakensi sen. Kahden mallin embeddingit eivät ole vertailukelpoisia — eivät ”vähemmän tarkkoja”, vaan eivät vertailukelpoisia, koska ne ovat pisteitä eri avaruuksissa. Vaihda embedding model, ja jokainen storeen tallennettu vektori on roskaa, kunnes se rakennetaan uudelleen. Siksi mallin nimi, ulottuvuuksien määrä, putken versio ja extractorin versio kirjoitetaan jokaisen dokumentin viereen indeksointihetkellä. Ilman niitä et päivityspäivänä voi sanoa, mitkä dokumentit ovat vanhentuneita ja mitkä ajantasaisia, ja puoliksi migroitu indeksi palauttaa itsevarmaa hölynpölyä ilman virhettä missään.
Yksi rikkinäinen dokumentti ei saa rikkoa kansiota, ja laskureiden on laskettava mitä tapahtui. Dokumentti, jonka extraction epäonnistuu, päätyy failed-tilaan syineen, näkyvänä ja uudelleen yritettävänä, samalla kun muut yhdeksänkymmentäyhdeksän pysyvät haettavina; ja indeksoitujen chunkien määrän kirjoittaa palvelin, kun se valmistuu, ei asiakas silloin kun se lataa tiedoston. Kansio, joka raportoi 400 fragmenttia ja sisältää 40, on valhe, joka nousee pintaan vain vastaamattomana kysymyksenä.
Mihin tästä mennään seuraavaksi
Linkki osioon: Mihin tästä mennään seuraavaksiTämän luvun järjestelmä vastaa kysymyksiin, joiden vastaukset on kirjoitettu ylös. Se hakee ne, järjestää ne, kieltäytyy kun ei voi ja viittaa siihen, mistä katsoi. Se on suurin osa siitä, mitä ihmiset haluavat omien dokumenttiensa yli toimivalta assistentilta, ja sitä rajaa yksi tarkka asia: haku voi palauttaa vain sen, minkä joku kirjoitti.
Jäljelle jää toinen puolikas. Osa siitä, mitä haluat mallin tekevän, ei ole fakta dokumentissa lainkaan — formaatti, jota sen täytyy ylläpitää, sävy, taksonomia neljälläsadalla labelilla, päätöksentapa, joka elää kymmenessä tuhannessa aiemmassa esimerkissä eikä missään kappaleessa. Haku ei voi toimittaa niitä, koska mitään haettavaa ei ole; pidempi prompt maksaa vain luvun 16 laskun kuvauksesta skillistä skillin sijaan.
Luku 20 on tuo päätös — fine-tune, retrieve vai prompt — ja sen löydös on, että päätös on taloudellinen ennen kuin se on tekninen: kaikki kolme hinnoitellaan alusta loppuun samalla kysymyksellä, ja leikkauspiste on token-määrä. Sen avaava kysymys on se, johon tämä luku ei voi vastata. Ei missä vastaus on kirjoitettu, vaan mitä teet, kun sitä ei koskaan ollut kirjoitettu.
Lähteet ja menetelmä
Linkki osioon: Lähteet ja menetelmäKaikki tässä luvussa mitattu käytti yhtä korpusta ja yhtä instrumenttia, ja molemmat ovat toistettavissa. Korpus on tämän kurssin luvut 1–13 sellaisina kuin ne olivat 7. syyskuuta 2026 — 13 dokumenttia, 359 067 merkkiä, 127 osiota, front matter ja bibliografiat poistettuina. Noita lukuja muokataan edelleen, joten saman säännön soveltaminen tänään laskee muutaman tuhannen merkin enemmän: osiomäärä on muuttumaton ja samoin jokainen alla oleva johtopäätös, mutta merkkimäärä on snapshot ja sellaiseksi merkitty. Ground truth on 32 kysymystä, joista jokaiseen on yhdistetty sanasta sanaan lause, joka esiintyy korpuksessa täsmälleen kerran eikä ole koskaan osion otsikko, kysyttynä kahdella muotoilulla 64 kyselyksi. Retrieval embeddingit ovat sentence-transformers/all-MiniLM-L6-v2 (384 ulottuvuutta, mean-pooled, L2-normalisoitu, 256-tokenin ikkuna); reranking on cross-encoder/ms-marco-MiniLM-L-6-v2 top 25:n yli; generointiesimerkki on Qwen/Qwen2.5-0.5B-Instruct greedy decodingilla. Kaikki ajoitukset ovat yksisäikeisellä CPU:lla. Tämän luvun tuottamiseen ei kutsuttu maksullista API:a, minkä vuoksi jokainen latenssi tässä on paikallinen ja merkitty sellaiseksi.
TypeScriptissä näytetty chunker on chunker, joka mitattiin: samaa sääntöä ja ts/chunk.ts toteuttavaa Python-instrumenttia verrattiin chunk chunkilta koko korpuksen yli, ja ne ovat samaa mieltä kaikista 940 chunkista, teksteistä ja offseteista. Välit ovat Wilson 95 %:ssa; parivertailut ovat kaksisuuntaisia täsmällisiä merkkitestejä erimielisille pareille.
Kaikki yllä mainitut neljätoista tunnistetta ratkaistiin arXiv API:a vasten ja tarkistettiin otsikko otsikolta 7. syyskuuta 2026 — mikä, ne kahdeksan huomioon ottaen jotka eivät olleet oikein, tuntui vähimmältä mitä tämä nimenomainen luku saattoi tehdä.
Viitteet
Linkki osioon: Viitteet-
Robertson, S. ja Zaragoza, H. The Probabilistic Relevance Framework: BM25 and Beyond. Foundations and Trends in Information Retrieval 3(4), s. 333–389 (2009). Saturaation funktion ja yllä käytettyjen kahden vakion lähde sekä paikka lukea, miksi ylipäätään on olemassa. ↩
-
Cormack, G. V., Clarke, C. L. A. ja Büttcher, S. Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR 2009. on heidän, ja menetelmän pointti on, ettei se tarvitse kalibrointia yhdistämiensä pisteasteikkojen välillä. ↩
-
Khattab, O. ja Zaharia, M. ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT. arXiv:2004.12832 (2020). Keskialue pistetulon ja cross-encoderin välissä. Reimers, N. ja Gurevych, I., Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, arXiv:1908.10084 (2019), on bi-encoder, jonka varaan tämän luvun indeksi on rakennettu ja joka mitattiin luvussa 8. ↩
-
Malkov, Yu. A. ja Yashunin, D. A. Efficient and Robust Approximate Nearest Neighbor Search using Hierarchical Navigable Small World Graphs. arXiv:1603.09320 (2016). Graafi-indeksi useimpien tällä hetkellä myytävien vektoritietokantojen takana. ↩
-
Johnson, J., Douze, M. ja Jégou, H. Billion-scale Similarity Search with GPUs. arXiv:1702.08734 (2017). FAISS ja yllä olevassa laatikossa mitatun IVF:n viitetoteutus. ↩
-
Kalai, A. T., Nachum, O., Vempala, S. S. ja Zhang, E. Why Language Models Hallucinate. arXiv:2509.04664 (2025). Argumentti siitä, että hallusinaatio syntyy binäärisen tarkkuuden arvioinnista, joka ei koskaan palkitse pidättäytymisestä, ja on siksi arviointiongelma ennen kuin se on mallinnusongelma. ↩
-
Lewis, P., Perez, E., Piktus, A., Petroni, F., Karpukhin, V., Goyal, N., Küttler, H., Lewis, M., Yih, W., Rocktäschel, T., Riedel, S. ja Kiela, D. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. arXiv:2005.11401 (2020). Paperi, joka nimesi mallin, ja se, joka kannattaa lukea siitä, mitä se korjaa ja mitä ei. Guu et al., REALM: Retrieval-Augmented Language Model Pre-Training, arXiv:2002.08909 (2020), on samanaikainen työ, joka kouluttaa retrieverin yhdessä mallin kanssa sen sijaan, että se pultattaisiin päälle; Karpukhin et al., Dense Passage Retrieval for Open-Domain Question Answering, arXiv:2004.04906 (2020), on paikka, josta koko tässä luvussa käytetty kahden encoderin dense retriever tulee; ja Izacard ja Grave, Leveraging Passage Retrieval with Generative Models for Open Domain Question Answering, arXiv:2007.01282 (2020), on fusion-in-decoder-järjestely monien passagejen syöttämiseksi yhdelle generaattorille. Gao et al., Retrieval-Augmented Generation for Large Language Models: A Survey, arXiv:2312.10997 (2023), on kartta kaikesta sen jälkeen tulleesta, mukaan lukien HyDE (Gao et al., Precise Zero-Shot Dense Retrieval without Relevance Labels, arXiv:2212.10496, 2022), joka embedaa hypoteettisen vastauksen kysymyksen sijaan. ↩
-
Manakul, P., Liusie, A. ja Gales, M. J. F. SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative Large Language Models. arXiv:2303.08896 (2023). Havaitseminen uudelleennäytteistyksellä ilman pääsyä mallin sisuksiin ja ilman ulkoista knowledge basea. ↩
-
Asai, A., Wu, Z., Wang, Y., Sil, A. ja Hajishirzi, H. Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection. arXiv:2310.11511 (2023). Mallin kouluttaminen päättämään, milloin hakea, sen sijaan että haetaan jokaisella vuorolla. ↩
-
Lin, S., Hilton, J. ja Evans, O. TruthfulQA: Measuring How Models Mimic Human Falsehoods. arXiv:2109.07958 (2021). Benchmark, joka on rakennettu kysymyksistä, joissa uskottava vastaus ja tosi vastaus eroavat — koko vaikeus yhdessä lauseessa. ↩