Evals

You change a prompt, try it once, the answer looks better, and you ship, then a week later users hit the cases it quietly made worse. An eval (short for evaluation) is the fix: a repeatable test of whether your AI feature works, a set of inputs paired with what a good output looks like, scored the same way every time so you can tell whether a change actually helped. A model is a probabilistic black box that answers the same question differently each run, so measuring is the only way to know, and that discipline is much of what separates a demo from a product.
Why eyeballing fails
Checking your AI's answers by hand feels fine at first. You run a prompt, read the reply, and it looks good, so you move on. The trouble is that three things quietly work against you, and you have met all of them already.
Here are the three:
- Output varies. The same prompt can give different answers each run, so one good result does not promise the next one will be good too.
- Changes cause regressions. A prompt edit that fixes one case often breaks another you were not looking at.
- It does not scale. You can read five answers. You cannot read five hundred every time you change a line.
None of this means you did anything wrong. It means eyeballing is the wrong tool for a job this size, and there is a calmer way to do it.
Build an eval set
An eval set is a list of test cases. Each case has an input and what a good output looks like for it. Start small. Ten to twenty cases that cover your common situations plus a few tricky edge cases are far more useful than none, and you can grow the set as you find new failures in the wild.
eval_set = [
{"input": "The food was cold and late.", "expected": "NEGATIVE"},
{"input": "Best meal of my life!", "expected": "POSITIVE"},
{"input": "It was fine, nothing special.", "expected": "NEUTRAL"},
# ...add real cases and edge cases as you discover them
]When a user reports a bad answer, add it to the set with the output it should have given. Your eval set then becomes a growing memory of every mistake you never want to make again, the same way a bug you fix becomes a test you keep.
How to grade
Different tasks need different grading, so the first step is matching the check to the kind of answer you expect.
- Exact match works when there is one right answer: a classification label, an extracted field, a yes or no. You compare the output to the expected value directly.
- Criteria-based grading works when there is no single right answer, like a summary or a reply. You check whether the output meets conditions: Does the summary mention the key fact? Is it under 50 words? Is the tone polite?
- LLM-as-judge uses a second model call to score the output against your criteria. It scales to fuzzy quality questions a simple check cannot capture, but the judge is itself a model, so it can be wrong, and you sanity-check it against human judgment now and then.
# exact-match grading over the eval set
def run_evals(eval_set, classify):
passed = 0
for test in eval_set:
output = classify(test["input"])
if output == test["expected"]:
passed += 1
else:
print(f'FAIL: "{test["input"]}" -> got {output}, wanted {test["expected"]}')
print(f"{passed}/{len(eval_set)} passed")Exact match is the friendliest place to start, and it fits the classification and extraction work from earlier chapters perfectly. The LLM-as-judge idea works because spotting whether an answer meets a standard is often an easier prediction than producing the answer was, the same way reviewing an essay is easier than writing one. But the judge is another predictor with the same flaws, so treat its scores as a useful estimate, give it a clear rubric, and check its grades against your own on a sample now and then.
Run them on every change
An eval set is only useful if you actually run it. Treat your prompts like code: before you ship a change to a prompt, a model, or your retrieval, run the evals and compare the score to what it was before. If the number went down, you caused a regression, even if your one hand-test happened to look fine.
This is what turns prompt engineering from guesswork into something you can iterate on with confidence. Change something, measure it, keep what scores higher, and discard what scores lower. It is the same loop that makes ordinary software reliable, now applied to the fuzzy parts of an AI feature.
If the score dropped, you caused a regression. One lucky hand-test does not overrule the set, so let the number be the tiebreaker every time.
In practice
The run_evals function from the last section is a complete, if small, eval harness. You point it at your classifier and your eval set, and it tells you how many cases pass and shows you exactly which ones fail:
run_evals(eval_set, classify_review)
# FAIL: "It was fine, nothing special." -> got POSITIVE, wanted NEUTRAL
# 2/3 passedThat one failing line is worth more than a dozen successful hand-tests, because it points at a real weakness you can now fix and re-measure. The final chapter, Safety and limits, covers the risks to design against before you put any of this in front of real users.
run_evals already earns its keep: it gives you a score and the exact cases that broke. Fix one, run it again, watch the number move. Next up, Safety and limits covers what to guard against before real users arrive. 
