Chapter 06
Evaluation & pitfalls
A RAG system has two things that can go wrong β retrieval and generation β so you have to measure both. βIt looks good in the demoβ is how RAG projects quietly fail in production.
The RAG triad
Three questions cover most failure modes. Together they separate a retrieval problem from a generation problem β which tells you what to fix.
Context relevance
Did retrieval fetch passages that actually relate to the question? Bad β fix chunking / retrieval / reranking.
Faithfulness
Is the answer supported by the retrieved context, with nothing made up? Bad β the model is hallucinating past its sources.
Answer relevance
Does the answer actually address the user's question? Bad β prompt/generation issue.
Metrics that pin down where it breaks
| Metric | Measures | Stage |
|---|---|---|
| Context precision | Of retrieved chunks, how many are relevant (and are the relevant ones ranked high)? | Retrieval |
| Context recall | Of the chunks needed to answer, how many did we actually retrieve? | Retrieval |
| Faithfulness | Are the answer's claims grounded in the retrieved context? | Generation |
| Answer relevance | Does the answer address the question directly? | Generation |
How to actually measure it
Golden set
Curate question β ideal-answer (and ideal-sources) pairs. Your regression test β run it on every change to the pipeline.
LLM-as-judge
Use a strong model to score faithfulness and relevance at scale. Correlates well with humans and is far cheaper than manual review.
Frameworks
RAGAS, TruLens, and similar compute the triad metrics for you from your questions, contexts, and answers.
Retrieval-only metrics
Evaluate the retriever in isolation with recall@k / precision@k / MRR before you even involve the LLM.
Faithfulness with an LLM judge
The core check: does every claim in the answer trace back to the retrieved context? A structured judge call turns that into a score.
import anthropic
client = anthropic.Anthropic()
def faithfulness(answer: str, context: str) -> dict:
resp = client.messages.create(
model="claude-opus-4-8",
max_tokens=512,
system=(
"You are a strict RAG evaluator. Decide whether EVERY claim in the answer "
"is supported by the context. Reply as JSON: "
'{"supported": true|false, "unsupported_claims": [...], "score": 0-1}.'
),
messages=[{
"role": "user",
"content": f"CONTEXT:\n{context}\n\nANSWER:\n{answer}",
}],
)
return resp.content[0].text # parse the JSON verdict
# Run this across your golden set and track the average score over time.
Failure modes to watch for
π³οΈ Missing content
The answer isn't in the corpus at all β the system should say βI don't knowβ, not invent one.
π Retrieval miss
The right chunk exists but ranked below top-k. Fix with hybrid search, reranking, or better chunking.
ποΈ Lost in the middle
Relevant context is buried among noise; models attend less to the middle of long prompts. Retrieve less, rerank harder.
π Ignored context
The right context was supplied but the model answered from its own priors anyway. Strengthen the βuse only the contextβ instruction.
π§© Bad chunking
Answers split across chunk boundaries, or chunks mixing topics. Revisit chunk size/overlap and structure-aware splitting.
π₯« Stale index
Docs changed but the index didn't. Schedule re-indexing for anything that updates.
Wrap-up
You now have the whole arc: why retrieval beats a bare model, the pipeline, the retrieval stack (chunking, embeddings, hybrid search, reranking), the advanced frontier, and how to evaluate it. That's a production-grade mental model of RAG β and of the broader move toward context engineering.