Handbooks  /  Evals in CI
Handbook~15 min readProductionworked math + runnable code
The Evals in CI Handbook

A test suite
for quality.

You'd never ship code without tests that block a broken build. Yet teams routinely tweak a prompt, eyeball three examples, and merge — then discover in production that they quietly broke a case they weren't looking at. Evals in CI fix that: run a golden set on every change and gate the merge on quality, exactly like unit tests gate on correctness. But there's a subtlety unique to LLMs — the average score can look fine while a real regression hides inside it. This handbook is about the gate that catches both.

01

Quality needs a gate

Code has a safety net: unit tests run on every change and block the merge if something breaks. LLM apps usually have nothing equivalent — quality lives in prompts, model versions, and retrieval configs, all of which change often, and all of which can silently degrade output. A one-line prompt edit that helps one case can wreck another, and without an automated check you find out from users.

Evals in CI close that gap. You maintain a golden set of representative cases, run the current build's outputs through a scorer, and gate the pull request on the result — merge only if quality holds. It's the same discipline as testing, applied to a fuzzier target. The one thing you can't reuse from normal testing is the assertion: LLM output isn't exact-match, so the gate has to be built from scores, and scoring non-deterministic output introduces the wrinkle that makes eval gates their own craft.

The one-sentence version

Run a golden set on every change and gate the merge on quality — but require both an aggregate score threshold AND per-case no-regression, because a healthy average can hide a broken case.

02

The golden set

A golden set is a curated collection of representative test cases — inputs paired with either reference answers or a scoring rubric — that stands in for "does this system work." It should cover the common paths, the tricky edge cases, and the failures you've fixed before (so they can't silently return). It's the LLM equivalent of a test suite, and its quality determines everything: a golden set that misses a scenario can't catch a regression in it.

Because outputs aren't exact-match, each case is scored, not asserted. The scorer might be an LLM judge against a rubric, embedding similarity to a reference, a task metric like token-level F1, or a hard check like "does the generated code pass its tests." Every case yields a number in a comparable range, and the eval run produces a vector of scores — one per case — for the current build. That vector, compared against a baseline, is what the gate reasons over. Keep the golden set in version control next to the code, and grow it every time a bug escapes: each production failure becomes a permanent test.

03

The hidden regression

The naive gate is "did the average score stay above the bar?" — and it's dangerously incomplete. Averages hide things. A change can lift several cases while quietly destroying one, and the mean stays comfortably above threshold. You merge, the dashboard is green, and a real capability is gone — the one case you broke just happened to be outvoted by the ones you helped.

A good average, a hidden regression
BaselineScores [0.9, 0.9, 0.9] — mean 0.90.
Your changeScores [1.0, 0.4, 1.0] — mean still 0.80, clears the bar.
Average gate0.80 ≥ 0.70 → passes — but case 2 collapsed 0.9 → 0.4.
Regression gateCase 2 dropped vs baseline → blocks. Caught.

So a real eval gate has two conditions: the aggregate score must clear a threshold (catches broad quality drops), and no individual case may score meaningfully below its baseline (catches narrow, hidden regressions). The first guards the overall bar; the second guards every case you already got right. Only when both hold should the change merge. That two-part gate is the core idea, and the math makes it precise.

04

The gate math

Let the current build's per-case scores be ci and the baseline's be bi. The aggregate gate checks the mean against a threshold θ; the regression gate flags any case that dropped more than a tolerance τ:

passagg = ( mean(c) ≥ θ )     regressions = { i : ci < biτ }

The threshold guards the overall bar; the tolerance band τ keeps normal run-to-run noise from flagging a case falsely.

The pull request passes only if both hold — the mean clears the bar and no case regressed:

gate  =  passagg  AND  ( |regressions| = 0 )

A high mean alone isn't enough — one regressed case blocks the merge even if the average looks great. That's the property the average-only gate lacks. The runnable version below runs a suite, flags regressions, and shows the two-part gate blocking a hidden regression a mean-only gate would pass.

RUN IT YOURSELF

Gate a pull request

An eval gate is two checks. The aggregate check passes when the mean score clears a threshold — catching broad quality drops. The regression check flags any case that scored below baseline beyond a tolerance — catching a narrow drop the average would hide. The gate passes only if both hold, so a change that lifts two cases to 1.0 but tanks one from 0.9 to 0.4 still has a mean of 0.8 (above 0.7) yet is blocked, because that one case regressed. Change the scores, threshold, or tolerance and watch the gate decide.

CPython · WebAssembly
05

Wiring it up

Turning the gate into a CI check is a short pipeline:

StepWhat it does
TriggerRun on every PR that touches a prompt, model version, or retrieval config.
RunExecute the golden set through the build, capturing outputs (and traces).
ScoreScore each case with your metric (judge, similarity, task check) → a score vector.
GateMean ≥ threshold AND no per-case regression vs baseline → pass; else fail the check.
ReportPost per-case diffs on the PR so a reviewer sees exactly what moved.

A few practicalities. Keep the baseline in version control and update it deliberately when an intended change legitimately shifts scores — never auto-adopt the current run as the new baseline, or regressions ratchet in silently. Budget the eval run: it costs tokens, so a fast smaller golden set can gate every PR while a larger suite runs nightly. And post a readable diff — the gate says pass/fail, but a reviewer needs to see which case moved and by how much to decide if a failure is a real regression or an intended change. This is evaluation wearing a CI hat: the same scoring, wired into the merge.

06

Pitfalls

The one that bites first is a flaky gate. LLM scores fluctuate run to run, so a zero-tolerance per-case check flags noise as regression and the gate flaps — developers learn to ignore it, which is worse than no gate. Set a sensible tolerance band, reduce variance (fix seed/temperature where possible, average a few runs per case), and only then does the gate earn trust. The opposite failure is a golden set that's too small or stale: it passes everything because it doesn't cover the cases that break, giving false confidence. Grow it from real failures.

Two more. Trusting the judge blindly — if you score with an LLM judge, the judge has its own biases and errors, so validate it against human labels before you let it gate merges (an unvalidated judge is a random number generator with opinions). And gaming the metric: teams optimize prompts to score well on the golden set and drift from real quality, so refresh the set and watch production, not just the gate. Done right, evals in CI make LLM quality a first-class, automated concern — you stop hoping a change didn't break anything and start knowing, on every merge.

Worth knowing

A zero-tolerance per-case gate flaps on LLM noise and gets ignored — the worst outcome. Use a tolerance band and variance reduction so the gate only fires on real regressions, and keep the baseline in version control so you update it on purpose, not by accident.

Frequently asked

Quick answers

What are evals in CI?

Running a golden set of quality evaluations on every change and blocking the merge if quality drops — unit tests for LLM quality.

Why not exact-match tests?

LLM output is non-deterministic and open-ended, so you score each case with a metric (judge, similarity, task check) instead of asserting equality.

Why gate on per-case regression?

A healthy average can hide a broken case, so require both a mean threshold and no individual case scoring below baseline.

How to handle non-determinism?

Use a tolerance band on the regression check, reduce variance (seed/temperature, multiple runs), and update the baseline deliberately.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.