Retrieval-Augmented Generation¶
Summary — Give the model the facts it needs at inference time instead of hoping they were memorized during training. RAG is the standard architecture for grounding LLMs in private or current data. This page covers the pipeline stage by stage, the chunking decisions that dominate quality, a failure taxonomy that tells you which stage to fix, and how to evaluate each stage separately.
Prerequisites: → Embeddings & vector search · Next: → Agents & tool use
1. The pipeline¶
INDEXING (offline, once)
────────────────────────
documents ──► parse ──► chunk ──► embed ──► vector index
│
└──► also: BM25 index, metadata store
QUERYING (online, per request)
──────────────────────────────
user query ──► [rewrite] ──► retrieve (hybrid) ──► rerank ──► top-k chunks
│
▼
LLM ◄── prompt = system + chunks + query
│
▼
answer + citations
Why RAG rather than fine-tuning
Fine-tuning changes behaviour; retrieval changes knowledge. Fine-tuning a model on your documents teaches it their style and makes it hallucinate confidently in that style. Retrieval puts the actual text in front of it.
| RAG | Fine-tuning | |
|---|---|---|
| Update knowledge | re-index (minutes) | retrain (days) |
| Attribution | ✅ natural — cite the chunk | ❌ impossible |
| Access control | ✅ filter at retrieval time | ❌ baked into weights |
| Cost per query | higher (more tokens) | lower |
| Teaches format/style | ❌ | ✅ |
| Hallucination risk | lower (grounded) | higher |
2. Chunking: the decision that matters most¶
That comparison assumes the retrieved chunks are actually good ones. Whether they are depends almost entirely on the first real decision in building a RAG system: how the source documents get split up in the first place.
Chunking quality sets the ceiling for everything downstream
No amount of retrieval or reranking sophistication recovers from chunks that split a table in half or separate a claim from its qualifier.
TOO SMALL (128 tokens) TOO LARGE (2000 tokens)
┌──────────┐ ┌────────────────────────┐
│ "...the │ ← context lost │ topic A │
└──────────┘ │ topic B ← the query │
┌──────────┐ │ topic C only needs │
│ rate is │ ← dangling │ topic D topic B │
└──────────┘ │ topic E │
┌──────────┐ └────────────────────────┘
│ 4.5%..." │ embedding is a blurred average;
└──────────┘ retrieval precision drops
Strategies, in increasing order of quality and effort:
| Strategy | How | When |
|---|---|---|
| Fixed-size | \(N\) tokens, \(M\) overlap | baseline; always works |
| Recursive | split on \n\n → \n → . → until under the limit |
⭐ good default |
| Structural | split on Markdown headings, HTML sections, code functions | ⭐ best when structure exists |
| Semantic | split where consecutive-sentence embedding similarity drops | expensive, modest gain |
| Proposition | an LLM rewrites text into standalone factual statements | expensive, high quality |
| Late chunking | embed the whole document, then pool per chunk | keeps global context in each chunk embedding |
Starting parameters: 512 tokens with 50–100 tokens of overlap. Overlap prevents a fact from being severed at a boundary.
Three techniques that punch above their weight
(a) Contextual retrieval — prepend an LLM-generated context sentence to each chunk before embedding it:
Original chunk: "Revenue grew 3% over the prior quarter."
Contextualized: "This chunk is from ACME Corp's Q2 2024 10-Q, discussing
segment performance. Revenue grew 3% over the prior quarter."
Anthropic reported this substantially reduces retrieval failures. It costs one cheap LLM call per chunk at index time — a one-off expense for a permanent gain. It directly fixes the "this chunk is meaningless without knowing what document it came from" problem.
(b) Parent-document retrieval — embed small chunks for precision, but return their larger parent section to the LLM. Best of both: sharp retrieval, complete context.
(c) Multi-representation indexing — index a summary or hypothetical question for each chunk, but return the chunk itself. Queries look more like questions than like document prose, so matching question-to-question works better than question-to-prose.
3. Query processing¶
Chunking decides what's in the index. A separate lever is what you actually search with — the raw user query is often a worse search string than something derived from it.
The user's query is often a poor retrieval key.
| Technique | Problem it solves |
|---|---|
| Query rewriting | conversational references ("what about last year?" → a standalone query) |
| Multi-query | generate 3–5 paraphrases, retrieve for each, union the results |
| HyDE | generate a hypothetical answer, embed that, and retrieve with it |
| Decomposition | split a multi-hop question into sub-questions |
| Step-back | ask a more general question first, retrieve background, then the specific one |
| Routing | choose which index/collection to search |
HyDE (Hypothetical Document Embeddings) is the cleverest of these
Queries and documents live in different linguistic registers — a question ("what causes X?") looks nothing like an answer passage ("X is caused by..."). Asking the LLM to hallucinate a plausible answer and embedding that puts the search vector in document-space, where the real answers are. The hallucination's factual content is irrelevant; only its shape matters.
Pitfall
Each of these adds latency and cost. Multi-query with 5 variants means 5× the retrieval work plus an LLM call. Measure whether it helps on your data before adopting it.
4. The failure taxonomy¶
Chunking, indexing and query processing are all places a RAG pipeline can go wrong, each in a different way. Rather than guess which one is broken, it helps to have a structured list of every failure mode and where in the pipeline each one points.
This table is the most practically useful thing on this page. When RAG gives a bad answer, diagnose which stage failed:
| # | Failure | Symptom | Fix |
|---|---|---|---|
| 1 | Missing content | the answer isn't in the corpus at all | ingest more; detect and say "I don't know" |
| 2 | Missed the top-k | the right chunk exists but wasn't retrieved | hybrid search, better embeddings, larger \(k\), reranker |
| 3 | Lost in reranking/context | retrieved but not used | reorder (best first/last), fewer chunks, reranker |
| 4 | Not extracted | the chunk is in context but the LLM missed it | better prompt, stronger model, less distraction |
| 5 | Wrong format | right facts, wrong shape | explicit format instruction, structured output |
| 6 | Incomplete | partial answer | multi-hop retrieval, larger \(k\) |
| 7 | Ignored context | model answers from parametric memory instead | prompt to use only the context; check for conflicts |
| 8 | Bad chunk boundaries | truncated tables, split claims | structural chunking, larger overlap |
The diagnostic procedure
For a failing query, check in order — (a) is the answer in the corpus? (b) is the right chunk in the top-50? (c) is it in the top-5 after reranking? (d) did the LLM use it? Each "no" points at exactly one fix. Debugging RAG without this decomposition is guesswork.
Empirically the most common failure is #2 — retrieval recall. This is why hybrid search and reranking are the two highest-value additions.
5. Generation¶
Retrieval failures dominate, but they aren't the only ones — a query can retrieve the perfect chunk and the system can still produce a bad answer, if the generation step mishandles what it was given.
A prompt template that handles the things that actually go wrong:
RAG_PROMPT = """Answer the question using ONLY the provided context.
Rules:
- If the context does not contain the answer, say "I don't have enough information
to answer that." Do not use outside knowledge.
- Cite the source id in brackets after each claim, like [3].
- If sources conflict, say so and present both.
- Quote exact figures and dates rather than paraphrasing them.
<context>
{context}
</context>
Question: {question}
Answer:"""
def format_context(chunks):
# Put the most relevant chunk FIRST and the second-most LAST:
# both ends of the context are attended to more strongly.
if len(chunks) > 2:
chunks = [chunks[0]] + chunks[2:] + [chunks[1]]
return "\n\n".join(
f"[{i}] (source: {c['source']}, section: {c.get('section','?')})\n{c['text']}"
for i, c in enumerate(chunks, 1))
The reordering trick exploits 'lost in the middle'
(→ Long context §6). Put your two best chunks at the extremes, where attention is strongest.
Knowledge conflict
When the retrieved context contradicts the model's parametric knowledge, behaviour is unpredictable. Models generally favour the context when it is clear and specific, and fall back on parametric memory when the context is vague. Explicit instructions help but are not a guarantee. For high-stakes applications, verify that cited claims actually appear in the cited chunk.
6. Evaluation¶
That knowledge-conflict problem is exactly the kind of failure a RAG system needs to be tested for, not just designed against. Turning "did the model use the context correctly" into a number you can track is the next step.
Evaluate retrieval and generation separately
A single end-to-end score tells you something is wrong but not what.
Retrieval metrics¶
| Metric | Formula | Meaning |
|---|---|---|
| Recall@k | (relevant in top-\(k\)) / (total relevant) | did we find it? ⭐ the one that matters most |
| Precision@k | (relevant in top-\(k\)) / \(k\) | how much noise? |
| MRR | \(\frac{1}{\|Q\|}\sum \frac{1}{\text{rank of first relevant}}\) | how high is the first hit? |
| nDCG@k | \(\frac{\text{DCG}@k}{\text{IDCG}@k}\), \(\text{DCG}=\sum_i \frac{rel_i}{\log_2(i+1)}\) | graded relevance, position-weighted |
| Hit rate | fraction of queries with ≥1 relevant hit | coarse but interpretable |
nDCG example. Retrieved relevance grades \([3, 0, 2, 1]\) (ideal would be \([3,2,1,0]\)):
Generation metrics (the RAGAS framework)¶
| Metric | Question it answers | How |
|---|---|---|
| Faithfulness | are all claims supported by the context? | decompose into claims, verify each against the context |
| Answer relevance | does it address the question? | generate questions from the answer, compare to the original |
| Context precision | is the retrieved context on-topic? | judge each chunk's relevance |
| Context recall | does the context contain everything needed? | check the ground-truth answer's claims against the context |
Faithfulness is the metric to prioritize
It directly measures hallucination, which is the failure mode RAG exists to prevent. A system that is 95% faithful and 80% relevant is far more trustworthy than the reverse.
Building an evaluation set without human labels: have an LLM generate questions from your chunks. The source chunk is then the ground-truth relevant document by construction. This gives you a few hundred retrieval test cases in an hour. ⚠️ It is biased toward questions that are easy to answer from a single chunk — supplement with real user queries as soon as you have them.
7. Advanced patterns¶
Faithfulness and context recall tell you the plain pipeline from §1 is working. For harder queries — ones that need several documents, or a query that has to be broken into parts — that plain pipeline isn't enough, and a few patterns extend it.
| Pattern | Idea | When |
|---|---|---|
| Self-RAG | the model decides whether to retrieve, and critiques what it gets | mixed query types |
| Corrective RAG (CRAG) | grade retrieved docs; if poor, fall back to web search | open-domain |
| GraphRAG | build an entity/relation graph; retrieve subgraphs and community summaries | questions spanning many documents |
| Agentic RAG | the LLM issues retrieval as a tool call, iteratively | complex multi-hop research |
| Recursive summarization | hierarchical summaries (RAPTOR) | "what is this whole corpus about?" |
| Long-context RAG | retrieve 50 chunks instead of 5 | when the window allows it |
GraphRAG addresses a genuine structural gap
Standard RAG answers "what does document X say about Y?" It cannot answer "what are the main themes across all 10,000 documents?" — no single chunk contains that, so retrieval has nothing to find. GraphRAG precomputes community summaries at index time so global questions have something to retrieve.
Long-context RAG is underrated
Retrieval recall failures dominate (§4), and a big context window lets you trade precision for recall — retrieve 50 chunks and let the model sort it out. This is more robust than heroic efforts to get \(k=5\) exactly right. → Long context §7
8. A complete implementation¶
from dataclasses import dataclass
@dataclass
class Chunk:
text: str
source: str
section: str = ""
parent_id: str | None = None
class RAGPipeline:
def __init__(self, retriever, reranker, llm):
self.retriever, self.reranker, self.llm = retriever, reranker, llm
def index(self, documents, chunk_size=512, overlap=64):
chunks = []
for doc in documents:
for c in recursive_split(doc.text, chunk_size, overlap):
# contextual retrieval: a one-off LLM call per chunk at index time
ctx = self.llm(
f"<document>{doc.text[:4000]}</document>\n"
f"<chunk>{c}</chunk>\n"
"In one sentence, situate this chunk within the document. "
"Output only that sentence.")
chunks.append(Chunk(text=f"{ctx}\n\n{c}", source=doc.id,
section=doc.section))
self.retriever.build(chunks) # builds BOTH the dense and BM25 indexes
self.chunks = chunks
def query(self, question, k=5, candidates=50, filters=None):
# 1. retrieve widely (hybrid), with metadata filters for access control
cands = self.retriever.search(question, n=candidates, filters=filters)
# 2. rerank with a cross-encoder
scored = self.reranker.rank(question, cands)
top = scored[:k]
# 3. abstain rather than answer from irrelevant context
if not top or top[0].score < RELEVANCE_FLOOR:
return {"answer": "I don't have enough information to answer that.",
"sources": []}
# 4. generate with citations
answer = self.llm(RAG_PROMPT.format(
context=format_context(top), question=question))
return {"answer": answer,
"sources": [{"id": i, "source": c.source, "text": c.text[:200]}
for i, c in enumerate(top, 1)]}
Pitfall
The RELEVANCE_FLOOR check is the single most important line for trustworthiness. Without
it, a query with no good match still retrieves the five least bad chunks and the model dutifully
answers from irrelevant context. Calibrate the floor on known-unanswerable queries.
Metadata filters are how you do access control
Never rely on the LLM to withhold information present in its context — filter at retrieval time, before anything reaches the model.
9. Exercises¶
Problem 1 — the failure taxonomy, diagnosed. A RAG system correctly retrieves the right chunk into the top-5, and it survives reranking into the top-3, but the final answer still omits a key fact that was in the retrieved context. Using §4's 8-point taxonomy, which failure number does this match, and what's the recommended fix?
Solution
This matches failure #4, "Not extracted": "the chunk is in context but the LLM missed it." Retrieval (failures #1–2) and reranking/ordering (failure #3) are both ruled out by the problem statement (the chunk made it to top-3, presumably within the context sent to the model). Per §4's fix column: "better prompt, stronger model, less distraction" — concretely, check whether the context has too many other chunks competing for attention (per the → Long context "lost in the middle" effect), try moving the critical chunk to the start or end of the context (§5's reordering trick), or make the prompt more explicit about needing to use all provided facts, not just the most salient one.
Problem 2 — contextual retrieval's cost, worked. A corpus has 50,000 chunks. Using §2's contextual-retrieval technique (one LLM call per chunk at index time to generate a situating sentence), and assuming each call costs $0.0003 (a small/cheap model) and takes 0.5 seconds (assume full parallelism across many concurrent calls, so wall-clock isn't simply \(50000\times0.5\)s), what's the one-time indexing cost in dollars? Per §2, is this a recurring cost or a one-off?
Solution
\(50{,}000\times\$0.0003=\$15.00\) — a modest one-time cost.
Per §2: "it costs one cheap LLM call per chunk at index time — a one-off expense for a permanent gain." This is not recurring for existing chunks — it only needs to run once per chunk, at the time that chunk is first indexed. It does recur incrementally as new documents are added to the corpus (each new chunk needs its own contextualizing call), but re-indexing the same 50,000 chunks repeatedly would be unnecessary and wasteful — the cost structure is "pay once per chunk, forever," not "pay per query" or "pay per re-index."
Problem 3 — abstention floor, calibrating it. Using §8's RELEVANCE_FLOOR mechanism,
suppose you test your system on 30 deliberately-unanswerable queries (questions with no answer
in the corpus) and 30 answerable ones, and plot the reranker's top-1 score for each group. The
unanswerable group's scores cluster around 0.15–0.35; the answerable group clusters around
0.55–0.95, with some overlap around 0.4–0.5. Where would you set RELEVANCE_FLOOR, and what's
the trade-off at each end of that overlap range?
Solution
Somewhere in the 0.4–0.5 overlap band is the right zone — exactly matching §8's instruction to "calibrate the floor on known-unanswerable queries."
Setting the floor near 0.4 (the low end of the overlap): fewer answerable queries get wrongly abstained-on (higher recall of real answers), but more unanswerable queries slip through and get a hallucinated-sounding "answer" from irrelevant context (worse precision on abstention) — riskier for trustworthiness.
Setting the floor near 0.5 (the high end): more unanswerable queries correctly trigger "I don't have enough information" (safer), but some genuinely answerable queries with weaker retrieval scores get incorrectly abstained-on too (lower recall, more frustrating false negatives for real users). Per §8's framing of the floor as "the single most important line for trustworthiness," the right choice depends on which failure mode is more costly in your specific application — a medical or legal RAG system would likely lean toward 0.5 (favor safety over recall), while a casual internal search tool might lean toward 0.4.
10. Key takeaways¶
| # | Takeaway |
|---|---|
| 1 | RAG supplies knowledge; fine-tuning shapes behaviour. Use the right tool. |
| 2 | Chunking sets the ceiling. Start at 512 tokens with overlap, and respect document structure. |
| 3 | Contextual retrieval (prepend an LLM-written situating sentence) is a cheap, large win. |
| 4 | HyDE works by moving the query into document-space; the hallucinated answer's accuracy is irrelevant. |
| 5 | Use the 8-way failure taxonomy to identify which stage broke before changing anything. |
| 6 | Retrieval recall is the most common failure → hybrid search + reranking are the top two fixes. |
| 7 | Put the best chunks at the start and end of the context. |
| 8 | Evaluate retrieval (recall@k, nDCG) and generation (faithfulness) separately. |
| 9 | Always implement an abstention path with a relevance floor. |
| 10 | Enforce access control with retrieval-time metadata filters, never by instructing the model. |
Further reading¶
- Lewis et al., Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks (2020) — the original.
- Gao et al., Retrieval-Augmented Generation for Large Language Models: A Survey (2023).
- Barnett et al., Seven Failure Points When Engineering a RAG System (2024).
- Gao et al., Precise Zero-Shot Dense Retrieval without Relevance Labels (HyDE, 2022).
- Es et al., RAGAS: Automated Evaluation of Retrieval Augmented Generation (2023).
- Edge et al., From Local to Global: A GraphRAG Approach to Query-Focused Summarization (2024).
- Sarthi et al., RAPTOR: Recursive Abstractive Processing for Tree-Organized Retrieval (2024).
Next → Agents & tool use