DEV Community

Cover image for "đź§Ş RAG Without CI Metrics Is Just a Chatbot"
Journal A.I.
Journal A.I.

Posted on

"đź§Ş RAG Without CI Metrics Is Just a Chatbot"

Staging “looks fine” is how RAG regressions ship.

Someone updates the corpus, bumps an embedding model, or tweaks chunk size. Answers still sound confident. A week later support hears “it cites the wrong policy page.”

Put metrics on the pull request

You need a frozen question set with expected doc ids or answer keys. Run it in CI when retrieval code or corpus fingerprints change.

Useful offline signals:

  • Recall@k — did the right chunk appear in the top k?
  • MRR — how high was the first relevant hit?
  • Faithfulness / citation hit — does the answer point at retrieved ids?
  • Empty retrieval rate — sudden spikes mean a broken index build

You do not need a perfect LLM judge on day one. Start with retrieval metrics. Add answer judges once retrieval is stable.

# evals/rag_ci.py
from statistics import mean

def recall_at_k(hits, relevant, k=5):
    top = {h.doc_id for h in hits[:k]}
    return len(top & set(relevant)) / max(1, len(relevant))

def evaluate(cases, retrieve):
    scores = []
    for case in cases:
        hits = retrieve(case["question"], k=5)
        scores.append(recall_at_k(hits, case["relevant_ids"], k=5))
    return mean(scores)

def main():
    score = evaluate(load_cases("evals/rag_cases.json"), build_retriever())
    threshold = 0.75
    print(f"recall@5={score:.3f} threshold={threshold}")
    if score < threshold:
        raise SystemExit(1)

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

When CI should run

Trigger on:

  • changes under rag/, embeddings/, chunking/
  • corpus fingerprint file changes
  • scheduled nightly for full rebuild drift

Skip on unrelated frontend PRs so people do not learn to hate the job.

Honest trade-off

A fixed case set goes stale. Budget an hour every two weeks to add failures from production. If you never refresh cases, CI protects an old product.

LLM-as-judge is convenient and noisy. Keep it advisory until you measure agreement with humans on a sample.

Wire it like any other test

- run: python -m evals.rag_ci
- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: rag-eval
    path: evals/last-run.json
Enter fullscreen mode Exit fullscreen mode

Make the check required when retrieval is on the critical path for customers. If RAG is still experimental, keep the job visible but non-blocking, and write a date to flip it to required.

Monday plan

  1. Export 30 real questions from support or search logs.
  2. Label relevant doc ids by hand (painful once, valuable forever).
  3. Fail the PR under a recall threshold you can defend.

Do you know last week’s recall@5, or only that the chatbot “felt better”?

Top comments (2)

Collapse
 
recallrundev profile image
RecallRun •

One thing I'd add from running a 30-question golden set in CI: the mean threshold hides more than you'd think. With 30 cases, one question flipping moves the score 3.3 points, so a 0.75 floor can let a couple of real regressions through while the average still looks fine.

A per-question diff against the last green run catches that: fail if a question that passed before now misses, and print which ones, so the PR author sees the actual queries. Gating on one metric isn't enough either. In my runs a hit@1-only gate missed a regression that dropped hit@5 from 0.933 to 0.800.

The nightly full rebuild is a good call too. Fixture-based CI won't catch drift in the live index.

Collapse
 
ahmetozel profile image
Ahmet Ă–zel •

The recall function maps an empty relevant_ids set to zero through max(1, len(relevant)). That makes a deliberate no-answer case indistinguishable from a complete retrieval miss, even if returning nothing was the correct behavior.

I would give negative cases a separate expectation, such as no eligible supporting document, and report their false-support rate alongside recall on positive cases. A fixture with one positive question and one intentionally unsupported question would expose the issue immediately. The aggregate gate could then enforce both behaviors without rewarding a retriever for always returning something.