TutorialsRAG evaluationRAG evaluation harnessLLM testingRAGAS

Build a RAG Evaluation Harness: A Step-by-Step Tutorial for 2026

Build a RAG evaluation harness in 2026: define datasets, instrument retrieval, wire up scoring, and gate CI on quality thresholds. Step-by-step guide.

Why RAG Is Still Ungoverned in 2026 — and What That Costs You

Retrieval-augmented generation (RAG) is now the default architecture for enterprise LLM applications. Yet many teams still ship RAG systems with no quantitative evaluation harness — they eyeball a few outputs, ship to production, and hope. That works until an embedding model upgrade shifts the vector space enough to quietly drop recall on your long-tail queries, or a prompt tweak increases unsupported claims on financial questions.

This tutorial walks you through building a working RAG evaluation harness in days, not quarters. You'll define a dataset, instrument retrieval and generation separately, wire up scoring, and gate your CI pipeline on measurable quality thresholds — the same pattern mature AI platform teams use in 2026. (See also: LLM observability best practices.)

Why RAG Systems Fail Without an Eval Harness

RAG quality degrades silently. Traditional software breaks loudly — tests fail, services crash, logs light up. RAG systems fail quietly: answers get slightly less grounded, retrieval misses a few more relevant chunks, and nobody notices until a customer escalates.

Every change to your stack is a potential regression vector: a new embedding model, a chunking parameter tweak, a prompt edit, a vector index rebuild, a reranker swap. Without automated evaluation, each of these ships blind.

For engineering leaders, eval tooling is now a procurement line item, not a side project. The build-vs-buy decision for AI evaluation — open-source frameworks versus managed platforms — deserves the same rigor as your observability stack.

The Three Failure Layers: Retrieval, Generation, Orchestration

RAG failures cluster into three layers, and each needs its own instrumentation:

  • Retrieval failures: The right chunk exists but never surfaces — embedding-space drift, poor chunking, or index staleness.
  • Generation failures: The right context is retrieved, but the model hallucinates, ignores sources, or answers off-topic.
  • Orchestration failures: Context window overflow, prompt template bugs, or reranker misconfiguration corrupts the pipeline between retrieval and generation.

If you can't tell which layer failed, you can't fix it. A harness that scores only the final answer hides the root cause.

Core Concepts: What You're Actually Measuring

Before building, align on what "good" means. RAG evaluation splits into two metric families.

Retrieval Metrics vs. Generation Metrics

Retrieval metrics assess whether you fetched the right context:

  • Context precision: Rank-weighted precision@k — relevant chunks retrieved higher in the ranking count more than those buried at position k. (This is the RAGAS definition; a flat "fraction relevant" ignores ranking, which is what your reranker actually changes.)
  • Context recall: What fraction of the ground-truth relevant chunks appear anywhere in the top-k?
  • Hit rate@k: How often does at least one relevant chunk appear in the top-k?
  • MRR@k (Mean Reciprocal Rank): The mean of 1/(rank of the first relevant chunk), computed over queries where a relevant chunk appears in the top-k.

Generation metrics assess whether the model used that context correctly:

  • Faithfulness: Is every claim in the answer entailed by the retrieved context? (Claim-level check; a single unsupported claim lowers the score.)
  • Answer relevancy: Does the answer address the question actually asked, without padding or drift?
  • Groundedness: Can each statement be traced to a specific source chunk? Distinct from faithfulness — groundedness asks which chunk supports a claim, faithfulness asks whether the context supports it at all.

The critical distinction: LLM testing differs fundamentally from traditional software testing. Outputs are non-deterministic, correctness is semantic rather than exact-match, and the same input can yield different valid answers. Your harness must tolerate variance while still catching real regressions. (Related: testing non-deterministic LLM outputs.)

The LLM-as-Judge Caveat

Most generation metrics are computed by an LLM judge, and judges have known failure modes you must control for:

  • Position bias: In pairwise comparisons, judges favor the first (or sometimes last) answer. Randomize order.
  • Verbosity bias: Longer answers score higher even when less correct. Normalize or instruct against it.
  • Self-preference: A model judges its own outputs more favorably. Use a different judge model than your generator where possible.
  • Judge drift: Provider model updates silently change your scores. Pin the judge model version and re-baseline when you upgrade.

Calibrate before you trust the judge: hand-label 50–100 examples and report agreement between judge and human. If agreement is below ~80% on your task, fix the rubric or change judges before gating CI on the number.

Step 1 — Define Your Evaluation Dataset

Your dataset is the foundation. A weak dataset produces meaningless scores.

Seed with 50–200 real production queries. Synthetic queries miss the messy phrasing, typos, and multi-part questions real users produce. Pull from logs, support tickets, and search analytics. The 50–200 range is a practical starting floor — enough to detect a roughly 5–10 point regression on a stratified stratum, small enough to hand-label. Grow it over time; a mature harness typically runs into the low thousands.

Stratify by query type. Group queries into categories — factual lookup, comparison, multi-hop reasoning, unanswerable questions — and score each stratum separately. A single aggregate score hides that your multi-hop performance collapsed while simple lookups stayed flat.

Include golden answers and ground-truth source chunks. For each query, record the ideal answer and the specific chunk IDs that should be retrieved. This enables retrieval metrics, not just answer scoring.

Version the dataset alongside your code. Treat it like a test suite: commit it to git, tag releases, and review changes in PRs. When you add a query, reviewers should see it.

[ILLUSTRATION: A table showing a sample evaluation dataset with columns for query, query type, golden answer, and expected chunk IDs]

Step 2 — Choose Your Evaluation Stack

The 2026 landscape offers mature options. Match the tool to your constraints, not the hype cycle. (Capabilities below reflect early 2026; re-validate before procurement.)

FrameworkStrengthsTrade-offs
RAGASPurpose-built RAG metrics; clean mapping to retrieval vs. generation layers; fast startMetric coverage narrower than general eval tools; judge quality depends on the model you supply
DeepEvalPytest-style CI integration; broad metric library; good assertion ergonomicsRAG-specific scoring takes more wiring than RAGAS
TruLensStrong tracing and observability; good for debugging why a score movedHeavier footprint for simple pipelines; more concepts to learn
LangSmithManaged SaaS; smooth for LangChain stacks; good dataset and experiment UISaaS default (self-host is a paid tier); per-trace costs grow at scale
Custom harnessFull control; tailored metrics; no external dependencyBuild and maintenance burden; you own judge calibration and drift

Decision criteria: evaluate on cost at your expected CI volume, CI integration quality, metric coverage for each failure layer, and self-hosted versus SaaS requirements. Regulated industries often need self-hosted scoring to keep data in-VPC.

Open-Source Frameworks: RAGAS and Alternatives

RAGAS is the common starting point — its faithfulness, answer relevancy, context precision, and context recall metrics map cleanly to the failure layers above. But don't assume it's the endpoint. Teams frequently start on RAGAS and later add DeepEval for pytest integration or a custom scorer for domain metrics (e.g., "did the answer cite the correct policy version?"). Design your harness so scorers are pluggable; you will swap and add them.

Step 3 — Instrument Retrieval and Generation Separately

The most common harness mistake is scoring only the final answer. Instrument both layers so a regression tells you where it broke.

# harness.py — minimal but complete scoring pipeline
from dataclasses import dataclass
from statistics import median

@dataclass
class EvalCase:
    query: str
    query_type: str
    golden_answer: str
    expected_chunk_ids: list[str]

def retrieval_metrics(retrieved_ids: list[str], expected_ids: list[str], k: int = 10) -> dict:
    top_k = retrieved_ids[:k]
    hits = [cid for cid in top_k if cid in expected_ids]
    recall = len(set(hits)) / len(expected_ids) if expected_ids else 0.0
    hit_rate = 1.0 if hits else 0.0
    mrr = 0.0
    for i, cid in enumerate(top_k, start=1):
        if cid in expected_ids:
            mrr = 1.0 / i
            break
    # rank-weighted precision: relevant chunks at higher ranks count more
    precision = sum((len(top_k) - i + 1) for i, cid in enumerate(top_k, start=1) if cid in expected_ids)
    precision = precision / (len(top_k) * (len(top_k) + 1) / 2) if top_k else 0.0
    return {"recall": recall, "hit_rate": hit_rate, "mrr": mrr, "precision": precision}

def score_case(case: EvalCase, retrieved_ids, answer, judge, n_samples: int = 3) -> dict:
    # Run generation metrics N times; take the median to damp judge variance.
    runs = [judge.score(case.query, answer, context=retrieved_ids) for _ in range(n_samples)]
    gen = {m: median(r[m] for r in runs) for m in runs[0]}
    return {**retrieval_metrics(retrieved_ids, case.expected_chunk_ids), **gen}

Two design rules this encodes:

  1. Retrieval is deterministic given a fixed index — score it once per case. Generation is not — score it N times and take the median (or mean with a confidence band). This is how you make a non-deterministic system gate-able.
  2. Every metric carries its layer. When a run fails, the report says "recall dropped on multi-hop queries" or "faithfulness fell on financial queries," not "score went down."

Step 4 — Gate CI on Measurable Thresholds

A harness that only reports is a dashboard, not a gate. Wire it into CI so a regression blocks the merge.

Set thresholds from a baseline, not a guess. Run the harness on your current production configuration, record per-stratum means and standard deviations, and set thresholds at mean minus 1–2 standard deviations — or as a fixed regression budget (e.g., "no stratum may drop more than 5 points"). Never set a threshold you haven't measured.

# .github/workflows/rag-eval.yml (excerpt)
- name: Run RAG eval
  run: python -m harness.run --dataset eval/dataset.jsonl --output eval/results.json

- name: Enforce thresholds
  run: |
    python -m harness.gate \
      --results eval/results.json \
      --baseline eval/baseline.json \
      --max-regression 0.05 \
      --min-faithfulness 0.90

Control flakiness. Non-determinism means a raw threshold will occasionally fail a good build. Mitigate with: median-of-N scoring (above), fixed seeds where the stack allows, a small tolerance band, and a rule that a failing gate is investigated, not re-run until green. Re-running to pass is how eval harnesses die.

Budget the cost. LLM-as-judge at CI volume is not free. Estimate: (cases) × (metrics) × (N samples) × (judge tokens). At 200 cases, 3 generation metrics, 3 samples, and a mid-tier judge model, expect low single-digit dollars per run — trivial per PR, but plan for it in your eval budget line.

What "Done" Looks Like

A working harness by the end of this tutorial gives you:

  • A versioned, stratified dataset with golden answers and expected chunk IDs.
  • Per-layer scores (retrieval and generation) reported per stratum, not just in aggregate.
  • A calibrated judge with documented human agreement.
  • A CI gate that fails on measured regressions, with flakiness controls in place.

That is the floor. The teams that keep RAG quality high over time treat the harness as a living system: they grow the dataset from production failures, re-baseline when the judge model changes, and review eval diffs in the same PR as the code change that caused them.

ShareX / TwitterLinkedIn
← Back to Learn