RAG Evaluation: Metrics to Test Retrieval and Answer Quality
How to evaluate a RAG system: hit rate, recall@k, MRR, faithfulness and answer relevance, a golden question set, LLM judges and a repeatable test loop.
At the end of a semester, a professor at an Indore college checks an open-book exam. She asks two separate questions about every answer. Did the student open the right pages of the textbook? And did they write what those pages actually say, without adding made-up facts?
A student can fail either way. One opens the wrong chapter and writes nonsense confidently. Another opens the right page and still misquotes it.
Grading a RAG system works the same way. You check the "looking up" and the "writing" separately, because they fail for different reasons and need different fixes.
What is RAG evaluation?
Mapping the exam: the textbook pages the student opened are the retrieved chunks, the written answer is the LLM's output, the answer key is your labelled golden set, and the professor's two checks are retrieval metrics and generation metrics.
RAG evaluation is the practice of measuring a retrieval-augmented generation system with a fixed set of test questions, scoring both whether it retrieves the right context and whether its answers are faithful to that context, relevant and correct.
Every lesson in this series ended with "measure it": chunk sizes in RAG chunking strategies, fusion settings in hybrid search and re-ranking, and HyDE in HyDE explained. This lesson shows how, on the same fictional KIT college handbook.
The two halves of RAG quality
Each metric checks one relationship. Context metrics test retrieval; faithfulness and relevance test generation; correctness needs a reference answer.
| Metric | Checks | Stage | Needs a label? |
|---|---|---|---|
| Hit rate@k | Is at least one right chunk in the top k? | Retrieval | Relevant chunk IDs |
| Recall@k | What share of all needed chunks is in the top k? | Retrieval | Relevant chunk IDs |
| Precision@k | What share of the top k is relevant? | Retrieval | Relevant chunk IDs |
| MRR | How high is the first right chunk? | Retrieval | Relevant chunk IDs |
| Faithfulness (groundedness) | Is every claim in the answer supported by the context? | Generation | No |
| Answer relevance | Does the answer address the question asked? | Generation | No |
| Correctness | Does the answer match the reference answer? | End to end | Reference answer |
The diagnosis follows from the pattern. Low retrieval scores mean fix chunking, search or re-ranking. Good retrieval with low faithfulness means fix the prompt or the model.
Retrieval metrics: hit rate, recall, precision and MRR
Take one question that needs two chunks: "Can I get a refund if I withdraw in week two, and when is it credited?". The retriever returns five chunks, and the two you need sit at ranks 2 and 4.
def hit_at_k(ranked, relevant, k):
return int(any(d in relevant for d in ranked[:k]))
def precision_at_k(ranked, relevant, k):
return sum(d in relevant for d in ranked[:k]) / k
def recall_at_k(ranked, relevant, k):
return sum(d in relevant for d in ranked[:k]) / len(relevant)
def reciprocal_rank(ranked, relevant):
for rank, d in enumerate(ranked, start=1):
if d in relevant:
return 1 / rank
return 0.0
# "Can I get a refund if I withdraw in week two, and when is it credited?"
relevant = {"fees#refund", "fees#credit"} # both chunks are needed
ranked = ["fees#late", "fees#refund", "lib#fine", "fees#credit", "att#min"]
for k in (1, 3, 5):
print(f"k={k}: hit={hit_at_k(ranked, relevant, k)} "
f"precision={precision_at_k(ranked, relevant, k):.2f} "
f"recall={recall_at_k(ranked, relevant, k):.2f}")
print("reciprocal rank:", reciprocal_rank(ranked, relevant))At k = 3 the hit rate is 1, yet recall is only 0.50: the credit-timing chunk is missing, so the answer will be half right.
At k = 5 recall reaches 1.00 but precision is 0.40, meaning three of five chunks are noise in the prompt. The reciprocal rank is 0.5 because the first relevant chunk sits at rank 2.
Mean reciprocal rank (MRR) is the average of that value over all questions. Use hit rate for single-fact questions, recall when answers need several chunks, and MRR when position matters, for example before a small top-k.
Quick check
A question needs two chunks, and only one is in the top 3. What are hit@3 and recall@3?
Hit rate only needs one relevant chunk in the top 3; recall counts the share of all needed chunks found, which is 1 of 2.
Comparing two retrievers on a golden set
A golden set is a list of real questions, each labelled with the chunk IDs that answer it. Once you have one, comparing two designs takes seconds. Here a word-level TF-IDF retriever competes with character n-grams, which tolerate variants like "refund" and "refunds":
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
chunks = {
"fees#late": "Semester fees are due by 15 July and a late fee of 100 rupees per day applies.",
"fees#refund": "Withdrawal in the first 15 days of classes earns a 50 percent refund.",
"fees#credit": "Approved refunds are credited to your bank account within 10 working days.",
"att#min": "Each subject needs 75 percent attendance to sit the end-semester exam.",
"att#medical": "A medical certificate must reach the office within 7 days of rejoining.",
"exam#reval": "Revaluation of a paper costs 300 rupees and must be requested within 7 days of the result.",
}
golden = [ # (question, ids of chunks that answer it)
("How many days until my refund is credited?", {"fees#credit"}),
("Do I get any money back if I withdraw in week two?", {"fees#refund"}),
("What is the penalty for paying semester fees late?", {"fees#late"}),
("What attendance do I need to write exams?", {"att#min"}),
("When must I submit a doctor's note after illness?", {"att#medical"}),
("How do I get my answer sheet rechecked?", {"exam#reval"}),
]
ids, texts = list(chunks), list(chunks.values())
def evaluate(vectorizer, k=3):
vec = vectorizer.fit(texts)
hits, rr = [], []
for q, relevant in golden:
scores = cosine_similarity(vec.transform([q]), vec.transform(texts))[0]
order = [ids[i] for i in np.argsort(-scores)]
rank = next((r for r, d in enumerate(order, 1) if d in relevant), None)
hits.append(rank is not None and rank <= k)
rr.append(1 / rank if rank else 0)
return np.mean(hits), np.mean(rr)
for name, v in {"word TF-IDF": TfidfVectorizer(stop_words="english"),
"char 3-5 grams": TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5))}.items():
hit, mrr = evaluate(v)
print(f"{name:<15} hit@3 = {hit:.2f} MRR = {mrr:.2f}")Character n-grams win clearly here: hit@3 rises from 0.67 to 1.00 and MRR from 0.56 to 1.00. Be suspicious of a perfect score on six questions, though.
"Rechecked" probably matched "revaluation" through shared fragments like "re", which is luck, not understanding. With 6 questions, one lucky match moves hit rate by 17 points. That is why you need 30 to 50.
Generation metrics: faithfulness and answer relevance
Good retrieval does not guarantee a good answer. The model can still add facts that are not in the context. Faithfulness, also called groundedness, is the share of claims in the answer that the retrieved context supports.
The usual method has three steps: split the answer into individual claims, check each claim against the context, and divide supported claims by total claims. This toy checker uses word overlap and numbers instead of an LLM, to show the shape of the calculation:
import re
context = ("Withdrawal in the first 15 days of classes earns a 50 percent refund. "
"Approved refunds are credited to your bank account within 10 working days.")
answer = ("You get a 50 percent refund if you withdraw within 15 days. "
"The money is credited within 10 working days. "
"You can also claim a full refund of the hostel deposit within 30 days.")
def numbers(text):
return set(re.findall(r"\d+", text))
def content_words(text):
stop = {"you", "the", "a", "if", "is", "of", "in", "to", "can", "get", "also", "your", "within"}
return {w for w in re.findall(r"[a-z]+", text.lower()) if w not in stop}
ctx_nums, ctx_words = numbers(context), content_words(context)
claims = re.split(r"(?<=\.)\s+", answer)
supported = 0
for claim in claims:
nums_ok = numbers(claim) <= ctx_nums # every number appears in context
overlap = len(content_words(claim) & ctx_words) / len(content_words(claim))
ok = nums_ok and overlap >= 0.5
supported += ok
print(f"{'SUPPORTED ' if ok else 'UNSUPPORTED'} overlap={overlap:.2f} | {claim}")
print(f"Faithfulness = {supported}/{len(claims)} = {supported / len(claims):.2f}")The first two claims are backed by the context. The third invents a hostel deposit rule and a 30-day window that appear nowhere in the retrieved text, so faithfulness is 0.67. The claim might even be true somewhere in the handbook, but the model did not get it from the context, which makes it a hallucination risk.
Word overlap is crude: it cannot tell "50 percent refund" from "no 50 percent refund". Real systems use an LLM judge for this check.
Using an LLM as the judge
An LLM judge receives the context and one claim and returns a verdict. Keep the prompt narrow, ask for a fixed format, and test it against human labels before trusting it. LLM-as-a-judge covers the biases to watch for.
You are checking whether a claim is supported by the context.
Answer with exactly one word: SUPPORTED, CONTRADICTED or NOT_FOUND.
Context:
{retrieved_chunks}
Claim:
{one_sentence_from_the_answer}Answer relevance is judged in a similar way: does the answer address the question, or does it wander? A faithful answer about hostel curfew is still useless if the student asked about refunds.
Open-source libraries package these metrics. Ragas, for example, offers faithfulness, answer relevancy, context precision and context recall. Their APIs change between versions, so check the current docs before copying examples.
Quick check
Retrieval hit rate is high, but users still get wrong facts that are not in the retrieved chunks. Which metric exposes this?
Faithfulness checks whether each claim in the answer is supported by the retrieved context; it catches hallucinations that good retrieval does not prevent.
How to build a RAG evaluation set
Evaluation is a loop: one fixed question set, one change at a time, and failure review after each run.
- Collect real questions. Use help-desk emails, chat logs and questions from students, not only ones you invent. Include Hinglish and typos if users write that way.
- Cover the hard cases. Add multi-part questions, questions using codes like "ATKT", and questions the handbook cannot answer.
- Label each question with the chunk IDs that answer it and a short reference answer. For unanswerable questions, the reference is "I don't know".
- Run the pipeline and store everything: retrieved IDs, scores, prompt and answer.
- Score retrieval and generation separately using the metrics above.
- Read the failures. Twenty minutes reading wrong answers teaches more than any average.
- Change one thing, rerun, compare. Keep the change only if the numbers improve and nothing important regresses.
LLMs can draft extra questions from your chunks to grow the set, but review them; generated questions tend to copy the chunk's wording and make retrieval look better than it is.
Common mistakes in RAG evaluation
- Only testing end-to-end answers. When the score drops you cannot tell whether retrieval or generation broke.
- Testing on questions you wrote while reading the documents. They reuse the documents' words and hide vocabulary mismatches.
- Too few questions. Six questions make every change look dramatic; 30 to 50 is a sensible minimum.
- Trusting an unvalidated judge. Compare the judge's verdicts with human labels on 20 to 30 examples first.
- Never re-running. Re-run after every change to chunking, embeddings, prompts or models, and before every release.
For evaluating LLM outputs more broadly, beyond RAG, see LLM evaluation methods.
Key takeaways
- Evaluate retrieval and generation separately so you know what to fix.
- Hit rate, recall, precision and MRR measure retrieval against labelled chunk IDs.
- Faithfulness and answer relevance measure generation, usually with a validated LLM judge.
- Build a golden set of 30 to 50 real questions, including unanswerable ones.
- Change one thing at a time and rerun the same set every time.
Evaluation is what separates a demo from a project an interviewer takes seriously. A Generative AI course in Indore that teaches RAG evaluation hands-on is offered at Debugshala, and you can benchmark yourself with timed MCQs on Debugshala Test.
FAQ
How do you evaluate a RAG system?
Build a set of real questions labelled with the chunks that answer them and reference answers. Run the pipeline, then score retrieval with hit rate, recall and MRR, and score answers for faithfulness, relevance and correctness, often with an LLM judge. Rerun the same set after every change.
What is faithfulness in RAG?
Faithfulness, also called groundedness, measures whether every claim in the answer is supported by the retrieved context. It is the share of supported claims out of all claims. Low faithfulness means the model is adding information from memory or inventing it, even when retrieval worked.
How many questions do I need to evaluate RAG?
Start with 30 to 50 real questions covering common, tricky and unanswerable cases. Fewer makes results noisy, because one question can swing scores by several points. Grow the set as you find new failure types in production logs, and keep a fixed core set for comparisons.
What is the difference between recall@k and hit rate@k?
Hit rate@k is 1 if at least one relevant chunk appears in the top k, otherwise 0. Recall@k is the fraction of all relevant chunks that appear in the top k. They are equal for single-chunk questions but differ when an answer needs several chunks.
Can I learn RAG evaluation at a GenAI course in Indore?
Yes. Debugshala's Generative AI course at its Old Palasia campus in Indore includes RAG projects, taught in English and Hindi. It is open to fresh graduates and non-IT learners, includes placement assistance, and you can attend a free demo class before paying.
From the team behind this site
Go further with the Generative AI Course at Debugshala
These tutorials are written by Debugshala, a training and placement institute at Old Palasia, Indore. The classroom course adds live mentoring, real projects and interview preparation.
- Offline classes at Old Palasia, Indore
- Free demo class before you pay
- Non-IT, fresher and year-gap friendly
- Placement assistance
700+ students placed · 200+ hiring partners · 4.9★ Google rating