RAG Mental Model
Retrieval-augmented generation is search, then generation, with a budget. The model only sees what you stuffed into the prompt.
- rag
- retrieval
The model did not “know” your handbook. You searched, you pasted, it summarized. Miss the search and the answer is fan fiction with a confident tone.
Retrieval augmented generation is that loop with a name: find text, pack it into the prompt, ask the model to answer from that, preferably with citations. The weights stay frozen. The model’s working memory is whatever survived your budget.
Key takeaways
- RAG is search, then pack, then generate. The model only sees the packed prompt.
- A vector database is one retriever. BM25, SQL, and grep can play the same role.
- Failures live in stages: bad index, bad retrieve, bad pack, bad generate, fake cite.
- Empty retrieval should refuse. Stuffing noise is how you get fluent wrong answers.
- Cite only ids the model named that you actually packed. Intersect, then drop the rest.
Retrieval augmented generation is a pipeline
Five stages. Each one can lie independently.
- Index. Split documents into chunks, attach metadata (url, heading path, tenant, date), store text plus whatever you search with (embeddings, keywords, both).
- Retrieve. Turn the user question into a query. Return top-k chunks.
- Pack. Fit a subset into the context window with instructions: answer from sources, or say you cannot.
- Generate. Sample an answer. Same request, different Tuesday.
- Cite. Attach chunk ids the UI can open. Verify the ids exist in this packed set before you render them as links.
If you only remember one sentence: the generator is not looking at your corpus. It is looking at a string you built.
Where it breaks (by stage)
| Stage | Typical failure | What you see |
|---|---|---|
| Index | Split a table in half, skip a PDF column, stale doc | Confident half-truths |
| Retrieve | Wrong query, no keywords, no tenant filter | Neighboring-but-wrong chunks |
| Pack | Truncate the useful paragraph, dump 40 near-duplicates | Lost-in-the-middle, wasted tokens |
| Generate | Sampler invents a number the chunks never stated | Fluent lie |
| Cite | Model names chunk-3 that was not in the prompt |
Clickable 404 |
Do not recap chunking or similarity search here. This page is the wiring diagram. Those pages are the knobs.
RAG is not a vector database
People say “we added RAG” and mean “we stood up Pinecone.” That is like saying “we added search” and meaning “we created an Elasticsearch cluster.”
You can retrieve with:
- Keyword ranking (BM25) for SKUs, error codes, rare names
- Vector nearest-neighbor for paraphrase
- Structured lookup (SQL, CMS API) when the fact is a row, not a paragraph
- A mix, then a rerank
The generation step does not care which retriever you used. It cares whether the packed text contains the answer, and whether you told it to stay inside that text.
When the right move is a tool (get_order(id)), not an index, skip RAG. When RAG is the wrong tool is the sibling argument.
Pack, then cite only what was packed
Retrieved chunks share the context window with the system prompt, history, and reserved output tokens. Count that budget in tokens (tokens and context windows); pack() below uses string.length so the control flow is visible.
type Chunk = {
id: string;
text: string;
sourceUrl: string;
headingPath: string[];
score: number;
};
type Completion = {
text: string;
// Model-emitted ids. Not trusted until they intersect `packed`.
citedIds: string[];
};
function pack(chunks: Chunk[], maxChars: number): Chunk[] {
const chosen: Chunk[] = [];
let used = 0;
for (const c of chunks) {
if (used + c.text.length > maxChars) break;
chosen.push(c);
used += c.text.length;
}
return chosen;
}
function formatSources(chunks: Chunk[]): string {
return chunks
.map(
(c) =>
`<source id="${c.id}" url="${c.sourceUrl}">\n${c.text}\n</source>`,
)
.join('\n\n');
}
function citationsUsed(citedIds: string[], packed: Chunk[]): string[] {
const allowed = new Set(packed.map((c) => c.id));
const seen = new Set<string>();
const out: string[] = [];
for (const id of citedIds) {
if (!allowed.has(id) || seen.has(id)) continue; // drop fake or duplicate ids
seen.add(id);
out.push(id);
}
return out;
}
async function answerFromRag(
query: string,
retrieve: (q: string) => Promise<Chunk[]>,
generate: (input: { messages: unknown[] }) => Promise<Completion>,
) {
const hits = await retrieve(query);
const packed = pack(hits, 12_000);
if (packed.length === 0) {
return { text: 'I do not have sources for that.', citations: [] as string[] };
}
const completion = await generate({
messages: [
{
role: 'system',
content:
'Answer only from <source> blocks. If they do not contain the answer, say so. Return citedIds for sources you actually used, never ids that were not provided.',
},
{
role: 'user',
content: `${formatSources(packed)}\n\nQuestion: ${query}`,
},
],
});
return {
text: completion.text,
citations: citationsUsed(completion.citedIds, packed),
};
}
Failure mode in one comment: if retrieve returns chunks about a different product with a similar name, generate will summarize that product. The model did its job. Your search did not.
A second failure, the one the table calls a clickable 404: attaching packed.map((c) => c.id) teaches the UI that every stuffed chunk was used. It was not. The model may name chunk-3 that was never packed, or skip a packed id that it never quoted. Intersect. Citations and grounding is the entailment check on top of that allow-list.
What the UI owes the user
A spinner that becomes a paragraph is a chatbot. RAG as a product usually needs:
- Citations the user can open. A footnote that does not match the sentence is worse than no footnote.
- A refusal path. “No sources” is a feature. Inventing policy is an incident.
- Trace ids. You will debug “it was weird” only if you stored query, packed chunk ids, model-emitted ids, prompt version, and token counts.
Untrusted retrieved HTML is still untrusted text. Treat it like XSS: it can carry instructions, not only facts.
Interview answer (out loud)
“RAG is search plus generation. I index chunks with metadata, retrieve top-k for the question, pack a budgeted subset into the prompt, and ask the model to answer only from those sources. The model never sees the rest of the corpus. A vector database is optional. Failures are usually retrieval or packing, not ‘the LLM forgot.’ Empty hits should refuse. Citations are the ids the model named, intersected with the packed set; anything else is a fake cite.”
Further reading (primary sources)
Related on this site
- Chunking strategies for RAG
- Embeddings and similarity search
- Citations and grounding
- When RAG is the wrong tool
- Tokens and context windows
Related guides
- Building a Docs RAG ChatbotBuild a docs chatbot on MDX: ingest pages, embed, retrieve, then stream a cited answer. The shape you can ship on a content site.
- Chunking Strategies for RAGChunking decides what the retriever can return. Size, overlap, headings, and why splitting on a 500-character timer wrecks tables and code.
- Citations and GroundingGrounding means the answer is entailed by retrieved text. Citations are the UI. How to require quotes, and how models still hallucinate a source.
- Document Ingestion PipelinesA document ingestion pipeline extracts, cleans, chunks, embeds, upserts, and deletes. Version it so a parser bug can be rebuilt.
- Embeddings and Similarity SearchNearest-neighbor search over embeddings is fast guesswork. Distance metrics, top-k, and why the nearest chunk can still be the wrong chunk.