Eval-Driven Development for LLM Systems
Evaluation-driven development with error analysis, golden sets, layered checks, and CI gates.
LLM application regressions often appear as distribution shifts rather than deterministic failures. A model upgrade may alter tool calls, a prompt edit may break one query class, and a retriever change may shift the supplied context. A versioned eval suite makes these changes visible before promotion.
Treat evals as the release contract
Eval-driven development treats a versioned eval suite as the release contract for an LLM application. The suite grows from failures found in real traces because the input distribution is too broad to specify in advance. It measures quality together with cost and latency, so an expensive quality gain can still count as a regression. A deterministic slice runs on every pull request, while slower LLM-judged checks run nightly or before release. Both enforce explicit promotion thresholds.
A note on the name. “Eval-driven development” gestures at TDD, and that gesture is partly misleading. Hamel Husain and Shreya Shankar argue strongly that pure write-evals-first does not work for LLM systems, because the surface area is unbounded and you cannot enumerate failure modes from a spec sheet. The right loop is error analysis first, evals second; look at traces, categorize what is actually breaking, then write an eval that catches the category. The TDD shape (write the failing test, fix the bug, commit both together) survives; the “imagine all the failures” shape does not. Call it eval-driven development as long as you remember which half of the analogy holds.
Start with error analysis
The loop that produces a useful eval suite is concrete enough to write down. Five steps, repeated weekly during early development, monthly once the system stabilizes:
- Capture traces: Log every model call with its inputs, retrieved context, output, latency, token counts, and cost. Sample randomly or stratify by query type. The production tracing layer covers span shape, OTel GenAI semantic conventions, sampling, and platform choices. Evals only require that the traces are queryable.
- Open-coded review. A single domain expert (not three) reads 50–100 traces back-to-back and writes free-text notes about every problem they see. No category schema yet. This is the “journaling” pass Hamel and Shreya borrow from qualitative research; the goal is to surface failure modes you didn’t know existed.
- Axial coding. Cluster the open-coded notes into categories: “wrong product name,” “ignored the prior turn,” “output JSON missed a required field,” “took the long path through the tool call graph.” Most categories will be small; a few will dominate the volume. The categories are now your taxonomy.
- Write the eval row. For each category, pick the cheapest assertion that catches it. Schema validation for the JSON case. Substring match for the product-name case. Tool-call-sequence assertion for the wrong-path case. LLM-as-judge with a specific rubric for the “ignored prior turn” case where no cheaper assertion exists. Add a row to the suite for each.
- Wire to CI and watch the trend. The deterministic rows gate the next merge. The judged rows trend on the dashboard. When a new failure shows up in production, return to step 1 with that trace.
The trap that kills internal evals is going straight from “we want quality” to “let’s write a faithfulness judge” without the open-coded pass. You end up with a metric that measures a generic property nobody on the team can characterize, scores in the 0.7–0.85 band on every change, and has no relationship to the bugs your users actually report. Error analysis grounds the eval in the system’s actual failure distribution.
Combine golden sets with live evaluation
The eval inputs come from two places, and both are necessary. The golden set is a frozen, curated list of inputs; typically 50–500; checked into version control, never modified except to add new rows or deprecate old ones. It’s the regression unit. Every change to the system is compared against the suite’s previous score on exactly the same inputs. Without this, you cannot tell whether a metric movement is system change or input change. The golden set is small enough to be hand-curated and stable enough to be a contract.
Live evals sample real production traffic, redact PII, and score the sample asynchronously. They catch distribution drift; the kinds of queries users started sending after Wednesday’s marketing push; that the golden set, frozen six months ago, doesn’t represent. The dashboard shows both: golden score (changes only when the system changes) and live score (changes when the system or the input distribution changes). The gap between them is the distribution drift signal.
A practical rule for golden-set construction: every row should map to a specific category from your error analysis. A row that exists “for coverage” of some imagined edge case is a row you can’t interpret when it regresses. The NurtureBoss case study in Hamel’s evals piece is the canonical worked example; they grew their golden set by mining the cases their date-handling logic had previously failed on, not by brainstorming dates a user might type.
Measure cost and latency with quality
A common failure mode is to track quality alone, leaving cost and latency in a separate dashboard nobody opens. The day you ship a model upgrade that improves faithfulness by three points and triples per-call cost, the quality-only dashboard cheers and the finance team panics. Treat each eval row as emitting three numbers; quality score, p50 token cost, p95 latency; and require the suite’s release-gating threshold to include all three. The right framing is the Pareto frontier: a change that moves quality up and cost down is unambiguous; a change that moves only one and worsens the other is a judgment call that has to happen on the PR, not after deploy. This is the same idea the prompt-caching article develops for the inference layer and the RAG-evaluation article develops for retrieval pipelines.
Code: a minimal Python eval harness
The simplest working harness is a couple hundred lines of Python that ingests a golden set, runs the system under test, evaluates each row with a mix of deterministic and judged checks, and writes a result row per commit SHA. Install: pip install anthropic pydantic (and your test runner; pytest works fine).
| |
The harness is deliberately minimal so the shape stays visible. Three things to flag. The cheap-first ladder; judged checks only run when deterministic checks pass; a row that fails schema validation doesn’t pay for a judge call. per-category breakdown; the aggregate is for the dashboard, the per-category numbers are for action. The cost and latency rollups are first-class outputs, not afterthoughts. A CI script wraps this with a pass/fail decision: result["pass_rate"] >= 0.95 and result["judge_mean"] >= 0.80 and result["p95_latency_ms"] <= 3000.
A worked row that exercises every assertion type:
| |
Further reading
- Hamel Husain, Your AI Product Needs Evals; an error-analysis-first approach with the NurtureBoss case study and LLM-as-judge guidance.
- Hamel Husain & Shreya Shankar; LLM Evals FAQ; the long-form Q&A version, updated through 2026, with extended treatment of error analysis, judge alignment, and why “write evals first” is the wrong instinct.
- Eugene Yan; Task-Specific LLM Evals That Do (and Don’t) Work; the practitioner’s take on which evals are worth building and which are vanity. Pairs well with Eugene’s broader LLM patterns piece on where evals fit in the larger stack.
- Anthropic; Demystifying Evals for AI Agents; Anthropic’s engineering blog walks through eval design for agent systems specifically, with worked examples from their own product surface.
- Shankar et al.; Who Validates the Validators?; the academic paper that introduced evaluation criteria drift and the iterative-rubric methodology that underpins much of the current practice.