Eval-driven development
Evals in CI, with a train and test split.
A way of working rather than a tool. The eval suite runs automatically on every change, the way unit tests do, and the number it produces is what decides whether the change ships. Prompts, retrieval settings and model choices are then argued about with a score rather than with impressions.
Evals that have to be run by hand get run once, at the start, and are quietly abandoned the first week a deadline arrives. The second failure is subtler: tune long enough against the same sixty examples and the score climbs while the system does not improve, because you have fitted the examples. That is why the set is split, one part to tune against and one held back to report on.
It is the loop around the other two: evals supplies the cases and the pass rule, LLM-as-judge often does the marking, and this is the discipline of firing them on every commit and treating the result as a gate. It pairs with observability, which is where the real production failures worth adding to the set come from.
An eval that has to be remembered is an eval that stops being run.
In more detailOnce evals run automatically on every change, prompt and architecture work becomes measurable rather than anecdotal. The discipline borrowed from ML is a held-out split: tune against one set, report on another, or you will overfit your prompts to your eval set exactly as a model overfits training data.
Learn first
Improving against a number
By the end: You can improve a system methodically instead of by impression.
- The loop: baseline, change, measure, keep or revert
- One change at a time
- Train and test splits
- Knowing when to stop
Improving a system against a number is the only way to know you are improving it, and it has a specific failure that catches careful people: run enough experiments against the same evaluation set and the score rises whether or not anything got better. This section is the loop — baseline, one change, measure, keep or revert — and the discipline that stops it deceiving you, with the deception measured so you can see how large it is.
The loop#
- 1.Measure a baseline and write the number down before changing anything.
- 2.Make exactly one change.
- 3.Re-run the same evaluation on the same examples.
- 4.Keep the change if it helped by more than the noise, revert it otherwise.
- 5.Record what you tried and what happened, including the reverts.
Step five is the one that feels like overhead and is not. The record of failed changes is what stops the team trying them again in three months, and it is also the honest history of where the gains came from — which someone will ask for the moment the system is questioned.
One change at a time#
Changing the prompt and the retrieval settings together and seeing the score rise tells you the pair helped. It does not tell you that both did, and it is entirely possible that one helped by more than the pair and the other hurt. That is not a hypothetical — the experiment-design topic measures exactly this case, where two changes together look like a modest gain and separating them shows one was worth twice as much alone and the other was actively harmful.
The temptation to batch changes comes from each evaluation run costing money and minutes. The answer is to make the evaluation cheap enough to run often, not to batch the changes: a smaller sample for the inner loop and the full set at the end is a reasonable compromise, and batching is not.
What repeated measurement does to your eval set#
Here is the failure that gives this section its point. Twenty changes are tried against a hundred-example evaluation set, and every one of them is worthless — the underlying quality never moves. Each time a change scores better than the current best, it is kept, which is exactly what the loop above says to do.
import numpy as np
rng = np.random.default_rng(0)
TRUE = 0.70 # the real quality. Nothing below changes it.
n_dev, n_held = 100, 2000
def evaluate(n):
"""Score on n examples. The change does nothing; only sampling noise moves."""
return (rng.random(n) < TRUE).mean()
dev_best, kept = evaluate(n_dev), 0
for attempt in range(20):
dev = evaluate(n_dev)
if dev > dev_best: # keep whatever helps, revert the rest
dev_best, kept = dev, kept + 1
print("dev score after 20 changes:", round(dev_best, 3), f"({kept} kept)")
print("held-out score :", round(evaluate(n_held), 3))
print("true quality :", TRUE)
# dev score after 20 changes: 0.82 (5 kept)
# held-out score : 0.705
# true quality : 0.7This is not an argument against measuring. It is an argument about how large the effect is, which depends on two things you control: how many examples the set has, and how many times you look at it.
def illusion(n_dev, attempts, trials=400, true=0.70):
"""Average apparent gain from keeping whatever helps, when nothing helps."""
rng = np.random.default_rng(0)
gains = []
for _ in range(trials):
best = (rng.random(n_dev) < true).mean()
for _ in range(attempts):
s = (rng.random(n_dev) < true).mean()
if s > best:
best = s
gains.append(best - true)
return float(np.mean(gains))
for n in (50, 100, 200, 500, 1000):
print(n, round(illusion(n, 20) * 100, 1))
# 50 +11.7 points of pure noise
# 100 +8.4
# 200 +6.0
# 500 +4.0
# 1000 +2.8
for attempts in (5, 20, 50, 200): # with a 200-example set
print(attempts, round(illusion(200, attempts) * 100, 1))
# 5 +4.1
# 20 +6.0
# 50 +7.1
# 200 +8.6Train and test splits#
The defence is the one every other part of machine learning already uses, applied to prompts and pipelines rather than to weights. Split your examples in two. The development set is the one you iterate against, look at freely and let yourself overfit — that is what it is for. The held-out set is touched only to answer the question that matters: is the system actually better than it was.
development set look at it constantly expect it to be optimistic
held-out set touch it rarely the number you report
production traces growing continuously where new examples come fromThe held-out set expires
After enough decisions the held-out set has been optimised against too, just more slowly, and the fix is to replace it rather than to trust it harder. Take fresh examples from production traffic and retire the old set, which also fixes a second problem: your traffic has drifted since you built it, and a set that no longer looks like your users is measuring a system nobody is running.
Knowing when to stop#
Hill climbing ends when the gains stop being real, and the signals are recognisable. Changes have started producing differences smaller than the run-to-run noise. The wins are arriving on the development set and not surviving on the held-out set. The remaining failures are all different from each other, which means there is no common cause left to fix. And the cheapest remaining change costs more than the problem it solves.
At that point the useful move is not another change to the same thing, but a different question — a harder set of examples, a different failure category, or an honest look at whether the metric still reflects what the product needs. Continuing to hill climb past this point is where the score keeps rising and the users stop noticing, which is the specific way that eval-driven development goes wrong.
Where people go wrong
Changing several things at once because it is faster. It is faster right up to the point where the score drops and you have no idea which change caused it.
Build this: Take a system with an eval and make five changes one at a time, recording score and cost after each. Revert the ones that did not help, and check whether your train and test numbers moved together.
Open this section on its own pageA week of improving one feature, worked through
By the end: You can run an improvement cycle end to end and recognise the decisions that make it work.
- Starting from traces
- Choosing the first fix by count
- Keeping and reverting changes
- Watching for overfitting
- Writing it up
Everything in this topic has been described one piece at a time: traces, failure categories, evals, held-out splits, one change at a time. This section runs them together on a single feature across five days, with the numbers at each step, including the two changes that were measured and thrown away. It is here because the order of the work is the part that is hard to convey in the abstract, and the order is what makes it work.
The setup#
A support assistant answers questions about a software product from its help centre. The team says it 'feels unreliable'. Here is a realistic week, condensed.
Monday: read before measuring#
Read 120 recent traces and write a note on the first failure in each. Group the notes.
Answered from outdated article 17
Follow-up question lost its context 12
Invented a setting that does not exist 8
Too long, buried the answer 6
Other 5
--
48 failing of 120 (40%)Tuesday: build the eval around those modes#
- 80 cases from real traffic, including all 48 failures, split into 56 to tune on and 24 held out.
- Code check for outdated sources: fail if any cited article is marked archived.
- Pass or fail judge for invented settings, validated on 40 labels: catches 17 of 19 failures, accepts 20 of 21 passes.
- Baseline: 22 of 56 on the tune set and 10 of 24 held out. That is 32 of 80, which matches the 48 known failures in the set.
Wednesday and Thursday: one change at a time#
Change Tune set Kept?
Exclude archived articles from the index 36/56 yes
Rewrite follow-ups into standalone questions 42/56 yes
Add 'only mention settings from the sources' 44/56 yes
Few-shot examples of short answers 45/56 no, +40% cost for +1 case
Switch to a larger model 46/56 no, 3x costThe first two changes were not prompt changes at all. The biggest wins in a system usually come from data and retrieval, which is why reading traces first matters.
Friday: check the held-out set once#
Held-out result: 17 of 24 pass, up from 10. The tune set went from 22 to 44 of 56 over the same changes. Both moved a lot, which suggests the changes generalise. If the tune set had jumped and the held-out set had barely moved, that would point to overfitting the tuning cases.
Is 10 to 17 of 24 real?
On 24 cases, check the disagreements. If seven cases went from fail to pass and none the other way, that is solid evidence even on a small set. If it was nine wins and two losses, the case is weaker, and growing the set is the next job.
Writing it up#
What we found: 40% of traces failed; top causes were outdated articles
and lost follow-up context.
What we changed: excluded archived articles, added standalone rewriting,
constrained settings to sources.
Result: held-out pass rate 10/24 -> 17/24. Cost per answer +8%, p95 +300 ms.
Not done: answer length (tried few-shot, not worth the cost).
Next: the eval set is small; add 100 cases from next week's traffic.Where people go wrong
Making five changes in one go because each seemed obviously good. When the score moves, nobody knows why, and when it drops, nobody knows what to revert.
Build this: Run this week on your own project: traces, failure table, eval with a held-out split, at least four single changes with a keep or revert decision each, and a write-up in the format above.
Open this section on its own pageWhere you use this
A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.
- Stage 6, The runner, the split and the report: Using the report as the thing you iterate against, one change at a time.
Now actually do it
Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.
1Hill climb with a held-out split
6hFive changes one at a time, scoring on train and test after each. Keep what moved the number. Watch for train improving while test does not.
Done when: You can point to the change where you started overfitting to your tuning set.
6 hours of practice
Interview questions
4 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.
Improving against a number
How do you improve an AI system methodically?
Score a baseline, make one change, score again, keep or revert, repeat. Hold out a test split you never inspect during iteration and report that number, and track cost alongside quality since most improvements are trades.
Why hold out a test split when you are only tuning prompts?
Because prompt tuning overfits exactly as model training does. Iterating against every case drives the score up while real performance stays flat. The held-out number is the one that still predicts reality.
How do you know when to stop improving?
When several consecutive changes produce nothing, when the remaining failures are cases humans also disagree on, when further improvement costs more than it is worth, or when the test score has stopped moving while the train score keeps rising.
A week of improving one feature, worked through
Tell me how you would improve an AI feature that 'feels unreliable'.
Read a hundred or so traces and categorise the failures, count them, and build an eval around the top modes with a held-out split. Then make one change at a time, starting with the most common failure, keep what improves the tune set at acceptable cost, and check the held-out set at the end.
Then they push further
Your first idea is to switch to a bigger model. Why might that be wrong?
If the failures come from outdated documents or lost context, a bigger model reads the same bad inputs. It also multiplies cost. Fix the causes the traces show first, then test a bigger model as one change among others.
How do you know you did not overfit to your eval?
Keep a held-out split you look at rarely, and check that it moves with the tuning set. Then watch production failure rates after release, since real traffic is the final held-out set.
What gets marked up. Strong answers start from data, change one thing at a time, weigh cost, and use a held-out set. This question often stands in for a behavioural round, so a real example with numbers scores highest.
Resources
Free to watch and read, picked for this topic.
Videos
For improving against a number
For a week of improving one feature, worked through
Full courses: the whole topic, start to finish
Stop 32 of 50 on the AI Engineer route
0 of 50 topics done · 0h behind you, 603h to go
What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.