Evals cheat sheet
The compressed version of all five lessons. Print it, or keep it open while you work in the lab.
Vocabulary
| Term | Meaning |
|---|---|
| task | One test case: defined inputs plus success criteria. |
| trial | One attempt at a task. Run several, because outputs vary. |
| grader | Logic that scores one aspect of a trial. A task can have several. |
| transcript | The full record of a trial: outputs, tool calls, intermediate results. Also called a trace. |
| outcome | The final state of the world after a trial (the refund row exists), not what the agent says. |
| reference solution | A known-good run that passes every grader. Proves a task is solvable. |
| span | One timed step in a trace (chat, execute_tool, invoke_agent). Spans nest. |
| root cause | The first span whose output was wrong given correct inputs. |
| paired difference | Per task: candidate score − baseline score. |
| gate | Written rules turning a comparison into SHIP / BLOCK / INCONCLUSIVE. |
Source: Anthropic, Demystifying evals for AI agents (2026).
Why the same input gives different outputs
| Source | Fixable? | What to do |
|---|---|---|
| Sampling (temperature > 0) | Partly | Lower temperature, but never rely on it. |
| Server batching changes numerics, even at temperature 0 | Not by you | Run many trials and grade rates. 1,000 runs → 80 unique |
| One request at a time on your own machine | Often repeatable | Measured on Ollama: 30 runs at temperature 0 → 1 unique output. Repeatable here isn't repeatable elsewhere: other hardware, Ollama versions or parallel load can differ. |
Model updates (ollama pull can move a tag to new weights) | Yes: pin it | Record model and its digest (from /api/tags) in every result, and refuse comparisons across digests. |
seed | Best effort | Ollama accepts it but documents no guarantee. |
Grader ladder
| Rung | Example | Use when |
|---|---|---|
| Exact match | out === golden | Output is a closed enum or ID, never free text. |
| Normalised | trim, lowercase, sort keys | Rarely. Usually a sign you want properties. |
| Properties | parses · field = value · type · regex never matches a UUID | Structured output, and the default unit test. |
| Outcome | refund row exists in DB | Agents that act on the world. Prefer it over checking the path. |
| Model grader | LLM judge + binary rubric | Open-ended quality. Calibrate against human labels. |
| Human | expert review | Building labels and calibrating judges. |
Rule: use the cheapest rung that still judges what the user cares about. Be strict about what matters and indifferent to what doesn't.
Formulas
| Metric | Formula | Question it answers |
|---|---|---|
| pass rate p | passes / n | How often does one trial pass? |
| pass@k | 1 − (1 − p)^k | Does at least one of k succeed? (candidates, retries a human picks from) |
| pass^k | p^k | Do all k succeed? (consistency, customer-facing) |
Anchors: 0.75³ ≈ 0.42 · 0.9⁵ ≈ 0.59 · 0.95¹⁰ ≈ 0.60 · 0.99¹⁰ ≈ 0.90.
Capability eval: starts low, a hill to climb. Regression eval: sits near 100% and guards what already works. Reliable capability tasks graduate into the regression suite.
Reference model: local client
import OpenAI from "openai" // used as an HTTP client: Ollama speaks this format
const client = new OpenAI({ baseURL: "http://localhost:11434/v1/", apiKey: "ollama" })
// Ollama: no tool_choice, no n, no logprobs (as of Oct 2026)
Reference model: trials + properties
let passes = 0
for (let i = 0; i < n; i++) {
const res = await client.chat.completions.create({ model, messages, temperature, seed })
const text = res.choices[0].message.content ?? ""
const json = text.match(/\{[\s\S]*\}/)?.[0] // tolerate fences and prose
const out = json ? JSON.parse(json) : null
if (out?.intent === "refund" && out?.order_id === "4417") passes++
log({ text }) // keep the transcript
}
const p = passes / n // a rate, compared to a threshold
The full version is in labs/01-variance/src/. The agent version, with tools, tracing and the gate, is labs/02-bench/.
Dataset (Lesson 2)
{ "id": "refund-outside-window", "suite": "regression" | "capability",
"tags": ["edge", "no-act"], "source": "seed" | "observed: <run> <task>#<k>",
"ticket": "…",
"expect": { "refunds": [], // the OUTCOME, checked first
"must_call": ["lookupOrder"], // path checks only for real rules
"reply_must": ["30"] }, // loose regex, never exact wording
"reference": { "calls": [ … ], "reply": "…" } } // npm run validate replays it
| Test every task for | Why |
|---|---|
| Two experts would agree on pass/fail | Otherwise you're measuring the ambiguity |
| A reference solution passes it | A 0% task is usually a broken task |
| It grades the outcome | Path checks punish valid approaches |
| The suite covers "act" and "don't act" | One-sided evals create one-sided optimisation |
Sources: 20–50 tasks from real failures to start (Anthropic). Error analysis: open coding (one note on the first failure per trace) → axial coding (group + count) → stop at saturation (Husain & Shankar). Synthetic: dimensions first, 20 tuples by hand, then expand.
Model judges: binary · one dimension per judge · an UNKNOWN way out · calibrated on your labels. Report TPR = failures caught ÷ all failures and TNR = good passed ÷ all good, never raw agreement.
Tracing (Lesson 3)
| Span name | Key attributes |
|---|---|
invoke_agent {agent} | gen_ai.agent.name, ticket, final reply |
chat {model} | gen_ai.request.model, .temperature, gen_ai.usage.*_tokens, gen_ai.response.finish_reasons, output messages |
execute_tool {tool} | gen_ai.tool.name, gen_ai.tool.call.id, .arguments, .result |
OpenTelemetry GenAI semantic conventions, status Development (Oct 2026).
| First wrong step is… | Seen as |
|---|---|
| a model decision | chat output: wrong or skipped tool, wrong args |
| a tool bug | execute_tool result wrong given right args |
| a misread result | right result, next chat contradicts it |
| an eval bug | every span right, grader still ✗ |
npm run failures → npm run trace -- <file> → fix as a new version → re-run the task with K=5 and the suite with K=3.
Comparing versions (Lesson 4)
| Quantity | Formula | Note |
|---|---|---|
| SE of a pass rate | √(p(1−p)/n) | 15 tasks at 90% → ±15 pp at 95% |
| task score | mean of its k trials | trials of a task aren't independent |
| paired SE | sd(dᵢ) / √n, dᵢ = candᵢ − baseᵢ | Miller Eq. 7 |
| 95% CI | mean(d) ± t·SE | t = 2.14 at n = 15, → 1.96 for large n |
| Verdict | Rule (gate.json) |
|---|---|
| NOT COMPARABLE | dataset hash, model, model digest or temperature differ, or synthetic data |
| BLOCK | regression suite < 95%, or the whole CI < 0 |
| INCONCLUSIVE | CI reaches below −5 pp (fails CI by default) |
| SHIP | otherwise |
CI without a GPU: run the eval locally, commit candidate/result.json with the PR. CI runs npm run verify (agent hash and dataset hash must match the PR's code) and then npm run compare.
Publishing checklist (Final)
- Reproducible: lockfile +
npm ci, dataset hash, config in every committed result file, model digest recorded, one command to reproduce, free on Ollama. - Comparable: same tasks and k, paired differences with 95% CIs, n stated, gate in CI.
- Honest: claim → evidence → caveat, judge TPR/TNR, failure taxonomy, three or more threats to validity, cost and time.
Go deeper
- Demystifying evals for AI agents
- Your AI Product Needs Evals
- AI Evals: Everything You Need to Know
- Defeating Nondeterminism in LLM Inference
- Why AI evals are the hottest new skill for product builders
- Using LLM-as-a-Judge For Evaluation
- OpenTelemetry GenAI semantic conventions
- A statistical approach to model evaluations and the paper Adding Error Bars to Evals