Post
Evals: measuring an LLM system
Why every LLM system needs an evaluation set, how to build one, code-based checks, LLM-as-judge with a rubric, and how to use evals to compare prompts, models and settings.
An eval is a set of test cases with a way to grade the output, run every time you change the prompt, the model, the retrieval or a setting. Without one, every change is a guess, and you discover regressions from your users.
The harness ran with Python 3.9 and its output is real; its model call is a stand-in. The LLM-judge code follows the official guide to developing tests and was not run.
What makes a good eval
The official guide gives four principles:
- Specific, measurable criteria. Not “safe outputs” but, in the guide’s example, “less than 0.1% of outputs out of 10,000 trials flagged for toxicity”.
- Task-specific cases that mirror real traffic, including edge cases: irrelevant input, very long input, ambiguous requests.
- Automate when possible, so you can run it on every change.
- Volume over polish. The guide says that “more questions with slightly lower signal automated grading is better than fewer questions with high-quality human hand-graded evals”.
Start with real examples: questions users asked, tickets that came in, documents you process. Anthropic’s multi-agent article started with about 20 realistic queries; a few dozen good cases already catch most regressions.
Code-based grading
When code can check the answer, use code: it is fast, cheap and exact. A classification eval with exact match:
# A tiny eval: each case has an input and the expected label
cases = [
{"input": "I was charged twice", "expected": "billing"},
{"input": "App crashes on upload", "expected": "bug"},
{"input": "How do I change my email?", "expected": "account"},
{"input": "Refund for last month please", "expected": "billing"},
]
def classify(text):
# Stand-in for the model call, so the harness itself can be tested
fake_answers = {"I was charged twice": "Billing", "App crashes on upload": "bug",
"How do I change my email?": "billing", "Refund for last month please": "billing"}
return fake_answers[text]
def exact_match(output, expected):
return output.strip().lower() == expected.lower()
failures = []
for case in cases:
output = classify(case["input"])
if not exact_match(output, case["expected"]):
failures.append((case["input"], case["expected"], output))
passed = len(cases) - len(failures)
print(f"passed {passed}/{len(cases)} ({passed / len(cases):.0%})")
for text, expected, got in failures:
print(f"FAIL: {text!r}: expected {expected!r}, got {got!r}")
passed 3/4 (75%)
FAIL: 'How do I change my email?': expected 'account', got 'billing'
Other code-based checks from the guide: string contains, valid JSON against a schema, embedding similarity between answers (for consistency), and ROUGE for summaries. For RAG, also check retrieval separately: was the right chunk in the top results?
LLM-as-judge
Tone, helpfulness and faithfulness to a document need a second model that grades the output against a rubric:
import anthropic
client = anthropic.Anthropic()
def judge(question, answer, documents):
prompt = f"""Grade the answer using only the documents.
<documents>{documents}</documents>
<question>{question}</question>
<answer>{answer}</answer>
Rubric:
- 1: every claim in the answer is supported by the documents
- 0: at least one claim is not supported, or the answer contradicts the documents
Think about each claim first, then end with a line "SCORE: 0" or "SCORE: 1"."""
reply = client.messages.create(
model="claude-opus-5-5",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}],
)
text = next(b.text for b in reply.content if b.type == "text")
return int(text.strip().splitlines()[-1].removeprefix("SCORE:").strip())
What makes a judge reliable:
- A detailed rubric with clear levels, not “is this good?”. Binary or small scales (0/1, 1 to 5) are more consistent than open scores.
- Reasoning before the score, so the judge looks at each claim before deciding.
- A different model from the one being graded, which the guide calls “generally best practice”.
- Check the judge against a few dozen cases you graded by hand. If it disagrees with you often, fix the rubric before trusting its numbers.
Using evals
- Before a change: run it, save the score. After: run again, compare. Look at the cases that flipped, not only the total.
- Choosing a model or effort level: the docs repeatedly recommend an effort sweep, running the same eval at several levels and taking the cheapest that keeps quality.
- In CI: a small, fast eval on every prompt change, like unit tests. A larger one before a release.
- From production: when a user reports a bad answer, add it as a case. The eval grows from real failures.
LLM outputs vary between runs, so for important decisions run each case more than once and compare averages.
The next post covers observability: seeing what your system actually does in production.