Index Ai Academy
Phase 1 · Building with LLMs · Quality & production

Evals

Measure output quality with test sets instead of vibes.

About 30 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. From gut feel to numbers
  2. Where datasets come from
  3. A first eval: review sentiment
  4. Accuracy hides things
  5. Worked example: grading a model
  6. Using evals to make decisions
  7. 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:

  1. 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.
  2. The system under test. Your prompt, model and code, run on every input.
  3. 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

GraderGood forExample
Exact matchLabels, categories, idspredicted == "negative"
Code checksFormat and rulesvalid JSON, under 60 characters, contains a link, no banned words
Contains / regexRequired factsanswer mentions "60 days"
LLM as judgeOpen-ended quality"Is this reply polite, and does it answer the question?" (next lesson)
Human reviewFinal say, building the labelsa 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.

Try it now

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.

Quick check
Which grader fits "the subject line must be under 60 characters"?
  1. A code check: len(subject) < 60
  2. An LLM judge
  3. Exact match against one correct subject line
  4. Human review of every subject line
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"])
It runs right here in your browser, with nothing to install.

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.

Quick check
What does this print?
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))
  1. 0.5
  2. 1.0
  3. 0.75
  4. 3
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.

Quick check
You switch your classifier to a cheaper model. Accuracy stays at 0.88, but recall for "negative" drops from 0.90 to 0.60. What should you do?
  1. Switch to a grader that ignores labels
  2. Hold the change: complaints are being missed, even though the headline number didn't move
  3. Delete the negative examples from the dataset
  4. Ship it: accuracy is unchanged
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

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)
In the lesson you can change this and run it.

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.

Quick check
You edit a prompt until all 40 eval examples pass, then report "100% accuracy". What's wrong?
  1. 40 examples is too many
  2. You tuned on the test, so 100% overstates how it will do on new inputs
  3. Accuracy can't be measured on prompts
  4. Nothing: 100% is the goal
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

Create a free account

Try a quiz question

What are the three parts of an eval?

  1. A dataset with expected outputs, the system under test, and a grader
  2. A prompt to test, a model to run it on, and an API key to pay for the calls
  3. Training data for the model, validation data to tune it, and a GPU to run it
  4. A dashboard of the scores, alerts for any drop, and logs of each request

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson