green builds

Evals are unit tests for your prompts - wire them into CI

Treat evals like unit tests: fast, version-controlled, and blocking on regression. How to structure an eval suite and gate prompt changes in CI.

You wouldn’t merge a code change without a passing test. So why do prompt changes, which can break your product just as badly, ship on “looked good to me”? A prompt is code. It deserves the same gate. The mental model that makes this click: evals are unit tests for your prompts.

The analogy, precisely

A unit test pins down behavior so a change can’t silently break it. An eval does the same for a non-deterministic output, with two adjustments:

  • You assert a score against a threshold, not an exact string (the output varies).
  • You run each case a few times and look at the distribution, because the failure mode is “usually fine, occasionally terrible.”

Where the analogy holds: cases live in the repo, run in CI, and block merges. Where it breaks: the “judge” scoring the output is itself a model, so you have to validate it (more on that below).

Structure the suite like tests

Keep it boring and version-controlled next to the code it guards:

evals/
  golden/
    support_replies.jsonl   # inputs + reference of a good answer
  rubrics/
    support_tone_v3.md      # what "good" means, written down
  baselines/
    main.json               # committed scores to compare against

A case carries both deterministic rules and the fuzzy reference:

{
  "id": "refund-001",
  "input": "I was charged twice",
  "reference": "Acknowledge the double charge; promise a refund in 5-10 days; no exact amount.",
  "must_not_contain": "guaranteed"
}

Two layers, same as good tests

Cheap exact checks first, scored quality second:

def test_reply(case):
    out = run_feature(case["input"])
    # Deterministic: fast, exact, no model needed.
    assert case["must_not_contain"] not in out.lower()
    # Scored: rubric + judge for the fuzzy part.
    result = score(rubric="support_tone_v3", output=out, reference=case["reference"])
    assert result.score >= 0.8, result.explanation

The deterministic layer catches the hard failures (leaks, bad format, missing refusal) for free. The scored layer handles “is this actually good.”

Baseline comparison: did it improve, or just change?

This is the part that turns evals into a real gate. Every prompt change is measured against the committed baseline, so an “improvement” that quietly regresses other cases gets caught:

$ eval run --baseline main
  prompt v36 (main) ...... 0.86 avg
  prompt v37 (this PR) ... 0.83 avg
FAIL  quality regressed 0.86 -> 0.83 on 4 cases

You update the baseline deliberately, in a reviewed commit, never silently. “It looked better to me” becomes a falsifiable claim.

The blocking gate

Same shape as any CI test job. A regression exits non-zero and the PR can’t merge:

# .github/workflows/evals.yml
on: [pull_request]
jobs:
  evals:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: python eval_runner.py --threshold 0.8
        env: { MODEL_API_KEY: '${{ secrets.MODEL_API_KEY }}' }

Two practical notes: cache or sample to keep CI cost sane (you don’t need all 500 cases on every PR, run a core set on PRs, the full set nightly), and pin the judge model version so the bar doesn’t drift under you.

Anti-patterns to avoid

  • Judge drift. If the scoring model changes, your scores move for no real reason. Validate the judge against human labels on a sample, and pin its version.
  • Threshold gaming. A 0.8 that everything passes isn’t a gate. Set it where it actually catches your known-bad cases.
  • Snapshot brittleness. Don’t assert exact outputs. Score against intent.

Evals are the single highest-leverage habit for an AI team: they turn prompt changes from a gamble into a measured, reviewable step. The full method, golden sets, judges, injection and cost gates, is on the LLM testing page, and installing all of it into your CI is exactly what a QA Foundation Sprint delivers.

One bad regression away from a lost week.

Book a 20-minute intro call. I’ll tell you honestly whether I can help and what the right next step is: audit, sprint, or nothing yet.