The Test Suite for Behaviour You Cannot Unit Test
An offline evaluation runs a fixed set of inputs through your system before release and scores the outputs. It is the nearest thing an LLM feature has to a regression suite: the check that tells you whether today's prompt edit, model upgrade or retrieval change made things better or worse, before users find out. Online evaluation watches real traffic after release. The two complement each other, but only the offline suite can stop a bad change from shipping.
- You fixed one bad answer by editing the prompt and broke three others you did not check
- A provider released a new model and you cannot say whether it is safe to switch
- Your evaluation score moves by a few points between identical runs
- The test set is the same twenty examples someone wrote in the first week
This article covers building the dataset, choosing graders, handling noise, and making the suite a release gate.
Build the Dataset From Real Use
The dataset decides what the evaluation can see. Examples invented at a desk test what you imagined users would ask. Examples taken from production test what they do ask.
| Source | What it gives you | Share of the set |
|---|---|---|
| Sampled production requests | The real distribution of topics, phrasing and length | About half |
| Past failures and bug reports | Regression cases, so that a fixed bug stays fixed | A growing share |
| Hand-written edge cases | Rare but important situations: empty input, conflicting instructions, out-of-scope requests | A fifth or so |
| Adversarial inputs | Injection attempts and policy boundary cases | A small fixed block |
Start with fifty to a hundred examples. That is enough to see large regressions, and small enough that you will read every failure. Add a case each time production shows you a new kind of mistake. Tag each example with a category, because the breakdown by category is where the useful signal is.
Use the Cheapest Grader That Works
- Code checks. Exact match, a required field, valid JSON, a number within a range, a forbidden phrase. These are fast, free and repeatable. Use them wherever the criterion can be written as code.
- Model graders. For qualities that code cannot check, such as whether an answer is supported by the source or whether the tone is appropriate. Give the grader a specific rubric and ask for a short reason before the verdict.
- Human review. For calibrating the model grader and for cases where the criteria are still being worked out. Review a sample regularly, not everything.
A model grader is a classifier you have built, and it needs its own test. Label a few dozen outputs yourself, run the grader over them, and measure how often it agrees. If agreement is poor, improve the rubric before you trust the scores.
A Minimal Harness
import json
from collections import defaultdict
def run_suite(system, cases, graders):
results = defaultdict(list)
for case in cases:
output = system(case["input"])
passed = all(grade(case, output) for grade in graders[case["category"]])
results[case["category"]].append(passed)
return {cat: sum(r) / len(r) for cat, r in results.items()}
cases = [json.loads(line) for line in open("evals/support.jsonl")]
scores = run_suite(candidate_system, cases, GRADERS)
baseline = json.load(open("evals/baseline.json"))
regressions = {
cat: (baseline[cat], score)
for cat, score in scores.items()
if score < baseline[cat] - 0.03 # tolerance for run-to-run noise
}
if regressions:
raise SystemExit(f"Regression in: {regressions}")Noise: Know How Much a Difference Means
LLM outputs vary between runs, and current reasoning models cannot be pinned with a temperature of zero. Model graders vary too. A two-point change on a hundred examples may be nothing. Before you rely on the suite, measure its noise.
- Run the unchanged system several times. The spread between those runs is your noise floor. A difference smaller than that is not a result.
- Compare case by case. Looking at which examples flipped from pass to fail tells you more than two averages do.
- Repeat unstable cases. Run each example three or five times and score the pass rate.
- Fix what you can. Pin the model version, the grader model and the reasoning effort, and record them with every run.
Make It a Gate
An evaluation that is run by hand is run when someone remembers. Put it in the pipeline: run the suite on every change to a prompt, a model setting or the retrieval code, compare each category with the stored baseline, and block the merge when a category falls by more than the noise tolerance. Update the baseline only as a deliberate, reviewed step.
Cost is a practical limit. Run a small fast subset on every pull request and the full suite before release and on a schedule. Use batch pricing and prompt caching for the full run, since it does not need an immediate answer.
1. Tuning on the test set
If you adjust the prompt until every case passes, the suite now measures how well you fitted those cases. Keep a held-out set that you look at rarely and never tune against.
2. Letting the dataset go stale
A suite that never changes drifts away from what users do. Refresh it from production samples on a schedule, and retire cases that no longer reflect the product.
3. Reporting a single number
An overall pass rate of 92% can hide a category that fell from 95% to 70%. Track every category and alert on each one.
4. Skipping the evaluation for a model upgrade
A newer model is not automatically better on your task. Run the full suite before switching, and expect to adjust the prompt.
Key Takeaways
- Offline evaluation is the release gate. Online evaluation tells you what happened afterwards.
- Build the dataset from production and add every new kind of failure to it.
- Prefer code checks, then model graders, then people. Validate model graders against human labels.
- Measure the noise floor before interpreting any difference.
- Automate it with per-category thresholds and a baseline that changes only on purpose.

