Module 12 · Part III · Practice

Error analysis: building your own eval

Nobody hands you an eval for your agent. You build it by reading what your agent actually does, naming the ways it fails, counting them, and only then writing graders for the failures that matter. This module is the practitioner's playbook for going from zero to a working eval suite.

Intermediate~50 minPrerequisites: Modules 02, 03, 05Python + pandas

By the end of this module you can

  • Explain why error analysis (looking at data) must come before choosing metrics or writing judges.
  • Run the full loop: collect traces, open coding, axial coding, count, decide, build evaluators, validate, add to CI, iterate.
  • Generate synthetic test inputs the right way, from structured dimensions and tuples, not "write 100 questions".
  • Know when to stop reviewing (theoretical saturation) and who should decide what "good" means (the benevolent dictator).
  • Do agent-specific analysis: transition failure matrices, and classifying tool, reasoning and environment errors.
  • Apply Anthropic's guidance for agent eval suites, and recognize criteria drift when it happens to you.
  • Walk through a complete worked example on a customer-support refund agent, with code.

Why you start by looking at data

Most people who set out to "add evals" start in the wrong place. They open a tool, browse its list of built-in metrics (helpfulness, coherence, faithfulness, toxicity), pick five that sound sensible, and wire them up. A dashboard appears with numbers on it. The numbers move a little each week. Nothing about the product gets better.

The reason is simple: those metrics were written by someone who has never seen your agent fail. A generic "helpfulness" score cannot tell you that your refund agent keeps refunding the wrong order when a customer has three recent purchases, because that failure is specific to your product, your tools and your users. You only discover it by reading transcripts.

Hamel Husain and Shreya Shankar, who teach the most widely followed practitioner course on this, put the point bluntly. In their Evals FAQ they report spending "60-80% of our development time on error analysis and evaluation", and they advise against generic, off-the-shelf metrics as a starting point. Hamel's earlier essay Your AI Product Needs Evals says "You must remove all friction from the process of looking at data." The method they teach is called error analysis, and it is borrowed from qualitative research in the social sciences.

Analogy: the doctor, not the thermometer

A thermometer gives you a number (a generic metric). A good doctor first examines the patient, forms a diagnosis, and then decides which tests to order. Error analysis is the examination. Evaluators are the tests you order once you know what you are looking for. Ordering every test in the catalog without examining the patient is expensive and tells you little.

Metrics first (common, weak) Pick 5 metrics helpfulness, coherence… Dashboard numbers on a chart Numbers wiggle no link to real failures Nothing improves no action to take ✗ Data first (error analysis) Read ~100 traces real or synthetic Name failures your own taxonomy Targeted checks code + narrow judges Fixes that stick measured and guarded ✓ repeat on new traffic: new failures, new checks
Where you start decides where you end up. Generic metrics were written by someone who never saw your agent fail, so they rarely point at a fix. Starting from traces produces failure modes that are specific, countable and fixable, and the checks you build measure exactly those.

Two new terms before we go on:

  • A trace (also transcript or trajectory) is the full record of one run of your agent: the user's messages, the model's replies, every tool call with its arguments and results, and the final state. (Module 01 introduced the vocabulary.)
  • A failure mode is a named, recurring way the agent goes wrong, such as "refunds an order outside the 30-day window". Error analysis is the process of discovering your failure modes and how often each occurs.

The whole process at a glance

Here is the entire loop you will learn in this module. Each step gets its own section below, and the worked example at the end runs through all of them on one agent.

1 · Collect traces real logs, or synthetic from dimension tuples 2 · Open coding free-form note on the FIRST failure per trace 3 · Axial coding cluster notes into a failure taxonomy 4 · Count frequency x severity, pick what to attack 5 · Decide fix directly, or build code check / LLM judge 6 · Validate judges vs human labels, TPR / TNR (Module 13) 7 · Add to CI run on every change, monitor prod (Module 15) 8 · Iterate new data, new failures, criteria drift repeat qualitative: read and name quantitative: measure, automate, guard
The error analysis loop. The top row is qualitative (humans read traces and name failures); the bottom row is quantitative (turn the important failures into automated, validated checks). The loop never really ends: new traffic brings new failures.

Notice the order. Evaluators appear in step 5, not step 1. You write graders only for failure modes you have actually observed and judged worth measuring. This is the single biggest difference between teams whose evals drive improvement and teams whose evals are decoration.

Step 1: collect traces

You need a pile of traces to read. There are two sources, and you usually want both.

Real traces (best)

If the agent is live, even in a beta, log every run with a tracing tool (Langfuse, LangSmith, Braintrust, Arize Phoenix and others; see Module 14). Sample traces for review, deliberately including negative feedback, escalations, long runs, and runs that errored. Real traffic contains the weird things you would never think to test.

Synthetic traces (to start, or to fill gaps)

Before launch, or to cover rare situations, you generate test inputs and run your agent on them. The danger is that naive generation produces bland, repetitive inputs. The fix is structured generation from dimensions, described next.

Synthetic data done right: dimensions, tuples, then language

The naive approach is to ask an LLM: "Generate 100 questions a customer might ask a refund bot." You will get 100 polite, well-formed, near-duplicate questions that all hit the happy path. LLMs regress to the most typical output, so unstructured generation collapses toward the middle of the distribution, which is exactly where your agent already works.

The approach Husain and Shankar recommend has three stages:

  1. Define dimensions. A dimension is an axis along which real inputs vary and along which you suspect your agent might fail. For a support agent: persona (calm, frustrated, confused, non-native speaker), feature or intent (full refund, partial refund, exchange, cancel), scenario (single order, multiple orders, outside return window, high-value order, missing order number). Choose dimensions from hypotheses about where things break.
  2. Generate tuples. A tuple is one combination of dimension values, such as (frustrated, partial refund, multiple orders). You can enumerate all combinations with code, or ask an LLM to propose realistic ones, then filter out the nonsensical ones by hand. Structured tuples guarantee coverage of the corners.
  3. Turn each tuple into natural language in a separate step. Now the LLM's only job is to write a realistic message for a specific, pinned-down situation. Separating "what situation" from "how would a person phrase it" is what gives you diversity without losing control.
Dimensions Persona calm · frustrated confused · terse Intent full · partial refund exchange · cancel Scenario multi-order · late high-value · no ID combine 4 x 4 x 4 = 64 drop nonsense, weight risky ones Tuples (frustrated, partial, multi) (confused, full, late) (terse, cancel, no-ID) ... 42 more LLM Realistic messages "i got charged for 3 things and only want ONE back. why is this so hard" "Hello, I bought a lamp in March, can I have money?" "cancel it"
Dimensions, then tuples, then language. Code or an LLM enumerates structured combinations (so you control coverage), and a separate LLM call only has to phrase each one realistically (so you get diversity). The tuple stays attached to each message as metadata, which later lets you slice results by dimension.

That last point in the caption is a hidden superpower: because each synthetic input remembers its tuple, you can later ask "what is our failure rate for frustrated customers with multiple orders?" and get a real answer.

# Structured synthetic input generation: dimensions -> tuples -> natural language
import itertools, json, random
import anthropic

DIMENSIONS = {
    "persona":  ["calm", "frustrated", "confused non-native speaker", "terse"],
    "intent":   ["full refund", "partial refund", "exchange", "cancel order"],
    "scenario": ["single recent order", "3 orders in the last month",
                 "order is 45 days old (policy window is 30)",
                 "order worth $900 (needs human approval)",
                 "customer has no order number"],
}

# 1) Enumerate every combination, then drop ones that make no sense.
all_tuples = [dict(zip(DIMENSIONS, combo))
              for combo in itertools.product(*DIMENSIONS.values())]
def plausible(t):
    return not (t["intent"] == "exchange" and "no order number" in t["scenario"])
tuples = [t for t in all_tuples if plausible(t)]

# 2) Sample, over-weighting scenarios you believe are risky.
random.seed(7)
risky = [t for t in tuples if "45 days" in t["scenario"] or "$900" in t["scenario"]]
sample = random.sample(tuples, 30) + random.sample(risky, 15)

# 3) Separate call: turn each tuple into a realistic first message.
client = anthropic.Anthropic()          # reads ANTHROPIC_API_KEY
MODEL = "claude-sonnet-5-5"             # example id; use any current model

def to_message(t):
    prompt = (
        "Write the FIRST chat message a real customer would send to an online "
        "store's support bot. Do not mention these labels explicitly.\n"
        f"Persona: {t['persona']}\nWhat they want: {t['intent']}\n"
        f"Situation: {t['scenario']}\n"
        "Match the persona's tone, typos and length. Output only the message."
    )
    resp = client.messages.create(model=MODEL, max_tokens=200,
                                  messages=[{"role": "user", "content": prompt}])
    return resp.content[0].text.strip()

with open("synthetic_inputs.jsonl", "w") as f:
    for i, t in enumerate(sample):
        f.write(json.dumps({"id": f"syn-{i:03d}", "tuple": t,
                            "message": to_message(t)}) + "\n")
Tips from practice

Read the generated messages before running them. Delete ones that no real customer would send. For agents, the scenario usually also needs a matching environment state: if the tuple says "3 orders in the last month", your test database must actually contain three orders for that customer. Generate the fixture data from the same tuple, so the input and the world agree. And if you have real traffic, use real messages as few-shot examples of tone so synthetic messages do not sound like an LLM.

Step 2: open coding

Open coding is a term from qualitative research (grounded theory). It means reading raw material and writing short, free-form notes about what you observe, without a predefined list of categories. In our setting: you open a trace, read it, decide pass or fail, and if it fails, write one or two sentences describing what went wrong.

Rules that make open coding work:

  • Note the first failure in each trace. Agents cascade: if the agent picked the wrong order in step 2, everything after is wrong too. The Evals FAQ advises focusing on "the first failure observed in a trace, as upstream errors can cause downstream issues." Fixing the root often removes the downstream symptoms for free.
  • Be concrete, not abstract. "Refunded order #1182 but customer asked about the blue jacket (#1190)" is useful. "Bad accuracy" is not.
  • Use a binary pass/fail verdict, not a 1-5 score. Husain and Shankar argue that a Likert scale mostly adds noise and hides the decision; a written critique carries the nuance instead.
  • Do the first batch yourself. Their guidance is to review about 100 diverse traces and annotate at least the first 30 yourself before accepting any LLM-suggested codes. You are training your own judgment, and that cannot be delegated.
  • Do not fix things mid-session. Write it down and keep reading. You want the distribution first.
Trace T-017, as the reviewer reads it 1 User: "refund the jacket pls, it's torn" 2 lookup_orders(881) → #1182 mug, #1190 jacket 3 issue_refund(order=1182, amount=64) FIRST FAILURE 4 Agent: "Done! $64 refunded for #1182." Open code (first failure only) Refunded the mug (#1182), not the jacket the user named (#1190). Took the most recent order without matching the item to the request. Step 4 is a downstream symptom: fixing step 3 removes it, so no separate code for it. Verdict: FAIL binary, with the note carrying the nuance
Open coding one trace. Read until the first thing goes wrong, mark it, and write a concrete note about it in plain words. Everything after that point is usually a consequence, so coding it separately would double-count one root cause. The verdict is pass or fail, never a 1 to 5 score.

Step 3: axial coding (the taxonomy)

Axial coding is the second qualitative pass: you take your pile of open codes and group them into categories, building connections ("axes") between notes that describe the same underlying problem. The output is a failure taxonomy: a short list of named failure modes, each with a clear definition and examples. The FAQ calls this "the most important step".

Open codes (raw notes) Axial codes (taxonomy) refunded #1182, user meant the jacket took most recent order without asking approved refund on 52-day-old order never called check_policy at all promised free return label (not a policy) said "5-7 days", policy says 10 API timed out, told user "done!" $900 refund, no escalation Wrong order selected Refund outside policy / policy skipped Hallucinated policy promise Tool failure mishandled $900 case: own mode? (see text)
From notes to a taxonomy. Many specific notes collapse into a few named failure modes. Some notes do not fit cleanly (the dashed line): that is a signal to create a new category or sharpen a definition, not to force it in.

Good taxonomy categories share four properties:

  1. Actionable: each category suggests a kind of fix. "Wrong order selected" points at order disambiguation. "Bad answer" points nowhere.
  2. Mutually exclusive enough that one trace's first failure lands in one bucket. Overlap is fine at the margins, but if half your traces could go in either of two buckets, merge or redefine them.
  3. Defined in writing, with a one-line definition and a pass and fail example each. That definition becomes the spec for any judge you build later.
  4. Small: typically somewhere between 5 and 15 categories. If you have 40, you are still at the open-code level.

You can use an LLM to help here: paste your open codes and ask it to propose groupings. That is a reasonable accelerator after you have done enough coding yourself to judge its suggestions. Then relabel every trace with the final categories (a spreadsheet column is enough).

Step 4: count and prioritize

Once each failing trace has a category, count. This is the moment qualitative work becomes quantitative, and it is often the most clarifying moment of the whole process: the failure you were emotionally sure was the big problem is frequently third or fourth on the list.

Frequency is not the only input. Multiply mentally by severity (a wrongly issued $900 refund costs real money; a slightly stiff tone does not) and by ease of fix. A useful rule: the top of your list is whatever is frequent and costly and not trivially fixed by a prompt change.

critical high medium low 04 812 how often (failing traces, first failure only) attack first rare but costly 1 2 3 4 5 6 7 8 9 Failure modes 1 Refund outside policy 2 Wrong order selected 3 Hallucinated promise 4 Assumed, didn't clarify 5 Missing escalation 6 Tool failure mishandled 7 Policy check skipped 8 Misclassified intent 9 Incomplete confirmation color = planned grader
Frequency is only half of priority. The worked example's nine failure modes (counts from later in this module), placed by how often they occur and how much damage each does. Frequent and severe modes (top right) come first. Missing escalation (5) is rare but lets a $900 refund through, so it still earns a guard. Colors follow the bar chart below: blue code check, violet LLM judge, green direct fix. Severity ratings are the team's judgment, not data.

When to stop reading: theoretical saturation

How many traces is enough? Qualitative researchers use the idea of theoretical saturation: you stop when new material stops producing new categories. Husain and Shankar translate this directly: "Continue until new traces stop revealing failure modes or changing existing ones," and they recommend reviewing at least 100 traces as a practical floor. Treat 100 as a rule of thumb, not a law. A narrow agent may saturate sooner; a broad general assistant can keep surprising you for much longer.

traces reviewed distinct failure modes 0 30 65 100 130 0 4 8 9 modes found in total saturation zone new traces add counts, not modes first 30: many new modes
Theoretical saturation (illustrative, not real data). Early traces keep revealing new failure modes; later ones only add counts to existing ones. When the curve has been flat for a while, stop discovering and start measuring. Reopen discovery whenever the product, model or traffic changes.

Who decides what "good" means

Error analysis depends on a judgment call on every trace: pass or fail? If three people with different mental standards annotate, you get three incompatible taxonomies and noisy labels. The Evals FAQ's answer is a benevolent dictator: "Appointing a single domain expert as a benevolent dictator is the most effective approach. This person becomes the definitive voice on quality standards."

  • It should be a domain expert: for a refund agent, someone who knows the refund policy and what great support feels like (a support team lead), not necessarily an engineer. For a legal agent, a lawyer. Hamel's LLM-judge essay calls this role the principal domain expert.
  • "Dictator" does not mean they work alone. Others can annotate and raise disagreements, but one person resolves them, so the labels stay consistent.
  • When you do use multiple annotators (for example, to scale up), measure their agreement (Cohen's kappa, see Module 13) and resolve disagreements into sharper written definitions.

Build a custom review interface (it pays for itself)

If looking at data is the core activity, the tool you look at it with matters. Generic trace viewers show JSON; your domain expert does not want to read JSON. The FAQ recommends building a simple custom annotation app, and lists features that matter: render the trace the way the domain looks ("If you're evaluating generated emails, render them to look like emails"), show progress ("Trace 45 of 100"), keyboard hotkeys for pass, fail and next, filtering and search by metadata, and surfacing traces already flagged by guardrails or automated evaluators. Keep all the context on one screen.

Refund agent review Trace 45 of 120 filter: persona=frustrated Customer: refund the jacket pls, it's torn lookup_orders(cust=881) -> 3 orders issue_refund(order=1182, $64) Agent: Done! $64 refunded for order #1182. Anything else? rendered like a chat, tool calls inline Context #1182 mug $64 day 3 #1190 jacket $120 day 5 policy: 30 days, >$500 escalate Pass [P] Fail [F] code: wrong_order_selected (autocomplete) note: refunded mug, customer said jacket; never asked which order. [N] next
A custom annotation UI (wireframe). Everything the reviewer needs is on one screen: the conversation rendered as a chat, tool calls inline, the ground-truth context (orders and policy) beside it, one-key verdicts, and a code field that autocompletes from the growing taxonomy. A few hours of building this can make hundreds of reviews bearable.

Why does it pay off? Because the cost of error analysis is almost entirely reviewer time and attention. A reviewer who has to click into nested JSON, cross-reference the order database in another tab, and type notes into a separate spreadsheet will review a fraction as many traces and will quit sooner. Today, a coding agent can build a bespoke review app for your trace format in an afternoon, which makes the trade even easier.

Step 5: decide, fix directly or build an evaluator

Now you have a ranked list of failure modes. For each one, the question is not "which metric?" but "what is the cheapest thing that makes this stop happening, and do I need to keep watching it?"

Failure mode from the taxonomy Is it a spec gap? (the prompt or tool never told the agent what to do) yes Fix directly: prompt, tool, code then re-check a few traces no / still fails Can code verify it objectively? (state, tool args, regex, schema) yes no Code-based check cheap, deterministic, run always LLM judge (1 mode) validate vs humans first optional regression check
Fix, check, or judge? Many failures vanish with a one-line prompt or tool change and need no evaluator at all. Persistent failures get the cheapest reliable grader: code where the answer is objective, a narrowly scoped LLM judge where it needs judgment.

Three principles guide this step:

  • Fix obvious bugs first. If the system prompt never mentions the 30-day window, the agent is not "failing", it was never told. Fix it. Husain and Shankar advise building LLM judges only for failures that persist after such fixes; a judge for a problem you could fix in one line is wasted work.
  • Prefer code checks. For agents, a surprising amount is objectively checkable from the environment and the tool log: was issue_refund called on an order older than 30 days? Was escalate_to_human called for amounts over $500? Did the final message claim success when the refund API returned an error? These are cheap, deterministic, and never drift (see Module 03).
  • One judge per failure mode. When you do need an LLM judge ("did the agent promise anything not in the policy?"), make it binary and narrow. The FAQ names "directing their LLM judge to catch too many different kinds of errors at once" as the most common mistake. Your taxonomy definition, with its pass and fail examples, is the judge's rubric.

Steps 6 to 8: validate, automate, iterate

Validate judges. An LLM judge is a classifier, and classifiers make mistakes. Before you trust it, compare its verdicts against your domain expert's labels on traces the judge was not tuned on. The FAQ recommends train, dev and test splits of your labeled traces and reporting the judge's true positive rate (TPR: of the traces the human passed, how many the judge passed) and true negative rate (TNR: of the traces the human failed, how many the judge failed). Your error analysis labels are exactly the ground truth you need. Module 13 does this in depth, including how to correct the measured pass rate for judge error.

Human-labeled traces from error analysis (n = 180) train · few-shot examples dev · tune the judge prompt test · measure once (n = 72) score the judge on test judge PASS judge FAIL human PASS human FAIL 42agree 3judge too harsh 3judge too lenient 24agree TPR = 42 / 45 = 93% of traces the human passed, share the judge passed TNR = 24 / 27 = 89% of traces the human failed, share the judge failed Both high? Then run the judge on unlabelled traffic, and correct its rate for known error (Module 13).
Validating a judge with your error-analysis labels. Few-shot examples come only from train, prompt tweaks are judged only on dev, and test is touched once so the reported TPR and TNR are honest. A judge that is too lenient (bottom left) hides real failures, which is usually the more dangerous error. Illustrative numbers; the split proportions are one reasonable choice.

Add to CI and production. Once checks and validated judges exist, run them automatically: on a fixed regression set on every prompt or code change, and on a sample of production traffic. Failures in production feed straight back into step 1. Module 15 covers the plumbing.

Iterate. Keep a steady trickle of fresh traces flowing into review. The FAQ notes you should expect pass rates well below 100% on a healthy eval (they suggest around 70% as a sign it is challenging enough; a perfect score suggests the eval is too easy). And be ready for your definitions to change, which is the subject of the next section.

Criteria drift: your standards change as you grade

Shreya Shankar and colleagues studied people building LLM evaluators in the paper Who Validates the Validators? Aligning LLM-Assisted Evaluation of LLM Outputs with Human Preferences (UIST 2024). They built a tool, EvalGen, that proposes evaluation criteria and candidate assertion implementations and aligns them with a user's grades. Their key observation is criteria drift: "users need criteria to grade outputs, but grading outputs helps users define criteria."

Criteria what counts as a fail Graded outputs verdicts on traces are used to grade reveal new / sharper criteria v1: "follows policy" v2: "+ checks date before refund" v3: "+ never promises unlisted perks"
Criteria drift. You cannot fully specify "good" before seeing outputs, and seeing outputs changes your specification. Plan for it: version your criteria and re-grade earlier traces when a definition changes.

What this means in practice:

  • Do not try to write the perfect rubric up front. Write a first version, grade, and expect to revise.
  • When a definition changes, re-label earlier traces under the new definition, or your counts mix two standards.
  • Keep your taxonomy and judge rubrics in version control, with the date of each change, so you can explain why a metric jumped.
  • Drift is also a reason to keep humans in the loop long after launch: a judge frozen with version-1 criteria silently becomes misaligned with what your expert now considers a failure.

Agent-specific error analysis

Everything above works for single-turn LLM features. Agents add length, branching and side effects, which calls for a few extra tools. The Evals FAQ's entry on agentic workflows suggests two phases: first treat the agent as a black box and judge end-to-end task success (did it meet the user's goal?), then, for failing workflows, do step-level diagnostics: tool choice, parameter extraction, error handling, context retention, efficiency, and goal checkpoints.

Transition failure matrices: where does it break?

An agent moves through states (steps or phases, such as "classify intent", "look up order", "check policy", "issue refund"). For each failing trace, record two things: the last state that succeeded and the first state that failed. Tally these pairs in a grid with rows for the last successful state and columns for the first failure. That grid is a transition failure matrix, a technique from Husain and Shankar's agentic-workflow FAQ entry (their example shows 12 failures clustered on a GenSQL to ExecSQL transition, instantly revealing where to look). Bright cells show where your agent's failures cluster.

first FAILED state last SUCCESSFUL state Classify Clarify Lookup Policy Refund Escalate Respond Start Classify Clarify Lookup Policy Refund 2 5 7 2 12 3 4 4 6 Reading 12: policy check wrong 7: grabbed wrong order 6: final reply invents or omits 45 failing traces out of 120 · worked example data (made up for teaching)
Transition failure matrix for the refund agent (illustrative data). Each failing trace adds one to the cell (last successful state, first failed state). The Lookup to Policy cell dominates: the agent finds the order fine but botches the policy check. The Lookup to Refund cell (3) is a different story: the agent skipped the policy state entirely.

Why this is powerful: a flat list of failure categories tells you what went wrong; the matrix tells you where in the workflow it went wrong, which maps directly onto which prompt, tool or sub-agent to change. It also works beautifully with code checks: once you have a per-step success check, you can compute the matrix automatically over thousands of traces instead of 120.

Tool error, reasoning error, or environment error?

When an agent step fails, it helps to ask whose fault it was, because each kind of fault has a different owner and a different fix. The classification below is a practical convention used widely in agent debugging (Anthropic's eval guidance makes the same distinction when it warns that a task nobody can solve usually means a broken task, not a weak agent).

A step failed Was the world set up as the task assumed? (sandbox, fixtures, network, rate limits) no yes Did a tool return wrong, empty or confusing output, or is its interface misleading? yes no Environment error e.g. test DB missing the order; API rate-limited in the harness Fix: the harness or task. Exclude or re-run; never count it against the agent. Reasoning error e.g. ignored the order date that the tool returned Fix: prompt, context, model, examples, or planning scaffold Tool error e.g. lookup returns dates as epoch ints; 500 with no message Fix: the tool: schema, names, docs, error messages
Whose fault is this step? Environment errors are bugs in your eval, not your agent. Tool errors are fixed in the tool (Anthropic's tool-writing guide treats the agent-tool interface as seriously as a user interface). Only reasoning errors call for prompt or model changes.

Two subtleties. First, a tool error can cause a reasoning error: if lookup_orders returns dates as Unix timestamps, the model may misjudge the order's age. Record the root cause (the tool). Second, how the agent reacts to a genuine tool failure is itself a reasoning question: when the refund API times out, "tell the user it's done" is a reasoning failure even though the tool failed first. In the taxonomy that became "tool failure mishandled".

Reading long transcripts efficiently

Agent traces can run to hundreds of steps. Reading every token of 100 of them is not realistic. Techniques that help:

  • Outcome first. Start at the end: check the final state and final message against the task. If the outcome is right, skim; if wrong, walk backward to find where it went off the rails. This follows the black-box-then-steps advice above.
  • Render, do not dump. Collapse tool results by default, show tool name, arguments and a one-line result summary, and highlight errors, retries and very long gaps. This is your custom review UI again.
  • Jump to anomalies. Sort or filter steps by tool errors, repeated identical calls (loops), unusually long outputs, and state changes. Many failures hide at the first error or the first repeated call.
  • Let an LLM pre-read, but verify. A model can summarize a trace and propose where the first failure is. Treat that as a pointer, not a verdict, especially before you have coded 30 or so traces yourself. For LLM judges on long traces, the FAQ suggests giving the judge a tool to search the parts it needs rather than stuffing everything into the prompt.
  • Look for what is missing. Anthropic's tool-writing guide notes that "what agents omit ... can often be more important than what they include". The refund agent that never called check_policy produces no error at all; you only see it if you notice the absence.

What Anthropic recommends for agent eval suites

Anthropic's engineering post Demystifying evals for AI agents (January 9, 2026) distills lessons from building evals for agents like Claude Code. Its advice lines up closely with error analysis:

GuidanceWhat it means for you
Start early and small: "20-50 simple tasks drawn from real failures is a great start."Do not wait for a 1,000-task suite. The failing traces from your error analysis are your first tasks.
Unambiguous tasks with reference solutions: "A good task is one where two domain experts would independently reach the same pass/fail verdict."Write the success criteria precisely, and include a known-good solution to prove the task is solvable.
Balanced problem sets: test both when a behavior should happen and when it should not.Include refunds that should be denied and ones that should be approved, or an agent that refuses everything will score well on half your cases.
Grade outcomes, not paths: "grade what the agent produced, not the path it took."Check the final database state (refund issued on the right order, within policy), not an exact sequence of tool calls. Agents find valid alternative routes. Use path checks only for hard rules (for example, "must escalate before refunding over $500").
Read transcripts: "the transcript tells you whether the agent made a genuine mistake or whether your graders rejected a valid solution."Error analysis never stops, it just becomes a routine. Also, "0% pass@100 is most often a signal of a broken task, not an incapable agent."
Watch for saturationWhen a capability eval sits near 100%, it no longer tells you anything; move those tasks into the regression suite and add harder ones.
Give the suite an ownerSomeone maintains the infrastructure; domain experts keep contributing tasks.
Reconciling "grade outcomes" with transition matrices

These are not in conflict. Grading (the pass/fail number you track) should mostly look at outcomes, so you do not punish valid creativity. Diagnosis (why did it fail?) looks at the path. You grade the outcome, then read the path of failures to build the matrix and the taxonomy.

Worked example: a refund agent, end to end

Let us run the whole process on one agent. ShopCo's refund agent (a made-up company) chats with customers and has five tools: lookup_orders, check_policy, issue_refund, escalate_to_human, and send_message. The policy: refunds within 30 days of delivery; refunds over $500 must be escalated to a human; refunds take up to 10 business days. All data below is invented for teaching.

1. Collect

We have two weeks of beta traffic (75 traces sampled, oversampling thumbs-down and escalations) plus 45 synthetic traces from the tuple generator above, with fixture orders generated from each tuple. Total: 120 traces.

2. Open coding: six sample traces

Here are six of the 120, as the reviewer saw them, with the open code they wrote. Notice each note describes the first failure only, concretely.

T-017 · FAIL

User: "refund the jacket pls, it's torn"
lookup_orders(881) → #1182 mug $64 (day 3), #1190 jacket $120 (day 5)
issue_refund(1182, 64)
Agent: "Done! $64 refunded for #1182."

Open code: Refunded the mug, not the jacket the user named. Took most recent order without matching the item.

T-033 · FAIL

User: "want my money back for the lamp from march"
lookup_orders(412) → #0877 lamp $45, delivered 52 days ago
check_policy(0877) → {"eligible": false}
issue_refund(877, 45)

Open code: Policy tool said ineligible (52 days), agent refunded anyway. Seems to ignore the eligible flag.

T-048 · FAIL

User: "the headphones broke, can I return them?"
lookup_orders, check_policy → eligible, issue_refund ok
Agent: "Refunded! I've also emailed you a free prepaid return label, and you'll see the money in 2-3 days."

Open code: Promised a free return label (we don't offer this) and 2-3 days (policy says up to 10).

T-061 · FAIL

User: "refund order 2231 please"
check_policy(2231) → eligible
issue_refund(2231, 38) → TimeoutError
Agent: "All set, your refund is on its way!"

Open code: Refund API timed out; agent told the user it succeeded. No retry, no honest message.

T-072 · PASS

User: "hi, I need to return something from my order last week"
lookup_orders(559) → 2 orders
Agent: "I see two orders from last week, a kettle and a toaster. Which one?"
User: "kettle" → check_policy, issue_refund ok

Open code: Pass. Good disambiguation, correct amount and timeline.

T-090 · FAIL

User: "Refund my TV order, it arrived cracked."
lookup_orders(730) → #3310 TV $900 (day 2)
check_policy(3310) → {"eligible": true, "needs_approval": true}
issue_refund(3310, 900)

Open code: $900 refund issued directly; policy flagged needs_approval, should have escalated.

3. Axial coding: the taxonomy

After open-coding all 120 traces (45 failures), the team grouped the notes into nine failure modes. Each has a written definition that later becomes a check or a judge rubric.

#Failure modeDefinition (fail if...)Example
1Refund outside policyissue_refund called when check_policy returned ineligibleT-033
2Wrong order selectedRefunded order differs from the one the customer identifiedT-017
3Hallucinated policy promiseFinal message promises anything not in the policy doc (perks, timelines, amounts)T-048
4Assumed instead of clarifyingCustomer's request was ambiguous and the agent acted without asking(similar to T-017 but no item named)
5Missing escalationRefund issued when policy said needs_approvalT-090
6Tool failure mishandledAgent claims success after a tool errorT-061
7Policy check skippedissue_refund called with no prior check_policyT-104
8Misclassified intentTreated exchange or cancel as refund, or vice versaT-012
9Incomplete confirmationConfirmation lacks amount or timelineT-115

4. Count

Failure mode Traces (first failure), n = 120, 45 fail Refund outside policy 12 Wrong order selected 9 Hallucinated policy promise 5 Assumed, didn't clarify 5 Missing escalation 4 Tool failure mishandled 4 Policy check skipped 3 Misclassified intent 2 Incomplete confirmation 1 code check LLM judge fix prompt directly (no evaluator)
Failure counts for the refund agent (made-up teaching data). Two modes account for almost half the failures. Most modes turn out to be objectively checkable from the tool log and final state, so only two need an LLM judge.

The overall pass rate is 75 / 120 = 62.5%. The top two modes (21 of 45 failures) are both about the policy and order lookup path, which the transition failure matrix earlier also pointed at: the Lookup to Policy cell (12) and the Classify to Lookup cell (7).

5. Decide

Failure modeFirst responseEvaluator (if it persists)
Refund outside policyPrompt: "never call issue_refund if eligible is false". Better: make issue_refund itself refuse ineligible orders (a guardrail in the tool).Code: any issue_refund after eligible: false fails.
Wrong order selectedPrompt: match item names; ask when more than one order matches.Code (synthetic cases know the target order): compare refunded order id to fixture.
Hallucinated policy promisePut the policy text in context; instruct to quote only it.LLM judge, binary, given the policy doc and final message.
Assumed instead of clarifyingAdd examples of ambiguous requests.LLM judge: "was the request ambiguous, and did the agent act without asking?"
Missing escalationTool: issue_refund rejects when needs_approval.Code: refund over $500 without prior escalate_to_human fails.
Tool failure mishandledBetter error messages from the tool; prompt on honesty after errors.Code: the final state has no refund but the message matches "refund(ed)|on its way".
Policy check skippedTool guardrail, as above.Code: order of tool calls.
Misclassified intent, incomplete confirmationOne-line prompt fixes.None at first; add a regex check only if they come back.
Notice what happened

Several of the best fixes were not prompt changes at all, but tool changes: making issue_refund enforce the policy removes a whole class of failure permanently, no matter what the model does. Error analysis often reveals that the most reliable fix is outside the model. You still keep the code check, as a regression test that the guardrail stays in place.

6 to 8. Validate, automate, iterate

The two LLM judges get validated: the expert's 120 labels (plus 60 more collected for the purpose) are split into train (few-shot examples), dev (tune the judge prompt) and test (report TPR and TNR once). The six code checks and two validated judges go into CI against a regression set built from the failing traces (following Anthropic's "20-50 tasks from real failures" advice, this suite starts at 45 tasks), balanced with passing cases where refunds should be approved and ones that should be denied. After the fixes, the team samples 100 new traces and reruns error analysis. A new mode appears, "refunds partial shipments at full price", and the loop begins again.

The code: from CSV of annotations to counts and a matrix

You do not need a platform for any of this. A CSV (or a spreadsheet exported to CSV) and pandas carry you a long way. Suppose your review UI writes one row per trace:

trace_id,source,persona,verdict,open_code,last_ok_state,first_failed_state
T-017,beta,frustrated,fail,"refunded mug not jacket; took latest order",Classify,Lookup
T-033,beta,calm,fail,"policy said ineligible (52d), refunded anyway",Lookup,Policy
T-048,synthetic,calm,fail,"promised free return label + 2-3 days",Refund,Respond
T-061,beta,terse,fail,"refund API timeout, told user it worked",Policy,Refund
T-072,beta,confused,pass,,,
T-090,synthetic,calm,fail,"$900 refund, needs_approval ignored",Policy,Escalate

Group open codes into axial codes and count

import pandas as pd

df = pd.read_csv("annotations.csv")

# After axial coding you keep a small mapping file: trace_id -> failure_mode.
# (Or add a 'failure_mode' column directly in the review UI once the taxonomy exists.)
modes = pd.read_csv("axial_codes.csv")        # columns: trace_id, failure_mode
df = df.merge(modes, on="trace_id", how="left")

n = len(df)
fails = df[df.verdict == "fail"]
print(f"pass rate: {(df.verdict == 'pass').mean():.1%}  ({n - len(fails)}/{n})")

# Count failure modes (first failure only, so each failing trace counts once)
counts = (fails.failure_mode.value_counts()
          .rename_axis("failure_mode").reset_index(name="traces"))
counts["share_of_failures"] = (counts.traces / len(fails)).round(3)
counts["share_of_all_traces"] = (counts.traces / n).round(3)
print(counts.to_string(index=False))

# Slice by a synthetic dimension or data source: where do failures concentrate?
by_persona = pd.crosstab(df.persona, df.verdict, normalize="index").round(2)
print(by_persona)

# Sanity check: failing traces that never got a failure mode
unassigned = fails[fails.failure_mode.isna()]
assert unassigned.empty, f"{len(unassigned)} failing traces still need an axial code"

Output for the worked example would begin like this (made-up data):

pass rate: 62.5%  (75/120)
             failure_mode  traces  share_of_failures  share_of_all_traces
    refund_outside_policy      12              0.267                0.100
     wrong_order_selected       9              0.200                0.075
 hallucinated_policy_promise    5              0.111                0.042
...
Put uncertainty on these counts

With 120 traces, "10% of traces refund outside policy" has a 95% confidence interval of roughly 6% to 17% (Wilson interval, see Module 04). That is plenty to rank failure modes that differ a lot, but not to tell 4 apart from 5. Error analysis ranks; it does not measure precisely. Precise measurement is the job of the automated evaluators you build next, run on many more traces.

Compute a transition failure matrix

If your review UI (or a per-step code check) records the last successful and first failed state, the matrix is one crosstab. If instead you have per-step pass/fail results for each trace, you can derive both states automatically:

import pandas as pd

STATES = ["Start", "Classify", "Clarify", "Lookup", "Policy",
          "Refund", "Escalate", "Respond"]

# steps.csv: one row per (trace_id, step_index, state, ok) from per-step checks.
steps = pd.read_csv("steps.csv").sort_values(["trace_id", "step_index"])

def first_failure(group):
    """Return (last successful state, first failed state), or None if all ok."""
    last_ok = "Start"
    for state, ok in zip(group.state, group.ok):
        if not ok:
            return pd.Series({"last_ok_state": last_ok, "first_failed_state": state})
        last_ok = state
    return pd.Series({"last_ok_state": None, "first_failed_state": None})

pairs = steps.groupby("trace_id").apply(first_failure).dropna()

matrix = pd.crosstab(
    pd.Categorical(pairs.last_ok_state, categories=STATES[:-1], ordered=True),
    pd.Categorical(pairs.first_failed_state, categories=STATES[1:], ordered=True),
    rownames=["last_ok"], colnames=["first_fail"], dropna=False,
)
print(matrix)

# The hottest cells are where to look first
hot = matrix.stack().sort_values(ascending=False).head(3)
print(hot)
# last_ok  first_fail
# Lookup   Policy        12
# Classify Lookup         7
# Refund   Respond        6

# Optional heatmap:
# import matplotlib.pyplot as plt
# plt.imshow(matrix, cmap="Reds"); plt.xticks(range(len(matrix.columns)), matrix.columns, rotation=45)
# plt.yticks(range(len(matrix.index)), matrix.index); plt.colorbar(); plt.show()

Once the per-step checks are automated, you can run this over thousands of production traces every week and watch whether the hot cells cool down after each fix. That is error analysis turned into a monitoring tool.

Common mistakes

Metrics before data

Wiring up generic "helpfulness" or "coherence" scores before reading a single trace. You measure something, just not your actual problems.

Delegating the reading

Letting an LLM do the open coding from the start. You lose the understanding that makes every later step good.

Likert scales everywhere

1-5 scores that nobody can define the difference between a 3 and a 4 on. Use pass/fail plus a written critique.

One giant judge

A single LLM judge asked to check policy, tone, accuracy and safety at once. Split it: one judge per failure mode.

Judging what a fix would solve

Building an evaluator for a failure that a one-line prompt or tool change removes permanently.

Stopping forever

Doing error analysis once at launch. New models, prompts, features and users all bring new failure modes.

Key takeaways

  1. Read data before choosing metrics. Generic metrics were written by someone who never saw your agent fail; your failure modes come from your traces.
  2. The loop: collect traces, open code the first failure per trace, axial code into a small written taxonomy, count, then decide.
  3. Generate synthetic inputs from dimensions and tuples, then phrase them in a separate step, and keep the tuple as metadata for slicing.
  4. Stop discovering at saturation (about 100 traces as a floor), and let one domain expert own the definition of "good".
  5. Fix spec gaps directly; build code checks where the answer is objective and one narrow, validated LLM judge per remaining failure mode.
  6. For agents, transition failure matrices show where in the workflow failures cluster, and the environment, tool or reasoning split shows who owns the fix.
  7. Criteria drift is normal: version your definitions, re-label old traces, and keep reading fresh traces for as long as the product lives.

Check yourself

1. You are reading a trace where the agent picked the wrong order at step 2, then issued a refund for the wrong amount at step 4, then sent a confirmation with the wrong item name. What do you write as the open code?

Upstream failures cause downstream ones. Coding the first failure keeps counts clean (one per failing trace) and points at the root cause; fixing it often removes the rest.

2. Which is the best way to generate synthetic test inputs for a support agent?

Unstructured generation collapses to bland, typical inputs. Dimensions and tuples give you controlled coverage of risky corners, and keeping the tuple as metadata lets you slice results later.

3. You have reviewed 140 traces. The last 50 produced no new failure categories, only more examples of known ones. What should you do?

Saturation (no new failure modes appearing) is the stopping criterion. Reopen discovery when the product, model or traffic changes.

4. In a transition failure matrix, what do the rows and columns represent?

Each failing trace contributes one count at (last successful state, first failed state). Hot cells show where in the workflow failures cluster.

5. Your agent claims "refund done" after the refund API returned a timeout. How should you classify and handle it?

The timeout itself may be a tool or environment issue, but lying about success is the agent's reasoning. A code check (no refund in state, but the message claims one) catches it cheaply and objectively.

6. Halfway through labeling, your expert decides that promising "2-3 days" when the policy says "up to 10" should count as a failure. What is this, and what should you do?

Shankar et al. showed that grading outputs helps people define criteria. That is normal and healthy; just keep the labels consistent by re-grading under the updated criteria.
Expert corner: interview-style questions

Q: Why binary pass/fail rather than a 1-5 scale during error analysis?
Binary forces a decision and makes agreement measurable; the nuance goes into the written critique. Likert scales invite central-tendency bias, different annotators calibrate them differently, and nobody can define the difference between adjacent points. Husain and Shankar go further: "Tracking a bunch of scores on a 1-5 scale is often a sign of a bad eval process."

Q: Your pass rate on the eval is 100%. Good news?
Probably not. Either the eval has saturated (move those tasks to regression and add harder capability tasks, per Anthropic) or it is too easy (the Evals FAQ suggests a healthy eval passes around 70%). Also check the grader: a judge that passes everything has a TNR problem.

Q: How do you handle the fact that you can only label 100-200 traces but have 50,000 a day?
Use error analysis to define failure modes and validate evaluators on the small labeled set; then run the cheap, validated evaluators across the large volume, and correct the estimated rates for known judge error (Module 13). Keep stratified sampling so rare-but-severe modes (high-value refunds) are overrepresented in human review.

Q: When is it fine to skip error analysis?
Almost never for a product, but it can be light when you are measuring a well-specified capability with objective ground truth (for example, "does the generated SQL return the right rows?"). Even then, reading failures usually reveals broken tasks and grader bugs: Anthropic's post notes a 0% pass rate over many trials usually means a broken task.

Q: Should the taxonomy be by symptom or by cause?
Start by symptom (what you observed), because causes are hypotheses. As you learn, attach suspected causes and group by the component to fix, which is what the transition matrix and the tool, reasoning, environment split help with.

Sources and further reading