Evals
Measure output quality with test sets instead of vibes.
Create a free account Try part of it here
Why it matters
Every change to an AI feature can quietly fix three cases and break five. That includes a reworded prompt, a new model or a cheaper one. An eval catches this. It's a fixed set of test inputs with known good answers, plus a script that scores your system on them. Think of it as an A/B test for prompts. Instead of "this feels better", you get "accuracy went from 0.80 to 0.90, and nothing got worse". Teams that ship reliable AI features all have evals. After prompting, it's the habit that matters most.
What you'll be able to do
- Explain what an eval is and why trying a few prompts by hand isn't enough
- Build a labeled dataset and choose a grader for it
- Compute accuracy, per-label recall and a majority baseline
- Use an eval to compare two prompts or two models before shipping a change
What it covers
- From gut feel to numbers
- Where datasets come from
- A first eval: review sentiment
- Accuracy hides things
- Worked example: grading a model
- Using evals to make decisions
- Don't tune until everything passes
From gut feel to numbers
Here's how most people test a prompt. They try it on three examples, like the answers and ship it. The problems show up later. There's a fourth kind of input nobody tried. Or a "small" prompt edit broke a case that used to work.
Think of a teacher who only ever checks three homework questions. The class looks great until the exam.
An eval fixes this. It has three parts:
- A dataset. Inputs, each paired with what a good output looks like. For a classifier, that's the correct label. For a support bot, it might be facts the answer must contain.
- The system under test. Your prompt, model and code, run on every input.
- A grader. Something that scores each output, plus one summary number across all of them.
You run the whole thing every time you change something. It's like comparing two ad versions on the same audience.
Choosing a grader
| Grader | Good for | Example |
|---|---|---|
| Exact match | Labels, categories, ids | predicted == "negative" |
| Code checks | Format and rules | valid JSON, under 60 characters, contains a link, no banned words |
| Contains / regex | Required facts | answer mentions "60 days" |
| LLM as judge | Open-ended quality | "Is this reply polite, and does it answer the question?" (next lesson) |
| Human review | Final say, building the labels | a weekly sample of 50 replies |
Use the cheapest grader that measures what you care about. Code is free and instant, and it never changes its mind. Use a model or a person only where code can't decide.
Use the cheapest grader that measures what you care about.
Where datasets come from
- Real inputs. Customer emails, search queries, past campaign briefs. These are best, because they include the messy cases.
- Hand-written edge cases. The sarcastic review, the email in two languages, the empty input.
- Every bug you fix. When something goes wrong in real use, add that input to the dataset. Then it can't quietly come back.
Start with 20 to 50 examples. That's enough to catch big drops, and you'll grow it over time.
Show the answer
A code check: len(subject) < 60
A length rule is something code can decide instantly and for free. Save model and human judgment for things code can't measure, like tone.
A first eval: review sentiment
The practice API has 20 product reviews. A person has labeled each one positive, negative or mixed. That's a ready-made dataset:
import requests reviews = requests.get("https://api.gradient.test/v1/reviews").json()["data"] labels = [r["label"] for r in reviews] print(len(reviews), "reviews") for label in ["positive", "negative", "mixed"]: print(label, labels.count(label)) print(reviews[3]["text"], "->", reviews[3]["label"])
Before you test any model, work out the baseline. That's the score of a classifier that always gives the most common label. Here the most common label is "positive". It's right 9 times out of 20, so the baseline is 9 ÷ 20 = 0.45.
Why bother? Say a prompt scores 0.50. That sounds fine, until you notice it barely beats one line of code that never even reads the review.
predicted = ["pos", "neg", "pos", "pos"] actual = ["pos", "neg", "neg", "pos"] right = sum(p == a for p, a in zip(predicted, actual)) print(right / len(actual))
Show the answer
0.75
p == a gives True or False for each pair, and sum counts each True as 1. Three of the four match, so accuracy is 3 / 4 = 0.75.
Accuracy hides things
Accuracy is the share of answers that are correct. It's a good headline number, but it hides where the mistakes are.
Suppose a classifier only ever says positive or negative. It never says mixed. Its accuracy can still look respectable while it gets every mixed review wrong.
Recall by label shows this straight away. For each label, it's the share of that label's examples the classifier got right:
labels = ["positive", "positive", "negative", "mixed", "mixed", "negative"] predictions = ["positive", "positive", "negative", "positive", "negative", "negative"] correct = sum(p == l for p, l in zip(predictions, labels)) print("accuracy:", round(correct / len(labels), 2)) for label in ["positive", "negative", "mixed"]: pairs = [(p, l) for p, l in zip(predictions, labels) if l == label] print(label, "recall:", sum(p == l for p, l in pairs) / len(pairs))
Read the output. Accuracy is 0.67, which looks okay. But recall for mixed is 0.0: the classifier never got a single mixed review right. Positive and negative are both 1.0.
Show the answer
Hold the change: complaints are being missed, even though the headline number didn't move
Missing complaints is likely the costliest mistake. The per-label numbers caught what accuracy hid.
Worked example: grading a model
Classify five reviews with structured output, and grade them by exact match. A small, fast model is often enough for classification. The eval is how you find out.
import json import requests SCHEMA = { "type": "object", "properties": {"label": {"type": "string", "enum": ["positive", "negative", "mixed"]}}, "required": ["label"], "additionalProperties": False, } def classify(text): response = requests.post("https://api.anthropic.com/v1/messages", headers={"x-api-key": "added-by-the-academy-server", "anthropic-version": "2026-10-21"}, json={"model": "claude-haiku-5-5", "max_tokens": 50, "system": "Classify the sentiment of a product review. Use mixed when it has clear praise and a clear complaint.", "messages": [{"role": "user", "content": text}], "output_config": {"format": {"type": "json_schema", "schema": SCHEMA}}}, timeout=60) response.raise_for_status() reply = "".join(b["text"] for b in response.json()["content"] if b["type"] == "text") return json.loads(reply)["label"] reviews = requests.get("https://api.gradient.test/v1/reviews").json()["data"][:5] for r in reviews: got = classify(r["text"]) print("ok " if got == r["label"] else "BAD", r["id"], "expected", r["label"], "got", got)
With an API key set, a real model classifies each review. Without one, the simulator fills the schema with its first allowed value, so every answer is "positive". Then you're watching the always-positive baseline. Either way the eval code is the same, and that's the point: the grader doesn't care who produced the answer.
Using evals to make decisions
Once your eval runs in a few seconds, use it for every decision:
- Prompt changes. Run it before and after. Keep the change only if the score goes up and no label gets worse.
- Model changes. Try the cheaper model. If it matches the expensive one on your eval, you've cut costs with evidence instead of hope.
- Catching breakage. Run the eval automatically before each release. If the score drops below a set level, stop the release.
Two habits keep evals honest.
Look at the failures, not only the score. Reading ten wrong answers usually tells you exactly what to fix.
Don't tune on your whole dataset. If you keep editing the prompt until every example passes, you've memorized the test. Keep a few examples aside. Check them only at the end.
For marketers: this is a holdout group. You wouldn't judge a campaign on the same audience you used to pick the creative.
Show the answer
You tuned on the test, so 100% overstates how it will do on new inputs
Like judging a campaign on the audience you used to choose it. Keep some examples aside that you only check at the end.
Don't tune until everything passes
If you edit a prompt until every example passes, you've only learned to pass those examples. Keep some back, so you can check on cases the prompt has never seen.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 5 review cards for spaced repetition
Try a quiz question
What are the three parts of an eval?