Articles › AI

LLM evals with a small test set: 30-100 cases, run on every prompt change

Build an LLM eval set from real inputs, grade with code or an LLM judge, rerun on every prompt or model change, and work out how many cases you need to catch a 10% drop.

Intermediate 8 min read October 22, 2026

What you will learn
  • An eval set is the unit test suite of an AI feature: 30-100 real inputs with expected results written down first, rerun on every prompt or model change
  • Grade with code wherever you can; use an LLM judge only for what rules cannot check, and compare it with human labels before trusting it
  • Diff case by case, not just the total: 85% to 80% can hide 17 broken cases
  • 100 cases can separate a 10-point drop only when you compare case by case; comparing totals as independent samples needs about 250

A feature that calls an LLM can change behaviour every time you edit the prompt, switch models, or the provider ships a new version. Without a test set, you find out it broke when a user tells you. A small eval set of 30-100 cases that runs in minutes does the job a unit test suite does: it tells you what this change improved and what it broke.

Tested on Python 3.12 (the script uses only the standard library). Every statistic in the article was recomputed for it; the grading advice was checked against the Anthropic and OpenAI docs and the MT-Bench paper (Zheng et al., 2023).

What an eval is, and why poking at it is not one

Typing three or four questions and deciding it “looks fine” is not testing: next time you will ask different questions and you won’t remember the old answers. An eval has three parts, all written to files:

  1. Cases: real inputs the system must handle, each with a permanent id
  2. Expected results, or the grading criterion for each case, written before the run
  3. A grader: code that returns pass/fail per case, summed into a pass rate

As files, any run can be repeated, diffed against the previous one, and kept in git next to the prompt.

Where cases come from

Anthropic’s docs put it plainly: cases should mirror the real task, edge cases included (typos, sarcasm, long rambling input). OpenAI recommends logging from the start of development so you can mine good cases. The mix we use:

KindRough shareExample (ticket triage)
Normal cases from real logs60%“My order hasn’t arrived, it’s been 5 days”
Edge cases25%mixed Thai and English, typos, two requests in one message
Cases that actually broke15% and upevery wrong answer a user hit becomes a permanent case

The last row matters most. When a user hits a wrong answer, add that case before you touch the prompt, the same way you write a regression test before fixing a bug. Strip personal data before saving it.

Collect real inputsthe first 30-100 casesChange prompt or modelone thing at a timeRun every casecode checks first, judge laterDiff case by casewhat broke, what got fixedFailures become casesadded to the set for goodthen back to the top
Every bug a user hits becomes a permanent test case

How to grade: code first, judge later

Anthropic’s docs rank grading methods and say to pick the fastest, most reliable, most scalable one that works:

MethodGood forWeakness
Code (exact match, regex, JSON parse, numeric checks)categories, extracted fields, output formatcannot grade free text
LLM-as-judge with a rubricsummaries, tone, “does it answer the question”biased; must be checked against humans
Human reviewseed labels, auditing the judgeslow, expensive

The trick is designing the output so code can grade it. If the feature returns JSON with clear fields (the structured output article), half your eval becomes a plain ==. The same docs add that many automatically graded cases beat a few carefully hand-graded ones.

LLM-as-judge and its biases

When you need a judge, follow the three tips in Anthropic’s docs: a detailed rubric (“must mention the order number, otherwise fail”), a measurable answer (pass/fail or 1-5), and reasoning before the score (thinking on). And use a different model from the one that wrote the answer.

The MT-Bench study (Zheng et al., 2023) measured three judge biases. It used 2023-era models; today’s numbers differ, but the kinds of bias still apply:

  • Position bias: favouring an answer because of where it sits. Even GPT-4 gave the same verdict after the two answers were swapped in only 65% of cases
  • Verbosity bias: favouring longer answers, even when the extra length is repetition
  • Self-enhancement: possibly rating its own answers higher (the authors say their data was too limited to be sure)

Countermeasures: when comparing two answers, judge twice with the order swapped and count a win only if it wins both times. When you don’t need a comparison, grade one answer pass/fail instead. Spot-check 20-30 cases by hand against the judge before trusting its numbers. OpenAI’s guide says the same: scale up a judge only once it agrees consistently with human labels.

The grader needs testing too. A judge never compared with humans produces numbers that look precise and mean nothing.

The smallest eval script that is actually useful

Keep cases in cases.jsonl, one per line, e.g. {"id": "c001", "input": "...", "expect": "refund"}. This script is standard library only; predict is the function that calls your AI feature.

import json, sys
from math import comb

def run(cases_path, predict, out_path):
    results = {}
    for line in open(cases_path, encoding="utf-8"):
        c = json.loads(line)
        got = predict(c["input"])
        results[c["id"]] = got.strip().lower() == c["expect"].lower()
    json.dump(results, open(out_path, "w"))
    print(f"pass {sum(results.values())}/{len(results)}")

def compare(old_path, new_path):
    old, new = json.load(open(old_path)), json.load(open(new_path))
    broke = [k for k in old if old[k] and not new[k]]
    fixed = [k for k in old if not old[k] and new[k]]
    n, k = len(broke) + len(fixed), min(len(broke), len(fixed))
    p = min(1.0, 2 * sum(comb(n, i) for i in range(k + 1)) / 2**n) if n else 1.0
    print(f"broke {len(broke)} {broke}\nfixed {len(fixed)} {fixed}\np = {p:.3f}")

if __name__ == "__main__":
    compare(sys.argv[1], sys.argv[2])
# diff the previous run against this one (dev machine or CI)
python evalrun.py results/old.json results/new.json

compare does not stop at the total. It lists broken cases (passed before, fail now) and fixed cases (failed before, pass now), then computes an exact McNemar p-value, which is a binomial test on the cases that changed (the same as exact=True in statsmodels). On simulated data, the old run passed 85/100 and the new one 80/100. The totals suggest a small dip; underneath, 17 cases broke and 12 were fixed, p = 0.458. The change reshuffled the whole set without improving it, and those 17 cases need reading.

Worked example: how many cases to see a 10-point drop?

Say the feature passes 85%, and a prompt edit drops it to 75%. How big must the set be to tell that apart from chance?

Step 1: the uncertainty of a single run. Standard error = √(p(1−p)/n); at p = 0.85 that is √(0.1275/n). The 95% interval is about ±1.96 × SE.

CasesSE95% interval
306.5 pts±12.8 pts
505.0 pts±9.9 pts
1003.6 pts±7.0 pts
2502.3 pts±4.4 pts

At 30 cases the noise band is wider than the drop you want to catch, and one case is worth 3.3 points: a 10-point drop is just 3 cases.

Step 2: comparing totals as two independent samples. The two-proportion sample-size formula at α = 0.05 (two-sided, z = 1.96) and 80% power (z = 0.84):

n = (1.96 + 0.84)² × (0.85×0.15 + 0.75×0.25) / 0.10² = 7.84 × 0.315 / 0.01 ≈ 247 cases

Step 3: comparing case by case on the same set. This works because both runs use the same cases, so only the cases that changed carry information. At 100 cases, a 10-point drop can happen several ways:

BrokeFixedNetp (exact McNemar)
100−100.002
122−100.013
155−100.041

All are below 0.05, so 100 cases compared case by case catch a 10-point drop. At 30 cases, a 10-point drop is 3 broken and 0 fixed: p = 0.25, still not conclusive.

In practice: 30 cases catch outright breakage and obvious bugs; around 100 with a per-case diff catch a 10-point change; anything subtler needs more cases or repeated runs.

The model’s own noise

LLMs don’t answer the same way every time. Before trusting any result, run the baseline twice with no changes and use compare to see how many cases flip on their own. If 5 cases flip per run, a change that breaks 5 cases tells you nothing. Run frequent flippers 3 times and count a pass by majority, or loosen the grader where it is fussy about details that don’t matter.

When to run it and how to track it

Run on every prompt edit, model switch, temperature or tool change, SDK upgrade (choosing a model should be decided by this set). Save each run’s results under the commit hash and watch the pass rate over time.

It costs less than people expect. 100 cases at 1,000 input and 200 output tokens each on Claude Sonnet 5.5 ($2 / $10 per million tokens, as of October 2026): 100 × (1,000 × $2 + 200 × $10) / 1,000,000 = $0.40 a run. A judge roughly doubles that. Far cheaper than a bug users find.

Common mistakes

  • Tuning the prompt until it memorises the set (overfitting). Patching case by case to 100% while production gets worse. Keep 20-30% as a holdout you don’t look at while tuning; run it only when deciding
  • A stale set. Real inputs moved on, the cases are three months old. Top up from logs weekly
  • Watching only the total. As above, 85 → 80 hid 17 broken cases
  • Trusting an unchecked judge, or letting a model grade itself
  • Personal data in cases. Strip names, phone numbers, account numbers before saving; eval sets live in git longer than you think
  • No attack cases. If the feature reads outside text, include prompt injection cases (defending at the system level)

A good eval does not answer “how good is it?” It answers “which cases did this change break?”

Sources

FAQ

How many cases is enough?

Thirty cases already pay off, because they catch obvious breakage. To separate a 10-point change from noise, grow to about 100 and compare each case with the previous run. Comparing only the overall percentage as two independent samples needs around 250 cases.

Can the same model be the judge?

It can, but it shouldn't. Anthropic's docs recommend grading with a different model, and the MT-Bench study found judges biased by answer position, by length, and possibly toward their own answers. Use a clear rubric, ask for pass/fail, and have a person spot-check a sample against the judge.

Our eval score is high. Why does production still break?

Usually the eval set no longer looks like real input, or the prompt has been tuned until it memorised the set (overfitting). Add cases from user-reported failures every week and keep a holdout slice you never look at while tuning.

Commands and settings were tested on sample systems. Try them on a test machine before production.