Overview
A single end-to-end accuracy number hides which half of a RAG system is broken. Score retrieval (did the right chunks come back?) and generation (did the answer use them correctly?) separately, on a fixed golden set, and rerun the suite on every change. For the general agent eval pattern, see evaluation.
Build the golden set from real failures
- Each row:
question,gold_chunk_ids[],gold_answer,slice_tag. - Start at 50 rows; grow toward 200 to 500 when you need to detect small differences between models. A 1-point recall gap on 200 queries is probably noise, so read the disagreements instead of trusting the aggregate.
- Sample queries from real logs. Include short queries, multi-hop questions, exact identifiers, rare terms, and time-sensitive questions.
- Version the set like code. Never edit a row to make a failing test pass.
Score retrieval with recall, MRR, and nDCG
recall@k: the fraction of gold chunks that appear in the top k. Track k = 5, 10, and 50. Usehit@k(at least one gold chunk in the top k) when one chunk suffices.MRR@k: the mean of 1 / rank of the first gold chunk. UseMRR@5when the prompt only sees five chunks.nDCG@k: use when chunks have graded relevance instead of binary.
def recall_at_k(retrieved, gold, k):
return len(set(retrieved[:k]) & gold) / len(gold)
def mrr_at_k(retrieved, gold, k):
return next((1 / r for r, c in enumerate(retrieved[:k], 1) if c in gold), 0.0)Read the gap between depths. High recall@50 with low recall@5 means the reranker is the lever (see rag-reranking). Low recall@50 means the candidate pool is missing the gold chunks, so fix chunking, the embedding model, or hybrid search first; a reranker cannot recover chunks that were never retrieved.
Score generation with faithfulness and answer relevance
- Faithfulness: is every claim in the answer supported by the retrieved context? Score per claim with a judge model or a RAGAS-style rubric.
- Answer relevance: does the answer respond to the question? An on-topic but evasive answer fails.
Faithfulness without relevance ships safe, useless answers; relevance without faithfulness ships confident hallucinations. Score citation validity and coverage too (see rag-citations).
Audit the judge
Judge models drift. Use a rubric with yes/no questions, not a vibe score, and prefer a stronger judge than the model under test. Each release, have humans rate 30 to 50 sampled rows and compare; when agreement is low, rewrite the rubric or retire the judge. See evaluation for the harness.
A/B embedding models and index settings on one golden set
When comparing models, dimensions, or index parameters, hold everything else fixed: same golden set, same chunks, same k, same query preprocessing. Change one variable at a time.
- Embed the corpus and queries with each candidate (same dimension unless dimension is the variable).
- Build identical indexes.
- Compute
recall@10,MRR@10, and latency per candidate, plus per-slice deltas. - Inspect the queries where the candidates disagree.
See embeddings for the models and embeddings-dimensionality for the dimension procedure.
Track slices, not just the aggregate
An average hides regressions on small slices. A change that is 2 points better overall but 10 points worse on adversarial is a regression. Tag rows (easy, edge, adversarial, time-sensitive, multi-hop, keyword-heavy) and report recall@5, faithfulness, answer relevance, p95 latency, and cost per query per slice.
Gate CI on the suite
prompt, model, chunker, or index change -> run suite -> diff vs baseline ->
block merge when any slice regresses past your tolerance- Run a sample on every PR and the full set nightly.
- Cache embeddings and reranker scores where deterministic so the suite stays cheap.
- Rerun after every embedding-model change, chunker change, or re-index.
Triage failures by stage
Read ten failed queries each release.
- Gold chunk never retrieved: retrieval failure (rag-chunking, rag-retrieval).
- Retrieved but outside the top k: reranker failure.
- In the prompt but the answer is wrong: generation or citation failure.