The mental model behind chunks, retrieval, and grounded answers
Ask a plain language model a question about documents it has never seen, and it will still answer. Confidently, and often wrong.
Retrieval-augmented generation is the fix: look the answer up before writing it.
A plain model has no way to check itself against real documents. It fills the gap with whatever is most probable, not what is true.
Same question, three different answers, and no way to tell which one is real.
Aurora Cloud is the fictional company behind every example in this deck: 48 documents across four topics.
Retrieval-augmented generation adds one step in front of writing: look before you answer.
The answer is no longer memory. It is a copy of what the corpus actually says.
Grounding is a contract, not a suggestion. A covered question gets a citation. An uncovered one gets a decline, never a guess.
A refusal is a correct answer too, when the corpus genuinely doesn’t cover it.
Cite-or-refuse, the contract behind every answer this deck builds toward: a claim traces to a retrieved chunk and names it with [n], or the system declines instead of guessing.
Retrieval turns a prompt into evidence; generation must cite it or refuse. Every concept from here on is one piece of making that contract real.
Building the index and answering a question are two different jobs, on two different clocks.
One runs once, ahead of time. The other runs every time someone types a question.
Two different jobs, two different clocks: the offline build (chunk, embed, index) happens once; the online loop (retrieve, augment, generate) reruns for every question.
The raw documents get chunked and turned into vectors a single time. After that, the index just sits there, ready to be searched.
A question comes in, gets turned into a search, and the results get folded into a prompt before the model ever runs.
Put both loops side by side and the contrast is the whole point: one column runs once, the other runs again for every question that comes in.
Everything from here on zooms into one box from this picture: how retrieval actually scores a match, how the prompt gets assembled, how the model is kept from making things up.
raw documents: HR, product, engineering, off-topic
chunk, embed, and fit the vectors into one structure
score every chunk against the query, keep the top-k
score just those candidates again, more carefully
cite the evidence, or refuse
A document mixes several separate facts into one block of text. A question only needs one of them back, not the whole page.
Chunk, the unit retrieval actually indexes and hands back: one self-contained slice of a document, small enough that a question only pulls the fact it needs.
A window of fixed size slides across the words. The simplest version just cuts, edge to edge, with nothing shared.
A fact that straddles a chunk boundary survives whole in neither chunk. Fixed-size, non-overlapping cuts guarantee this happens somewhere in a large corpus, overlap (the next slide) is the guard.
Shift the window by less than its full size, and consecutive chunks share a few words at the seam.
A bigger window swallows unrelated ideas into one chunk. A smaller window slices a single idea into fragments that lose their surrounding context.
Under the window this pipeline actually uses, nearly every one of the 48 documents collapses to a single chunk. Shrink the window, and one of them finally splits for real.
Chunk size is a tradeoff, not a default: too big dilutes a chunk across unrelated facts, too small shatters one fact into pieces with no context. Overlap fixes the second failure, not the first.
A chunk of text turns into a list of numbers. That list is a location - somewhere you could point to on a map.
Once meaning has a location, comparing meanings becomes measuring distance.
Embedding, the list of numbers a chunk (or a query) turns into: a location in a shared space where distance stands in for how related two pieces of text are.
The query gets vectorized exactly like every chunk, then dropped onto the same map. Whichever dot it lands nearest is the answer.
Not a mockup: the real 48-document Aurora Cloud corpus, chunked and turned into real TF-IDF vectors, projected to 2 axes. Each dot is a real chunk, colored by its document’s tag. The star is a real query, placed by the same vectorizer, at its real projected position.
Once meaning has a location, “search by meaning” has a precise mechanical definition: nearest-neighbor distance on that map. Retrieval is exactly this, run at real scale.
Every chunk now has a vector. Those vectors don’t sit around loose - they get packed together into one structure: the index. Building it happens once. Answering a question against it never does.
Vector index, every chunk’s embedding stored together in one fitted structure. Built once from the whole corpus; every later question is a read against it, never a rebuild.
48 separate vectors. One structure holds all of them.
The fit is a single event. Every search after it is free to repeat.
Three unrelated questions. One already-built index answers all of them.
The wrong mental model refits per question. The right one fits once.
Fitting the index is a one-time cost; search is what recurs, and search is cheap. Treating “build the index” as a per-question step is the mistake to rule out.
The index holds 48 points. Retrieval is the one operation that matters: drop the query onto the same map, and read off whichever points sit closest.
Nothing about retrieval is mysterious once the geometry is in place. It’s a distance calculation, run once per question.
Retrieval, scoring every chunk in the index against the query’s vector and keeping the top few. One distance calculation, run once per question, before generation ever sees the corpus.
Not a mockup: the real 48-document Aurora Cloud corpus, the real fitted index from the last concept, one real query run through .search(). The ranked list, the scores, and the map are all computed live.
Retrieval always returns its top-k, confidently, whether the best score is 0.48 or 0.02. Deciding what a weak score means is generation’s job, not retrieval’s.
Dense search ranks by meaning. Sparse search ranks by exact words. Run the same query through both and they don’t agree - fusing the two beats trusting either alone.
BM25 / TF-IDF, rewards exact term overlap, blind to paraphrase
dense cosine similarity, rewards topical closeness, blind to rare exact terms
reciprocal rank fusion, combines both rankings by position, not raw score
Dense and sparse have different blind spots, paraphrase versus rare exact terms. Fusing them means the system never has to bet on which failure mode a given query will hit.
Retrieval scores every chunk against the query with one cheap, global signal, across the whole corpus at once. That is what makes it fast, and it is also why the order it returns is only approximately right.
Reranking takes just the candidates retrieval already found and looks at each one again, one at a time, with a narrower, more literal question: how much of the query’s actual wording is in this chunk.
Reranking, a second, narrower pass over just the candidates retrieval already found, scoring each one on its own instead of against the whole corpus at once. It reorders; it never fetches a new document.
A single global signal, spread across every word in the query, does not always put the right document on top.
A single global signal, scored across the whole corpus at once, is what makes retrieval fast, and exactly why its order is only approximately right. Reranking trades that speed for a second, narrower look.
Tried the near-duplicate pairs first (PTO days, expense threshold, outage lag) - none of them swap, because the one word that tells those two docs apart never appears in the query. This query, tried next, produces a real swap at #1.
The corpus has three near-duplicate pairs, same wording, one changed number. None of them swap under this reranker.
That is an honest limit, not a bug: a reranker that only counts word overlap can’t tell two documents apart on the one word neither of them is being asked about.
A reranker that only counts word overlap can’t break a tie on the one word neither candidate mentions. Aurora Cloud’s near-duplicate PTO pair (15 days vs. 20) never swaps under this reranker.
Retrieval hands back three objects. The model reads one string. Something has to build that string.
A chat model takes plain text in. Three Chunk objects are not plain text yet.
The retrieved chunks flow into a single card, and each one gains a number as it lands.
A chunk’s doc title never reaches the model directly. Its [n] does.
The model never sees the corpus. It never sees the index. It sees one string, built once, per question.
The model’s entire universe for an answer is the literal prompt string assemble_prompt() built. Corpus, index, and every other chunk never cross that boundary, which is exactly what makes grounding checkable.
Retrieval and augmentation hand the model a stack of excerpts. Generation is the last step: turn that stack into a sentence a human can trust, or say, honestly, that the corpus doesn’t have the answer.
Cite or refuse is the discipline that makes a RAG system trustworthy.
No relevant chunk retrieved, without a refusal gate, the model falls back on parametric memory and answers fluently anyway, with no warning it is guessing. The similarity threshold is the guard that turns “no evidence” into an honest decline instead of a confident hallucination.
A plain model always produces text. Cite-or-refuse gives the system a branch a plain model doesn’t have: the ability to say “I don’t know” instead of a fluent guess.
Every concept so far runs retrieval exactly once per question. An agent gets to check its own result first, and search again if it wasn’t good enough.
The retrieval loop so far always asks once. An agent gets to check its own work first.
One tool call, one confidence check, and a fork: answer now, or search again first.
The question: “what’s up with the database when it just kind of goes down.” Vague on purpose.
The reformulation is a word filter, not a rewrite from scratch: drop anything four letters or shorter.
The agent only pays for a second search when the first one earns it.
The only difference between one-shot and agentic retrieval is the ability to check a confidence score and try again. The retry is capped at one call, gated on a real threshold, never an unbounded search spiral.
A demo can look convincing and still be wrong. The only way to trust a RAG system is to run it against questions with known answers, and grade it honestly.
Every concept so far - chunking, retrieval, hybrid search, reranking, generation - is a design choice. Evaluation is how you find out whether those choices actually work.
Write down real questions a user would ask, then name the one document that actually answers each. Freeze the list. Score every future change against it, unchanged.
Recall@k asks whether the right document is anywhere in the top few results. Faithful asks a stricter question - is it the very top hit, with a real similarity score worth trusting.
Recall@k, whether the right document lands anywhere in the top k results. A looser bar than faithful, which asks whether it’s the #1 hit with a score worth trusting.
A plain language model would have answered the meaning-of-life question anyway, fluently and with no warning that it was guessing. This system checks itself against real evidence first. It only speaks when it has some.
Every design choice in this deck, chunk size, hybrid fusion, reranking, the refusal threshold, is just a hypothesis until it’s run against frozen questions with known answers. Evaluation is how you find out whether those choices actually work.
Everything so far ran a hand-built TF-IDF index. Swap in chromadb, a real vector store, and chonkie, a real chunker, and the retrieval story does not change, only the plumbing underneath it does.
Chroma, an embedded vector store, pip install chromadb, no server to run. A collection stores documents, embeds them, and answers .query() calls with the nearest matches, all inside the same Python process.
The hand-built TF-IDF index becomes one real library call, coll.query(). Same corpus, same query, a relevant document retrieved, now backed by a real vector store instead of a few hundred lines of hand-rolled code.
embedded and local, runs in-process, no server, the vector store this coda actually calls
managed vector stores built for scale, a server (or a cloud service) to run and operate
chonkie chunks text, used above for real, docling ingests PDFs and other documents into text, named here, not run
Guides Lewis et al., Retrieval-Augmented Generation (2020) · Robertson & Zaragoza, BM25 and Beyond (2009) · LangChain, retrieval concepts · Anthropic, Contextual Retrieval
Siblings How Git Works, Visually · How Inference & Serving Work, Visually · How Evals & Observability Work, Visually · How Agentic Harnesses Work, Visually · How LangChain & LangGraph Work, Visually
Every figure in this deck: a real TF-IDF pipeline over the 48-document Aurora Cloud corpus, regenerable with build/regen_all.py