Regression tests for prompts and models
A small prompt edit or a model upgrade can quietly break things that used to work. Run your eval suite automatically on every change, with clear thresholds.
In 60 seconds
Regression tests for prompts and models
A small prompt edit or a model upgrade can quietly break things that used to work. Run your eval suite automatically on every change, with clear thresholds.
What goes in the eval suite
| Type of check | Example | Scoring |
|---|---|---|
| Deterministic | Output is valid JSON with the required fields; no phone numbers in the reply | Pass or fail, exact rules |
| Reference-based | Answer contains the correct deadline from the reference | Match against must_include / must_not lists |
| Judge-based | Answer is clear and complete by the rubric | Calibrated LLM judge (Lesson 5) |
| Safety | Red-team prompts are refused; normal requests are not | Rates on two sets (Lesson 9) |
| Cost and latency | Average tokens and time per case | Must stay within budget |
Thresholds that make sense
- Hard gates: must-pass items fail the build at once, for example a data leak or broken output format.
- Floors: each important score must stay above a minimum, per slice (for example, per language).
- No big drops: compare with the last accepted run; a drop larger than normal run-to-run noise needs a human look.
import json
import sys
def call_model(prompt: str) -> str:
... # plug in your provider
def check(case, output):
text = (output or "").lower()
ok_in = all(p.lower() in text for p in case.get("must_include", []))
ok_out = not any(p.lower() in text for p in case.get("must_not", []))
return ok_in and ok_out
def run(cases_path, baseline_path, max_drop=0.03):
with open(cases_path, encoding="utf-8") as f:
cases = [json.loads(line) for line in f if line.strip()]
results = {c["id"]: check(c, call_model(c["input"])) for c in cases}
score = sum(results.values()) / len(results)
must_fail = [c["id"] for c in cases if c.get("must_pass") and not results[c["id"]]]
with open(baseline_path, encoding="utf-8") as f:
baseline = json.load(f)["score"]
print(f"score {score:.1%} (baseline {baseline:.1%}), must-pass failures: {must_fail}")
if must_fail or score < baseline - max_drop:
sys.exit(1) # non-zero exit fails the CI job
if __name__ == "__main__":
run("evals/cases.jsonl", "evals/baseline.json")Model upgrades
- 1
Pin the version
Use a fixed model version where your provider offers one, so the model does not change under you. Check your provider's docs for how versions and retirement dates work. - 2
Run the full suite on the new model
Same cases, same judge, same thresholds. - 3
Compare slice by slice
Look for drops in any language, task type or safety set, not only the average. - 4
Read the changed answers
Scores miss things. Read a sample of cases where the outputs changed. - 5
Switch with a way back
Roll out gradually, watch live metrics, and keep the old version ready.
Watch and read more
Lab
A working eval runner with a baseline and a threshold, run before and after a prompt change.
The problem
- Save the starter script. It includes a fake model so it runs without any API key.
- Run it and note the score. Save it as your baseline.
- "Change the prompt" by editing the fake model's answers so that one must-pass case breaks. Run again and confirm the script exits with an error.
- Restore that case, but make two ordinary cases fail. Does the script fail with
max_drop=0.03? What aboutmax_drop=0.25? - Write three sentences on how you would choose
max_dropfor a real system.
Practice lab
Edit the code and run it. Python runs in your browser — nothing is sent to our server.
No output. Add print(...) to see values.
You are done when
Hard questions
Try to answer before you reveal. If you can answer these, you understood the lesson.
Q1With 5 cases, each case is 20 points. What does that tell you about thresholds on small suites?Reveal
On a tiny suite, any single failure is a huge score change, so a percentage threshold is really a "number of failures allowed" rule. That is fine for must-pass checks, but for quality scores you need many more cases before small percentage thresholds mean anything. Grow the suite, and use hard gates for the few cases that must never fail.
Q2A provider retires the model version you pinned. What is your plan?Reveal
Treat it like any upgrade, but with a deadline. Read the provider's notice, run your full suite on the candidate replacement early, compare slice by slice, fix prompts where needed and re-run, then roll out gradually with the old version still available until retirement. Keep the eval results as a record of why you chose the replacement.
Questions people ask
Running the full suite on every change is slow and costs money. What can I do?
Split it. Run a small, fast set of deterministic and must-pass cases on every change, and the full suite (with the LLM judge) before release or nightly. Cache results for cases whose inputs, prompt and model did not change.
What should I do when a case fails but the new answer is actually fine?
Read it carefully. If the new answer is genuinely correct, the test was too strict: update the reference or the check, and record why. Do not quietly delete failing cases to make the build green.
How is this different from security testing in the pipeline?
It is the same practice with a wider scope. Security tests check that attacks keep failing; regression evals check that quality, format, cost and safety all hold. They usually run in the same pipeline.
Do I need a special tool for this?
No. A script, a JSONL file of cases and your normal CI system are enough to start. Open-source eval frameworks can add dashboards and helpers later. Choose based on your needs, and check each tool's current documentation.
Lesson test
5 questions. Get 3 right (60%) to pass and complete this lesson.