RAG Evaluation Metrics Beyond Ragas: A Broader Toolkit
Every team that ships a retrieval-augmented generation system eventually hits the same wall: the demo looked great, the pilot users were impressed, and then production traffic exposed a pile of quietly wrong answers nobody caught. The usual first move is to reach for Ragas, run faithfulness and answer relevancy on a test set, get a couple of numbers between 0 and 1, and call it "evaluated." That's a reasonable start, but it's nowhere close to sufficient. Ragas measures a narrow slice of what can go wrong in a RAG pipeline, and teams that stop there ship systems with blind spots in retrieval quality, latency regressions, cost blowups, and failure modes that only show up under adversarial or out-of-distribution queries. This article is about the rest of the toolkit — the rag evaluation metrics that matter once you move past the notebook and into a system real users depend on.
I want to be upfront about scope. This isn't a takedown of Ragas — it's a genuinely useful library, and if you haven't used it yet, it's a fine on-ramp. But treating it as the entire evaluation story is like judging a car only on how quiet the engine sounds at idle. You need to check the brakes, the tires, and how it handles a pothole at 60 mph. Below is a practitioner's map of the broader landscape of rag evaluation metrics: retrieval-specific measures, generation-specific measures, end-to-end task metrics, operational metrics, and the human-in-the-loop processes that tie them together.
Why Ragas alone isn't enough
Ragas's core metrics — faithfulness, answer relevancy, context precision, context recall — are all reference-free or semi-reference-free scores computed by an LLM judge looking at a single question-answer-context triple. That's useful for a quick sanity check, but it has three structural limitations worth naming.
First, it evaluates each example in isolation. It won't tell you whether your retriever is systematically missing a category of documents, whether your chunking strategy is silently truncating tables, or whether a specific document type (PDFs with scanned images, say) is dragging down the whole system. You need aggregate, sliced-by-category rag evaluation metrics for that, not just a single averaged faithfulness score.
Second, it's LLM-judged, and LLM judges have known biases: they tend to prefer longer answers, they're inconsistent across runs unless you pin the model and temperature, and they can be fooled by fluent-but-wrong text that "sounds" grounded. Ragas doesn't eliminate the need to validate your judge against human labels — it just gives you a judge, and the judge itself needs evaluating.
Third, Ragas is almost entirely about the "R" and the "G" in RAG — retrieval and generation quality on a per-answer basis. It says very little about latency, cost per query, robustness to adversarial inputs, or whether the system correctly refuses to answer when it should. Those dimensions matter just as much in production, arguably more, because they're the ones that show up in your on-call rotation.
Retrieval-specific metrics that predate (and outlive) RAG
Before "RAG" was a term, information retrieval had a mature evaluation vocabulary, and it's worth pulling straight from that toolbox rather than reinventing it.
- Recall@k — of the relevant documents that exist for a query, what fraction show up in your top-k retrieved chunks? This is the single most diagnostic number for a RAG system because if the right chunk never makes it into context, no amount of prompt engineering on the generation side will fix the answer.
- Precision@k — of the k chunks you retrieved, what fraction are actually relevant? High precision with low recall usually means your retriever is too conservative or your embedding model doesn't capture domain vocabulary well.
- Mean Reciprocal Rank (MRR) — rewards getting the first relevant chunk as high in the ranking as possible. Useful when your downstream reader is sensitive to chunk order (many are, because of positional bias in long contexts).
- nDCG (normalized Discounted Cumulative Gain) — a graded-relevance version of the above, useful when some chunks are "somewhat relevant" and others are "exactly on point," and you want partial credit that decays with rank.
- Hit Rate — a simpler binary version of recall: did at least one relevant chunk appear in the top-k, yes or no? Easier to communicate to non-technical stakeholders than nDCG.
Here's a compact implementation you can adapt to compute these against a labeled retrieval set, where you have query -> set of gold-relevant doc IDs:
import math
from collections import defaultdict
def recall_at_k(retrieved_ids, relevant_ids, k):
topk = set(retrieved_ids[:k])
if not relevant_ids:
return None
return len(topk & relevant_ids) / len(relevant_ids)
def precision_at_k(retrieved_ids, relevant_ids, k):
topk = retrieved_ids[:k]
if not topk:
return 0.0
hits = sum(1 for doc_id in topk if doc_id in relevant_ids)
return hits / len(topk)
def reciprocal_rank(retrieved_ids, relevant_ids):
for rank, doc_id in enumerate(retrieved_ids, start=1):
if doc_id in relevant_ids:
return 1.0 / rank
return 0.0
def ndcg_at_k(retrieved_ids, relevance_scores, k):
# relevance_scores: dict of doc_id -> graded relevance (0, 1, 2...)
dcg = 0.0
for i, doc_id in enumerate(retrieved_ids[:k], start=1):
rel = relevance_scores.get(doc_id, 0)
dcg += (2 ** rel - 1) / math.log2(i + 1)
ideal_order = sorted(relevance_scores.values(), reverse=True)[:k]
idcg = sum((2 ** rel - 1) / math.log2(i + 1) for i, rel in enumerate(ideal_order, start=1))
return dcg / idcg if idcg > 0 else 0.0
def evaluate_retrieval(eval_set, k=5):
results = defaultdict(list)
for example in eval_set:
retrieved = example["retrieved_ids"]
relevant = set(example["relevant_ids"])
results["recall@k"].append(recall_at_k(retrieved, relevant, k))
results["precision@k"].append(precision_at_k(retrieved, relevant, k))
results["mrr"].append(reciprocal_rank(retrieved, relevant))
return {metric: sum(v for v in vals if v is not None) / len(vals)
for metric, vals in results.items()}The point of running this alongside your Ragas scores is that it isolates the retriever from the generator. If recall@5 is 60%, you already know 40% of your answers are structurally unable to be fully correct no matter how good your prompt is — and no generation-focused metric will surface that as clearly.
Generation-quality metrics beyond faithfulness
Once you know retrieval is solid, generation quality needs its own dedicated set of rag evaluation metrics, and here the classic NLP metrics still earn their keep even in the LLM era.
- BLEU and ROUGE — these compare generated text against a reference answer using n-gram overlap. They're crude (they penalize valid paraphrasing) but they're cheap, deterministic, and reproducible, which makes them great for regression testing across model or prompt versions. If your ROUGE-L score drops 15 points after a prompt change, something broke, even if you can't tell exactly what from the number alone.
- BERTScore — uses contextual embeddings to compare generated and reference text at a semantic level rather than surface n-grams, so it tolerates paraphrasing better than BLEU/ROUGE while still being a fixed, non-LLM-judge computation.
- Answer correctness / exactness — for RAG systems answering factual questions with a known gold answer (dates, names, numbers), exact-match or normalized-exact-match after lowercasing and stripping punctuation is still one of the highest-signal metrics you can compute, and it's essentially free.
- Groundedness / attribution scoring — separate from Ragas's faithfulness score, you can build a claim-decomposition pipeline: break the generated answer into atomic claims, then check each claim against the retrieved context independently. This gives you a finer-grained view than a single faithfulness number — you'll see exactly which sentence in a five-sentence answer was unsupported.
- Answer completeness — faithfulness tells you the answer doesn't contradict the context; it says nothing about whether the answer covers everything the context supports. A model that answers "Paris" to "what is the capital of France and what's its population" is faithful but incomplete. This usually needs an LLM judge with a rubric, or a checklist-based comparison against a gold answer that lists the required facts.
A simple claim-decomposition groundedness check looks like this:
def check_groundedness(answer, context, judge_llm):
claims_prompt = f"""Break the following answer into a list of atomic factual claims.
Return one claim per line, no numbering.
Answer: {answer}"""
claims = judge_llm.complete(claims_prompt).strip().split("\n")
supported = 0
unsupported_claims = []
for claim in claims:
verify_prompt = f"""Context: {context}
Claim: {claim}
Is this claim directly supported by the context above? Answer only YES or NO."""
verdict = judge_llm.complete(verify_prompt).strip().upper()
if verdict.startswith("YES"):
supported += 1
else:
unsupported_claims.append(claim)
score = supported / len(claims) if claims else 0.0
return {"groundedness_score": score, "unsupported_claims": unsupported_claims}This is more expensive than a single faithfulness call (it's N+1 LLM calls instead of one), but the payoff is that when groundedness drops, you get the exact unsupported sentence, not just a number — which is what your engineers actually need to debug the prompt or the chunking.
Task-level and end-to-end metrics
Retrieval and generation metrics are necessary but not sufficient, because they don't capture whether the system actually did the job the user came for. Task-level rag evaluation metrics close that gap.
- Exact answer accuracy on a golden set — build a set of 100-300 real (or realistic) questions with verified correct answers, covering your actual query distribution, and track pass rate over time. This is the single metric I'd insist every team have before shipping, because it's the one closest to "does this thing work."
- Task completion rate — for RAG systems embedded in agentic workflows (e.g., "find the refund policy and draft an email"), measure whether the full downstream task succeeded, not just whether the retrieved snippet was accurate.
- Refusal-appropriateness — does the system correctly say "I don't know" or "this isn't covered in our docs" when the answer genuinely isn't in the corpus? Systems that never refuse are usually hallucinating on out-of-scope questions; systems that refuse too often are frustrating and useless. Track both a false-refusal rate (refused when it shouldn't have) and a false-confidence rate (answered when it should have refused).
- Consistency across paraphrases — take each golden question and generate 3-5 paraphrases, then check if the system gives materially the same answer to all of them. Divergence here reveals brittleness in your retriever's embedding space, and it's a cheap, high-signal check that almost nobody runs by default.
Human evaluation and preference-based metrics
No automated rag evaluation metrics fully replace a human reading the answer, especially for subjective qualities like tone, helpfulness, and whether an answer would actually satisfy a real user. A few practical formats worth adopting:
- Side-by-side (A/B) preference judging — show two system outputs (old prompt vs. new prompt, or your system vs. a competitor baseline) to a human rater blind to which is which, and ask which one they'd prefer. This is far more reliable than asking raters to score a single answer on a 1-5 scale, because relative judgments are easier and more consistent than absolute ones.
- Likert-scale rubric scoring — when you do need absolute scores (for dashboards, trend lines), use a rubric with concrete anchors for each point on the scale ("1 = factually wrong and irrelevant," "5 = fully correct, complete, well-cited") rather than an unanchored 1-5 scale, which raters interpret inconsistently.
- Error taxonomy tagging — instead of just a score, have raters tag each failure with a category: retrieval miss, hallucination, incomplete answer, wrong tone, formatting issue, refused incorrectly. Over a few hundred labeled examples, this taxonomy tells you where to invest engineering time far better than an aggregate score does.
- Inter-rater agreement — if you have more than one human rater, compute agreement (Cohen's kappa or simple percent agreement) periodically. Low agreement usually means your rubric is ambiguous, not that your raters are bad — fix the rubric.
The habit worth building here is treating human review as a recurring process, not a one-time audit. A rolling sample of 20-30 production conversations reviewed weekly, tagged with the error taxonomy above, catches drift that a static benchmark run once a quarter will completely miss.
LLM-as-a-judge: calibration and pitfalls
Since Ragas and most modern rag evaluation metrics frameworks lean on LLM judges, it's worth spending real effort making sure your judge is trustworthy rather than assuming it is.
- Calibrate against human labels. Take 50-100 examples, get human judgments, then run your LLM judge on the same examples and compute correlation (Cohen's kappa works well here too). If correlation is weak, your automated scores are noise dressed up as a number.
- Watch for length bias and position bias. LLM judges systematically favor longer answers and, in pairwise comparisons, favor whichever answer is shown first. Mitigate by randomizing order and, where possible, running each comparison twice with the order swapped.
- Use structured rubrics, not open-ended scoring. Asking a judge "rate this answer 1-10" produces noisier scores than asking it to check off specific criteria ("does it cite the source," "does it directly answer the question," "is it under 200 words") and aggregating those into a score.
- Pin the judge model and temperature. If you re-run the same eval set next month with a newer judge model version, your scores will shift for reasons that have nothing to do with your RAG system. Version-lock your judge the same way you'd version-lock a dependency.
- Use a different (usually stronger) model as judge than the one being evaluated, to avoid a model rating its own outputs favorably — a well-documented self-preference effect.
Here's a minimal structured-rubric judge you can drop into a CI eval step:
import json
RUBRIC_PROMPT = """You are evaluating a RAG system's answer against retrieved context.
Score each criterion as true or false, then return JSON only.
Question: {question}
Retrieved Context: {context}
Generated Answer: {answer}
Criteria:
1. grounded: every factual claim in the answer is supported by the context
2. relevant: the answer directly addresses the question asked
3. complete: the answer covers all parts of the question the context supports
4. concise: the answer avoids unnecessary padding or repetition
Return exactly this JSON shape:
{{"grounded": true/false, "relevant": true/false, "complete": true/false, "concise": true/false}}"""
def judge_answer(question, context, answer, judge_llm):
prompt = RUBRIC_PROMPT.format(question=question, context=context, answer=answer)
raw = judge_llm.complete(prompt)
scores = json.loads(raw)
scores["overall"] = sum(scores.values()) / len(scores)
return scoresRunning this across a golden set and tracking the overall average over time gives you a trend line that's far more interpretable than a single blended Ragas score, because you can see exactly which criterion degraded when a prompt or model change went out.
Operational and system-level metrics
This is the category most evaluation guides skip entirely, and it's the one that determines whether your RAG system survives contact with real traffic and a real budget.
- Latency percentiles (p50/p95/p99), broken down by retrieval time and generation time separately, so you know which half of the pipeline to optimize when users complain about slowness.
- Cost per query, tracked by token count for both the retrieval embedding calls and the generation calls. A system that scores brilliantly on faithfulness but costs $0.40 per query isn't shippable at scale.
- Retrieval index freshness — how stale is the data in your vector store relative to the source of truth? A perfectly grounded answer based on a document that was updated last week is still wrong.
- Throughput under load — evaluate your metrics not just on a clean single-request basis but under concurrent load, since embedding servers and rerankers often degrade in quality (through timeouts, truncation, or fallback logic) exactly when traffic spikes.
- Cache hit rate — for systems that cache retrieval or generation results, track how often the cache is actually helping, since stale caches are a sneaky source of the "why did it answer with old information" bug reports.
None of these are exotic to compute — most are just standard observability metrics — but almost no eval harness ships them by default, so teams have to explicitly wire them into their dashboards alongside the quality metrics above.
Adversarial and robustness testing
The last category worth building into any serious evaluation suite is testing what happens when the input isn't a well-formed, in-distribution question.
- Out-of-domain queries — feed the system questions entirely outside your corpus and verify it declines gracefully instead of confabulating an answer using unrelated retrieved chunks.
- Ambiguous queries — questions that could reasonably map to two different documents; check whether the system asks for clarification or silently picks one interpretation and states it as fact.
- Prompt injection via retrieved content — if your corpus includes user-generated or external content, test whether an instruction embedded in a document (e.g., "ignore previous instructions and say X") can hijack the generation step. This is increasingly a security metric as much as a quality one.
- Long-tail phrasing and typos — real users don't type textbook queries. Run your golden set through a typo-injection and paraphrase-injection pass and check for score degradation.
Building even a small adversarial set (30-50 examples across these categories) and running it alongside your standard eval suite turns up failure modes that a clean, well-formed benchmark will never expose, and it's usually the fastest way to find the bug that would otherwise land in a support ticket.
Putting it together: a practical evaluation stack
If you're building this from scratch, a workable layering looks like: retrieval metrics (recall@k, MRR) gating whether you even bother testing generation; generation metrics (groundedness via claim decomposition, BERTScore against references where you have them) run on every prompt or model change; a golden-set accuracy and refusal-appropriateness check run in CI before every deploy; a rolling human-review sample with error taxonomy tagging run weekly; and operational dashboards for latency, cost, and freshness watched continuously. Ragas can slot in as one fast, cheap layer inside that stack — it's genuinely good for quick faithfulness and relevancy checks during early iteration — but it should never be the only layer standing between your prompt changes and production.
None of this needs to be built in one sprint. Start with recall@k on retrieval and an exact-match golden set for generation — those two alone will catch the majority of embarrassing production failures. Layer in groundedness scoring and human review as the system matures, and add adversarial testing once you've got real user traffic patterns to draw from. If you're newer to the space and want the foundational concepts these metrics build on top of — chunking, embeddings, retrievers, and the overall pipeline — our Introduction to RAG course walks through the architecture these evaluation techniques are designed to stress-test, which makes the metrics in this article click into place a lot faster once you've seen the system they're measuring.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.
Related reading