Skip to main content
Back to Blog
Guide
2026-06-03

RAG Regression Testing: Prevent Quality Drift 2026

Stop RAG quality drift in 2026 with golden datasets, CI eval gates, groundedness scoring, threshold alerts and versioned prompts and retrievers.

RAG Regression Testing: Prevent Quality Drift in 2026

A retrieval-augmented generation system is never finished. You upgrade the embedding model, re-chunk the corpus, tweak a prompt, swap the LLM, add ten thousand new documents, and every one of those changes can silently degrade answer quality. The demo that wowed your stakeholders last quarter can quietly start hallucinating, retrieving stale chunks, or answering a different question than the one asked, and nobody notices until a customer files a ticket. This slow, invisible decay is called quality drift, and it is the single biggest operational risk in production RAG.

RAG regression testing is the practice of catching drift before it ships. It borrows the core idea from software regression testing, a frozen suite of test cases that must keep passing, and adapts it for non-deterministic, LLM-generated output. Instead of asserting exact strings, you assert that quality metrics like faithfulness, groundedness, context recall and answer relevancy stay above thresholds on a curated golden dataset. Wire that suite into CI and every pull request that touches a prompt, a retriever or a model is automatically gated. This guide shows you how to build the whole system: golden datasets, CI eval gates, groundedness scoring in the pipeline, threshold alerts and proper versioning of prompts and retrievers.

If you are new to the underlying metrics, read our complete guide to RAG evaluation metrics first; this guide assumes you know what faithfulness and context recall mean and focuses on the testing methodology around them. The AI and LLM evaluation skills hub collects related installable skills.

What Quality Drift Actually Looks Like

Drift rarely announces itself. A typical sequence: a teammate updates the system prompt to be "more concise," which subtly drops the instruction to answer only from context. Faithfulness falls from 0.94 to 0.81 because the model now leans on its pretraining for gaps. End users see plausible-sounding answers that are occasionally fabricated. No exception is thrown, no log line is red, the latency is unchanged. Without a regression gate, this ships.

Other common drift triggers include an embedding model upgrade that reshuffles retrieval rankings, a corpus re-index that changes chunk boundaries, a provider silently updating the underlying LLM, and dependency bumps that change tokenization. Each is individually reasonable; collectively they erode quality. Regression testing converts each of these from an invisible risk into a visible, blocking signal.

Anatomy of a RAG Regression Suite

A regression suite has four parts: a golden dataset, a set of metrics with thresholds, a runner that executes the live pipeline against the dataset and scores the output, and a gate that fails the build when scores drop. Versioning ties it together so you can attribute a regression to the exact change that caused it.

ComponentPurposeFailure if missing
Golden datasetFrozen, representative queries with verified answersNo baseline to compare against
Metrics + thresholdsDefine "good enough" numericallyCannot decide pass/fail objectively
Eval runnerExecutes pipeline, computes scoresNo automation, drift goes unmeasured
CI gateBlocks merges that regress qualityBad changes reach production
Version manifestPins prompt, retriever, model versionsCannot reproduce or attribute regressions

Building the Golden Dataset

The golden dataset is the foundation. It is a curated, version-controlled collection of queries that represent how your system is actually used, each paired with a hand-verified ideal answer and, ideally, the document IDs that should be retrieved. Store it as JSON or YAML in your repo so changes are reviewed like code.

{
  "version": "2026.06.01",
  "cases": [
    {
      "id": "refund-window-annual",
      "query": "What is the refund window for annual plans?",
      "ground_truth": "Annual plans are refundable within 30 days of purchase.",
      "expected_doc_ids": ["billing-refunds-v3"],
      "category": "billing",
      "difficulty": "easy"
    },
    {
      "id": "multi-hop-sso-mfa",
      "query": "If SSO is enabled, do users still set up MFA separately?",
      "ground_truth": "No. When SSO is enabled, MFA is delegated to the identity provider and is not configured in-app.",
      "expected_doc_ids": ["auth-sso-v2", "auth-mfa-v2"],
      "category": "auth",
      "difficulty": "multi-hop"
    },
    {
      "id": "out-of-scope-pricing-2019",
      "query": "What did the Pro plan cost in 2019?",
      "ground_truth": "I do not have information about 2019 pricing.",
      "expected_doc_ids": [],
      "category": "out-of-scope",
      "difficulty": "adversarial"
    }
  ]
}

Three principles make a golden dataset effective. First, cover the long tail: include multi-hop questions that require combining two documents, ambiguous questions, and adversarial questions whose answer is not in the corpus, where the correct behavior is to refuse. A dataset of only easy lookups reports inflated scores. Second, label each case with a category and difficulty so you can slice results and spot which segment regressed. Third, version the dataset itself; when you add cases, bump the version so historical scores remain comparable.

Aim for 50 to 200 cases to start. Fewer and you miss failure modes; many more and CI gets slow and expensive. Grow the set by mining production logs: every real failure a user reports becomes a new golden case, so the suite gets stronger over time and the same bug can never regress twice.

Groundedness Scoring in the Pipeline

The phrase "a RAG workflow builder with groundedness scoring" describes a pipeline that computes groundedness, whether every claim in the answer is supported by retrieved context, on each response and exposes the result. In a regression suite you run this scorer over the whole golden dataset and assert the aggregate stays above threshold. The scorer is an LLM judge that decomposes the answer into claims and verifies each against the context.

from deepeval.test_case import LLMTestCase
from deepeval.metrics import FaithfulnessMetric, ContextualRecallMetric
import json


def load_golden(path: str) -> list[dict]:
    with open(path) as f:
        return json.load(f)["cases"]


def run_pipeline(query: str) -> tuple[str, list[str]]:
    """Your live RAG call. Returns (answer, retrieved_contexts)."""
    chunks = retriever.search(query, top_k=5)
    answer = generator.answer(query, chunks)
    return answer, [c.text for c in chunks]


def score_groundedness(cases: list[dict]) -> dict:
    faithfulness = FaithfulnessMetric(threshold=0.9)
    recall = ContextualRecallMetric(threshold=0.85)

    f_scores, r_scores, failures = [], [], []
    for case in cases:
        answer, contexts = run_pipeline(case["query"])
        tc = LLMTestCase(
            input=case["query"],
            actual_output=answer,
            expected_output=case["ground_truth"],
            retrieval_context=contexts,
        )
        faithfulness.measure(tc)
        recall.measure(tc)
        f_scores.append(faithfulness.score)
        r_scores.append(recall.score)
        if faithfulness.score < 0.9 or recall.score < 0.85:
            failures.append({
                "id": case["id"],
                "faithfulness": round(faithfulness.score, 3),
                "recall": round(recall.score, 3),
                "reason": faithfulness.reason,
            })

    return {
        "mean_faithfulness": sum(f_scores) / len(f_scores),
        "mean_recall": sum(r_scores) / len(r_scores),
        "failures": failures,
    }

The per-case failure list with reasons is what makes this actionable. When the gate trips, you do not just see "faithfulness dropped"; you see exactly which queries regressed and the judge's explanation, so you can reproduce and fix in minutes.

The CI Eval Gate

The gate is where regression testing earns its keep. On every pull request that touches prompts, retrievers, models or the corpus, CI runs the eval and fails the build if any metric falls below its floor. Below is a GitHub Actions workflow that gates merges.

name: rag-eval-gate

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'retriever/**'
      - 'rag/**'
      - 'eval/golden_dataset.json'

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      - run: pip install -r requirements.txt
      - name: Run RAG regression eval
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: python eval/run_gate.py --dataset eval/golden_dataset.json

The gate script enforces thresholds and writes a summary that posts back to the PR. Keep thresholds in a config file so adjusting them is a reviewed change, not a code edit.

# eval/run_gate.py
import sys
import json

THRESHOLDS = {
    "mean_faithfulness": 0.90,
    "mean_recall": 0.85,
    "mean_answer_relevancy": 0.85,
}


def main(dataset_path: str) -> int:
    cases = load_golden(dataset_path)
    results = score_groundedness(cases)

    violations = [
        f"{metric}: {results[metric]:.3f} < {floor}"
        for metric, floor in THRESHOLDS.items()
        if metric in results and results[metric] < floor
    ]

    print(json.dumps(results, indent=2))
    if violations:
        print("REGRESSION DETECTED:")
        for v in violations:
            print(f"  - {v}")
        return 1
    print("All RAG quality gates passed.")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[-1]))

A nonzero exit blocks the merge. Now a prompt edit that quietly drops faithfulness cannot reach main; the author sees the failing gate, the per-case reasons, and fixes it before review even starts. This is the same shift-left discipline covered in our DeepEval pytest LLM testing guide, applied to retrieval pipelines.

Threshold Alerts for Production Drift

CI gates catch drift caused by code changes, but some drift originates outside your repo: a provider updates the underlying model, the corpus grows, or query patterns shift. For that you need continuous evaluation on production traffic with threshold alerts. Sample a slice of real requests, score them with the same metrics, and alert when a rolling average crosses a floor.

# Scheduled job: score a sample of yesterday's production traffic
def daily_drift_check(samples: list[dict], alert_floor: float = 0.88) -> None:
    results = score_groundedness(samples)
    score = results["mean_faithfulness"]
    if score < alert_floor:
        send_alert(
            channel="#rag-oncall",
            message=(
                f"Faithfulness drift: {score:.3f} < {alert_floor} "
                f"over {len(samples)} sampled requests. "
                f"{len(results['failures'])} cases below floor."
            ),
        )

Set the alert floor slightly above your CI gate so you get a warning before quality becomes user-visible. Run the job nightly and chart the rolling average so gradual erosion is obvious. This pairs naturally with observability tooling like TruLens, covered in our TruLens evaluation framework guide, which logs feedback functions on live calls automatically.

Versioning Prompts and Retrievers

Regression attribution is impossible without versioning. When faithfulness drops, you need to answer "what changed?" instantly. Capture a manifest of every component on each eval run and store it alongside the scores.

ArtifactWhat to versionWhy
PromptTemplate hash + semantic versionMost frequent drift source
RetrieverEmbedding model, top_k, chunk size, rerankerChanges retrieval rankings
CorpusIndex snapshot ID + document countRe-indexing shifts chunks
GeneratorLLM model name and versionProvider updates change behavior
JudgeEval model + temperatureScores only comparable within one judge
# eval/manifest.yaml — pinned configuration for a reproducible eval run
run_id: 2026-06-03T09-14Z
prompt:
  version: '4.2.0'
  hash: 'a91f3c'
retriever:
  embedding_model: 'text-embedding-3-small'
  top_k: 5
  chunk_size: 512
  reranker: 'bge-reranker-v2'
corpus:
  index_snapshot: 'kb-2026-06-01'
  document_count: 14820
generator:
  model: 'gpt-4o'
judge:
  model: 'gpt-4o-mini'
  temperature: 0

With manifests, a regression review becomes a diff: compare the failing run's manifest to the last passing one, and the changed line is your prime suspect. Critically, pin the judge model and temperature; a score from one judge is not comparable to a score from another, so changing the judge requires re-baselining the entire suite.

A Practical Rollout Plan

Adopt regression testing incrementally. Week one, build a 50-case golden dataset from your most common queries plus a handful of known failures, and write the scorer. Week two, stand up the CI gate on faithfulness and context recall only, with thresholds set just below your current baseline so the gate is not noisy. Week three, add answer relevancy and correctness, then introduce the production drift job with alerts. From there, every user-reported failure becomes a new golden case, and the suite compounds in value.

Resist two temptations. Do not set thresholds so high that the gate flaps and the team starts ignoring it; a gate that is muted is worse than no gate. And do not let the golden dataset go stale; review it quarterly, prune cases that no longer reflect usage, and keep mining production for new ones. A living dataset that tracks real usage is the difference between regression testing that catches drift and a suite that gives false confidence.

Comparing Per-Component and End-to-End Gates

Teams frequently debate whether to gate on a single end-to-end answer-correctness score or on the individual component metrics. The honest answer is that you need both, but they play different roles, and conflating them is a common source of weak regression suites.

Gate typeCatchesMissesUse as
End-to-end correctness"The final answer got worse"Which component caused itTop-line health signal
FaithfulnessNew hallucinationsRetrieval gapsHard CI gate
Context recallMissing evidenceGenerator problemsHard CI gate
Context precisionRetrieval noise creeping inGeneration qualitySoft warning gate
Answer relevancyOff-topic or rambling answersFactual errorsSoft warning gate

The pattern that works in practice is to make faithfulness and context recall the hard gates that block a merge, because they catch the two failure modes that most damage user trust: fabrication and incompleteness. Treat context precision and answer relevancy as soft gates that post a warning on the pull request but do not block, since modest fluctuations there are usually tuning noise rather than true regressions. Track end-to-end correctness as a top-line number on the dashboard so leadership has a single quality figure, but never rely on it alone to gate, because by the time it moves you have lost the diagnostic signal of which component broke. This layered gating mirrors the metric-to-fix mapping covered in our RAG evaluation metrics guide: the component that regresses tells you exactly where to look.

Handling Non-Determinism in the Gate

The hardest practical problem in RAG regression testing is that both the system under test and the judge are probabilistic. The same query can produce a slightly different answer on each run, and the same answer can earn a slightly different score from the judge. If you ignore this, your gate will flap, firing on noise, and the team will learn to re-run it until it passes, which destroys its value.

Three techniques tame the noise. First, pin the generator temperature to a low value (0 or near it) during evaluation so the answer is as stable as the model allows; you can still keep a higher temperature in production, but the eval should test a deterministic configuration. Second, pin the judge model and its temperature to 0, and never silently upgrade the judge, because a score from one judge is not comparable to a score from another. Third, run high-variance cases a small number of times and aggregate, treating a case as failing only if it falls below threshold on the majority of runs.

def stable_score(case: dict, runs: int = 3) -> float:
    """Average a metric over a few runs to damp judge noise."""
    scores = []
    for _ in range(runs):
        answer, contexts = run_pipeline(case["query"])
        scores.append(measure_faithfulness(case["query"], answer, contexts))
    scores.sort()
    # Median is robust to a single outlier run.
    return scores[len(scores) // 2]

The cost is more model calls, so reserve multi-run scoring for the cases that historically show variance and single-run scoring for the stable majority. The payoff is a gate that fires on real regressions and stays quiet on noise, which is the only kind of gate a team will keep trusting over the long term.

Frequently Asked Questions

What is RAG regression testing?

RAG regression testing is the practice of running a frozen golden dataset of queries through your live retrieval pipeline on every change and asserting that quality metrics, faithfulness, groundedness, context recall and answer relevancy, stay above defined thresholds. It adapts software regression testing for non-deterministic LLM output by checking statistical quality properties instead of exact string matches, catching drift before it reaches users.

What is a RAG workflow builder with groundedness scoring?

It is a pipeline that computes groundedness, whether every claim in the answer is supported by the retrieved context, on each response and surfaces the score along with supporting source spans. In a regression suite you run this scorer over the entire golden dataset and gate the build on the aggregate, so any change that introduces hallucination is blocked automatically before merge.

How big should my RAG golden dataset be?

Start with 50 to 200 cases. Fewer than 50 and you miss important failure modes; many more and CI becomes slow and costly. Prioritize coverage over volume: include easy lookups, multi-hop questions requiring two documents, ambiguous queries, and adversarial out-of-scope questions where the correct answer is a refusal. Grow the set over time by converting every production failure into a new golden case.

How do I gate CI on RAG quality without flaky failures?

Pin the judge model and temperature to zero for reproducible scores, set thresholds slightly below your current baseline rather than at aspirational levels, and average each metric across the full dataset so one noisy case cannot trip the gate. Run a few repetitions on high-variance cases and exclude or rewrite any that the judge scores inconsistently. A stable gate is one the team trusts and never mutes.

How do I detect RAG quality drift in production?

Sample a slice of real requests daily, score them with the same metrics used in CI, and chart a rolling average. Set a threshold alert slightly above your CI gate so a warning fires before quality becomes user-visible. Observability tools that log feedback functions on every live call make this continuous and surface gradual erosion, like a provider model update, days before users complain.

How do I attribute a RAG regression to its cause?

Version every component on each eval run in a manifest: prompt hash and version, embedding model, top_k, chunk size, reranker, corpus snapshot, generator model and judge configuration. When a metric drops, diff the failing run's manifest against the last passing one. The changed line, a new prompt version or an upgraded embedding model, is your prime suspect, turning regression triage into a quick comparison.

Conclusion

Quality drift is inevitable in any evolving RAG system, but it does not have to be invisible. A golden dataset, metric thresholds, a CI eval gate, groundedness scoring in the pipeline, production threshold alerts and disciplined versioning together turn drift from a silent risk into a blocking, attributable signal. Build the suite incrementally, keep the dataset alive, and pin your judge, and you will ship retrieval changes with the same confidence you ship ordinary code.

Start by defining the metrics you will gate on in our RAG evaluation metrics guide, explore automated tooling at the AI and LLM evaluation skills hub, and browse the full QA skills directory to install agent skills that scaffold golden datasets and CI gates for you. You can also compare evaluation frameworks to choose the right scorer for your pipeline.

RAG Regression Testing: Prevent Quality Drift 2026 | QASkills.sh