LLM Evals
An eval is a scored dataset you run on every prompt change. Offline, sampled online, and why "we tried it and it looked good" is not a release bar.
- evals
- quality
- testing
You cannot unit-test a poem. You can unit-test “did we refund the right order id.” LLM evaluation is that test, run as a scored dataset on every prompt, model, or tool change. “We tried it and it looked good” is a demo. It is not a release bar.
If you already ship CI for React, this is the same habit with worse determinism. You pin inputs, you score outputs, you fail the build when the score drops. Start with assertions on structured fields. Add rubrics only when language is the product.
Key takeaways
- An eval is a dataset plus a scorer you rerun. A hallway QA pass is not an eval.
- Exact-match on ids, tools, and JSON first. Rubrics and pairwise later.
- Offline golden sets catch regressions. Sampled production traces catch drift.
- Pin grader versions. A moving judge is not a test.
- Score the field you would argue about in an incident, not a 1-5 “quality” blob.
LLM evaluation is a dataset plus a scorer
OpenAI’s eval docs describe the same two ingredients: test data, and testing criteria (graders). Anthropic’s eval tooling is the same idea in a different console: success criteria, then a set you can rerun. You can also do it in 80 lines in CI. The product is the habit, not the vendor tab.
import { z } from 'zod';
const Output = z.object({
orderId: z.string(),
action: z.enum(['refund', 'deny', 'ask_human']),
amountCents: z.number().int().nonnegative().optional(),
});
type Case = {
id: string;
input: { user: string; orderId: string };
expect: z.infer<typeof Output>;
};
type Score = { caseId: string; pass: boolean; checks: Record<string, boolean> };
export async function runEvals(
cases: Case[],
generate: (input: Case['input']) => Promise<unknown>,
): Promise<{ passed: number; scores: Score[] }> {
const scores: Score[] = [];
for (const c of cases) {
const parsed = Output.safeParse(await generate(c.input));
if (!parsed.success) {
scores.push({ caseId: c.id, pass: false, checks: { schema: false } });
continue;
}
const checks = {
schema: true,
orderId: parsed.data.orderId === c.expect.orderId,
action: parsed.data.action === c.expect.action,
amount:
c.expect.amountCents === undefined ||
parsed.data.amountCents === c.expect.amountCents,
};
scores.push({ caseId: c.id, pass: Object.values(checks).every(Boolean), checks });
}
return { passed: scores.filter((s) => s.pass).length, scores };
}
That is an eval: labeled inputs, acceptable outputs, a scorer that does not care how eloquent the model was. A poem about the refund can still fail orderId.
Which scorer?
| Scorer | Use for | Do not use for |
|---|---|---|
| Schema / exact match | Ids, enums, tool names, JSON fields | Tone, summaries |
| String contains / regex | Required citations, refusal phrases | Paraphrases |
| Execution | “SQL runs”, “tests still pass” | Prose quality |
| Rubric (LLM judge) | Helpfulness when humans defined the rubric | Math, amounts, authz |
| Pairwise | A vs B on the same case | A release gate with no human calibration |
Start at the top of the table. LLM as judge is a later tool: biased toward long polite answers unless you fight it. Golden datasets are how you stop cheating; if the team wrote twelve examples from memory, you measured imagination.
For agents, score the trace as well as the final text: did it call get_order before refund_order? Did it call a forbidden tool? OpenAI’s agent-eval guidance splits tool selection from argument precision. That split is worth copying even if you never use their dashboard.
Offline, sampled online
Offline. A held-out set in git. Run on every prompt PR. This is the eval gate. Keep it small enough to finish in CI, hard enough that a prompt typo fails. Quarantine flaky cases; do not lower the threshold until everything is green.
Online. Sample production traces (with PII redaction). Score them with the same assertions where you can (parsed JSON, tool names). Use a judge only on a slice a human still spot-checks. Online is how you find the query your golden set never dreamed of: slang, new SKUs, empty retrieval.
import { z } from 'zod';
const OnlineOutput = z.object({
orderId: z.string(),
action: z.enum(['refund', 'deny', 'ask_human']),
});
type SampledTrace = {
id: string;
output: unknown;
tools: string[];
};
export function scoreOnlineSample(
traces: SampledTrace[],
everyNth: number,
allowedTools: Set<string>,
) {
return traces
.filter((_, i) => i % everyNth === 0)
.map((t) => {
const parsed = OnlineOutput.safeParse(t.output);
const checks = {
schema: parsed.success,
toolsAllowed: t.tools.every((name) => allowedTools.has(name)),
};
return { traceId: t.id, pass: Object.values(checks).every(Boolean), checks };
});
}
// Nightly: scoreOnlineSample(redactedProdTraces, 20, allowed).
// 1/20 of traffic, same assertions as CI. Not a vibe pass on Slack screenshots.
Keep the online schema aligned with the offline runner. If the two scorers drift, you built two products.
Pair them. Offline is regression. Online is coverage. Neither replaces the other. “Looks good on my laptop” is neither.
Pin versions: prompt hash, model id, tool schema version, grader prompt version. If the judge model silently upgrades, your trend line is a lie.
What not to score with vibes
Do not average a 1-5 “quality” into a single dashboard number and call it health. Helpfulness can go up while refund accuracy dies. Split metrics the way you already split Web Vitals: one number per failure you care about.
Do not eval math by asking another model “does this look right?” Execute it, or compare the field.
Do not put the few-shot examples from the prompt into the golden set and then celebrate 100%. That is training leakage. Hold some cases out.
Do not skip the boring assertions because they feel unlike “AI.” They are the ones that survive an incident review.
Interview answer (out loud)
“An LLM eval is a scored dataset I rerun on every prompt or model change. I start with schema and exact-match on structured fields like order id and action, then add rubrics only for language. Offline golden sets are the CI gate. Sampled production traces catch drift. ‘We tried it and it looked good’ is not a bar. I pin grader versions so the test does not move under my feet.”
Further reading (primary sources)
- OpenAI evals
- OpenAI evaluation best practices
- Anthropic: define success criteria and build evaluations
Related on this site
Related guides
- Golden Datasets for LLM AppsA golden set is labeled inputs and acceptable outputs you own. How to collect it from production, keep it from rotting, and stop cheating on it.
- LLM as JudgeA judge model scores outputs against a rubric. It is biased, cheap, and useful if you calibrate it against humans and keep the rubric tight.
- Eval Gates in CIFail the build when a golden set regresses. Thresholds, flake budgets, and why an LLM judge in CI needs pinning and retries of its own.
- Debugging LLM HallucinationsA hallucination is a fluent claim without support. Debug it as retrieval miss, prompt pressure, sampler noise, or missing refusal path.
- RAG Evaluation MetricsSplit retrieval metrics from answer metrics. Hit rate, MRR, faithfulness, and why a single 1–5 "quality" score hides the broken half.