Why Evaluation Matters in RAG
Understand the two independent failure modes in RAG systems: retrieval failure and generation failure, and learn why you need separate metrics to diagnose each one.
Why Evaluation Matters in RAG is a free AI Engineering Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
You Cannot Improve What You Do Not Measure
A RAG system can feel like it is working because it returns fluent, plausible-sounding answers. But without measurement, you have no idea whether it is actually retrieving the right chunks or generating faithful answers. Teams that skip evaluation often spend months tweaking chunking strategies and prompt formats based on gut feeling, only to discover they made things worse. Rigorous evaluation is what turns RAG development from guessing into engineering.
Two Independent Failure Modes
RAG has two distinct stages that can fail independently: retrieval and generation. Retrieval fails when the relevant chunks are not ranked in the top-K results — the LLM cannot generate a good answer if the right information was never retrieved. Generation fails when the correct chunks were retrieved but the LLM ignored them, misread them, or added hallucinated information. You need separate metrics for each stage to pinpoint which component is causing the problem.
The Danger of End-to-End Evaluation Only
Measuring only the final answer quality hides where failures come from. Suppose your system gives wrong answers 30% of the time. Is that because retrieval is missing the right chunks, or because the LLM is ignoring good chunks? If you only know the final error rate, you cannot know which component to fix. Instrument both stages separately: measure retrieval quality with golden datasets and generation quality with faithfulness scores.
# Diagnosis example: which stage is failing?
# Test 1: Is retrieval finding the right chunks?
retrieval_hit_rate = evaluate_retrieval(questions, ground_truth_chunks)
print(f'Retrieval hit rate@5: {retrieval_hit_rate:.1%}')
# If this is low (< 80%), fix chunking and embedding first
# Test 2: Given perfect context, does LLM generate correct answers?
generation_faithfulness = evaluate_generation(questions, perfect_context)
print(f'Generation faithfulness: {generation_faithfulness:.1%}')
# If retrieval is fine but this is low, fix your promptsBuilding a Golden Dataset
Evaluation requires a golden dataset: a set of question-answer pairs where you know the correct answer and, ideally, which document and chunk the answer comes from. For a minimum viable evaluation set, collect 50-100 questions representative of real user queries. Answer them by hand or by reading the source documents. Include diverse question types: factual lookups, comparisons, multi-hop reasoning, and out-of-domain questions the system should refuse to answer.
# Golden dataset format
golden_dataset = [
{
'question': 'How many vacation days do employees receive in their first year?',
'answer': '15 days',
'relevant_chunks': ['employee_handbook_p24', 'benefits_summary_p3'],
'source_doc': 'employee_handbook_2025.pdf'
},
{
'question': 'What is the parental leave duration for primary caregivers?',
'answer': '16 weeks fully paid',
'relevant_chunks': ['parental_leave_policy_p1'],
'source_doc': 'parental_leave_policy.pdf'
}
]Generating Golden Datasets with LLMs
Creating 100 questions manually is tedious. Accelerate this with a data generation LLM: feed each document chunk to GPT-4o and ask it to generate 3-5 diverse questions whose answers can be found in that chunk, plus the expected answer text. Review a sample manually to catch quality issues. This approach scales to thousands of questions quickly, though it may miss edge cases that only real users would ask.
def generate_qa_pairs_for_chunk(chunk_text, llm_client):
prompt = (
'Given the following document excerpt, generate 3 diverse questions '
'that can be answered using ONLY this text. '
'For each question, provide the exact answer from the text.\n\n'
f'Text:\n{chunk_text}\n\n'
'Format each as JSON: {"question": ..., "answer": ...}'
)
response = llm_client.chat.completions.create(
model='gpt-4o-mini',
messages=[{'role': 'user', 'content': prompt}]
)
return response.choices[0].message.contentRetrieval Evaluation: Hit Rate
Hit rate@K is the fraction of questions for which at least one relevant chunk appears in the top-K retrieval results. It is the simplest and most intuitive retrieval metric. A hit rate@5 of 85% means that for 85 out of 100 questions, the relevant chunk was among the top 5 results. Track hit rate separately for different document types, query lengths, and topic categories to find where your retriever struggles most.
def compute_hit_rate(golden_dataset, retriever, top_k=5):
hits = 0
for item in golden_dataset:
results = retriever.retrieve(item['question'], top_k=top_k)
retrieved_ids = {r['id'] for r in results}
relevant_ids = set(item['relevant_chunks'])
if retrieved_ids & relevant_ids: # intersection not empty
hits += 1
hit_rate = hits / len(golden_dataset)
print(f'Hit rate@{top_k}: {hit_rate:.1%} ({hits}/{len(golden_dataset)})')
return hit_rateGeneration Evaluation: Faithfulness
Faithfulness measures whether the generated answer contains only information that can be verified in the retrieved context. An unfaithful answer adds facts the context does not support — that is hallucination. Score faithfulness by having a judge LLM (or human evaluator) check each sentence in the answer against the context and flag any claims not supported by the retrieved chunks. A faithfulness score of 95%+ is the target for production systems.
def evaluate_faithfulness(answer, context, llm_client):
prompt = (
'Given this context and answer, evaluate faithfulness.\n\n'
f'Context: {context}\n\n'
f'Answer: {answer}\n\n'
'For each sentence in the answer, determine if it is '
'supported by the context (FAITHFUL) or not (HALLUCINATED). '
'Return a JSON: {"score": 0.0-1.0, "issues": ["sentence..."]}.'
)
response = llm_client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}]
)
return response.choices[0].message.contentGeneration Evaluation: Answer Relevance
Answer relevance measures whether the generated answer actually addresses the user's question. A highly faithful answer might still miss the point by answering a related but different question. Measure relevance separately from faithfulness. Use a judge LLM to rate whether the answer directly addresses what was asked, using a 1-5 scale. Low answer relevance often indicates a problem with the prompt structure or the retrieved context not actually containing the answer.
def evaluate_answer_relevance(question, answer, llm_client):
prompt = (
f'Question: {question}\n\n'
f'Answer: {answer}\n\n'
'Rate how well this answer addresses the question on a 1-5 scale:\n'
'5 = fully answers the question\n'
'3 = partially answers but misses key aspects\n'
'1 = does not address the question at all\n\n'
'Return JSON: {"score": 1-5, "reason": "brief explanation"}'
)
response = llm_client.chat.completions.create(
model='gpt-4o',
messages=[{'role': 'user', 'content': prompt}]
)
return response.choices[0].message.contentTracking Metrics Over Time
Evaluation is most valuable when you track metrics over time as you make changes. Store evaluation results in a database or spreadsheet with timestamps and version labels (e.g., chunk_size=500, embed=3-small, k=5). When you try a new chunking strategy or embedding model, run the same evaluation and compare. This prevents regression — you might improve faithfulness but accidentally reduce hit rate. Always run the full evaluation before merging changes to production.
import json
from datetime import datetime
def save_evaluation_results(metrics, config, output_file='eval_history.jsonl'):
record = {
'timestamp': datetime.utcnow().isoformat(),
'config': config,
'metrics': metrics
}
with open(output_file, 'a') as f:
f.write(json.dumps(record) + '\n')
print(f'Saved eval result: hit_rate={metrics["hit_rate"]:.1%}, '
f'faithfulness={metrics["faithfulness"]:.1%}')The Evaluation Mindset
Beyond specific metrics, evaluation requires a mindset shift: treat RAG as a machine learning system with measurable performance, not a chatbot you subjectively assess by chatting with it. Define success criteria upfront (e.g., hit rate@5 > 85%, faithfulness > 95%). Establish a test set that stays frozen and is never used for development decisions. Reserve a separate dev set for iteration. This discipline separates teams that ship reliable RAG from teams that ship impressive demos that fail in production.
Test Set vs Development Set
A critical discipline in machine learning that also applies to RAG evaluation is the train-dev-test split. Your test set should be completely frozen — never used to make development decisions. Use a separate dev set for experimenting with chunking strategies, prompt changes, and embedding models. Only run the test set when you believe a change is production-ready. This separation prevents overfitting your RAG pipeline to the test set and ensures your final reported metrics reflect genuine generalization.
import json
from sklearn.model_selection import train_test_split
def split_golden_dataset(all_questions, test_ratio=0.3, seed=42):
dev_set, test_set = train_test_split(
all_questions,
test_size=test_ratio,
random_state=seed
)
print(f'Dev set: {len(dev_set)} questions')
print(f'Test set: {len(test_set)} questions (FROZEN)')
with open('eval/dev_set.json', 'w') as f:
json.dump(dev_set, f, indent=2)
with open('eval/test_set.json', 'w') as f:
json.dump(test_set, f, indent=2)
return dev_set, test_setQuick Check
Test your understanding of AI Engineering concepts from this lesson.
Lesson Recap
In this lesson you learned: the two independent failure modes of retrieval and generation that require separate metrics, how to build a golden dataset of question-answer-chunk triples for objective evaluation, hit rate as the core retrieval metric and faithfulness and answer relevance as the core generation metrics, and the importance of tracking metrics over time to prevent regressions. Next up we implement specific retrieval metrics including MRR and NDCG.
Frequently asked questions
Is the “Why Evaluation Matters in RAG” lesson free?
Yes — the full text of “Why Evaluation Matters in RAG” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.
What will I learn in “Why Evaluation Matters in RAG”?
Understand the two independent failure modes in RAG systems: retrieval failure and generation failure, and learn why you need separate metrics to diagnose each one. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start AI Engineering Academy?
No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Why Evaluation Matters in RAG” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this AI Engineering Academy lesson?
Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.