AI agents

Feeding a Stopped Review Back Into the Eval Set

Antonio Evans10 min read

A single failed sample (represented as a red dot) is pulled from a review queue, converted into a labeled card, filed into a versioned folder of fixtures.
Key points

Key points

  1. 1Record the failing input, the exact context that produced it, the expected behavior and the reason it failed, then store all four in a versioned fixture file.
  2. 2A regression case is not a benchmark: it protects one specific failure from returning, while a benchmark compares systems.
  3. 3Assert on observable behavior (tool called, refusal given, citation present) rather than exact strings, because probabilistic systems phrase correct answers in many ways.
  4. 4Run the fixture on every candidate release inside the pull request gate, so a returning bug fails the build before the next sampling round.

Why sampling keeps rediscovering the same bug

Model evaluation usually starts as a sampling exercise. You draw a batch of traffic, run it through the candidate model, read the outputs, and file the bad ones. The statistical machinery behind that draw is well understood: to estimate a proportion within a given margin of error you compute a sample size from a confidence level and a z-score, and the formula is n = z² × p(1−p) / e² Explained in this sample size walkthrough. A commonly used calculator shows that estimating a proportion near 50% to within 5 percentage points at 95% confidence needs 384 responses, and tightening the margin to 3 points pushes the requirement past a thousand Sample Size - Population Proportion.

That arithmetic is fine for measuring a rate. It is terrible as a bug-catching mechanism, for one structural reason: sampling is memoryless. If a defect affects one input in two hundred, a fresh batch of two hundred has roughly a 37% chance of containing zero instances of it, and the next batch has the same chance all over again. Nothing about having reviewed the failure makes the next draw more likely to include it. Teams experience this as deja vu: the same malformed refund request, the same wrong-city booking, the same unescaped SQL table name, reviewed in April, reviewed again in July.

The fix is not a bigger sample. It is converting the reviewed failure into a deterministic test so detection stops depending on luck. Reviews of production incidents make the same point: the failure becomes a permanent regression test, added to a versioned dataset with defined expected behavior, run in the pull request gate so the issue cannot reappear silently turning a production failure into an evaluation case.

A worked number makes the difference concrete. Suppose you review 60 sampled outputs a week and find 6 real defects. Under pure sampling, each of those 6 is protected by a fresh draw each release; if a given defect occurs in 1% of traffic and the weekly sample is 400 items, expected detections per week are 4, but the probability of catching that specific defect in any one release is about 98% only if the draw includes it. As traffic grows and the sample stays at 400, that per-defect catch probability stays flat while the defect count keeps rising. Six new permanent cases per week, by contrast, compound: after a quarter you hold roughly 78 deterministic gates that never degrade and never need re-drawing.

Capture the full context, not just the prompt

A failing final answer is the least useful part of an incident. For agents and retrieval systems, the failure often lives in the execution path: the wrong tool was selected, an argument was malformed, the retrieved chunk was stale. Regression datasets built from production need the entire trajectory, including the user request and conversation history, the system prompt and its version, tool calls with inputs and outputs, retrieval results and which documents were not pulled, intermediate decision points, the final response, and metadata such as model version and session identifiers Arthur on regression datasets from production failures. If you store only input and output, the root cause disappears and the case becomes impossible to reproduce.

Before any of that becomes a fixture, it has to be sanitized. Strip names, emails, account numbers and any regulated data; remove credentials and keys; replace live database IDs and server URLs with deterministic placeholders or mocks; and capture whatever external state the failure depended on so it can be replayed. Where the original content is too sensitive to retain, rewrite it into a synthetic equivalent that preserves the failure pattern. A booking agent that confused Portland, Maine with Portland, Oregon does not need the real customer record; it needs a case asserting the agent asks a clarifying question when the destination is ambiguous Arthur.

Capture also needs a taxonomy, or the fixture set turns into an unlabeled pile. Tag each incident with a primary failure mode (wrong tool, bad retrieval, unsafe output, format violation, missing refusal) so similar issues cluster, priority becomes visible, and the dataset stays balanced across real weaknesses instead of being dominated by one loud category.

Define the expected behavior precisely

The labeling step is where most regression programs quietly fail, because "the answer should be better" is not a test. Behavior has to be specified in a form a machine can judge, or with a documented scoring rubric when only a human or a judge model can decide.

The decision rule that works: assert on what the system must do, not on how it says it.

Failure modeWeak assertion (avoid)Strong assertion (use)
Wrong tool selectedOutput text matches a golden stringTool refund_lookup called exactly once with a valid order ID
Ambiguous inputContains the word "clarify"No tool called; response asks a question naming both candidates
Unsafe requestDoes not contain the harmful phraseRefusal present, no tool call, policy category recorded
Missing groundingAnswer mentions a sourceEvery factual sentence carries a citation to a retrieved chunk
Format breakJSON parsesJSON parses and matches the schema with required keys
Stale retrievalAnswer text unchangedRetrieved chunk IDs do not include documents older than the cutoff
Over-long contextResponse is shortResponse complete within the token budget, with all constraints honored

Each row mixes deterministic assertions (tool name, schema, citation presence) with bounded checks where exact matching is impossible. The hybrid approach is standard advice: the scoring method matters less than the case being stable enough to fail again if the behavior returns NHI Mgmt Group.

Write the "reason it failed" field as prose aimed at a future maintainer. In six months someone will look at an odd-looking test and want to delete it. A one-paragraph note linking to the incident, naming the user harm, and stating the decision rule ("when a destination is ambiguous, ask; never guess") is what stops the guard from being removed because it looked annoying.

Store the case in a versioned fixture

A regression case is a code artifact. Keep it in the repository next to the prompts, model configuration and retrieval settings that produced the failure, and review changes to it like any other diff. Anything that lives only in a chat thread or an issue tracker never becomes executable, and therefore never blocks anything NHI Mgmt Group.

A minimal, readable format goes a long way. The example below keeps the case self-contained and cites its origin; the fields are the four the workflow needs plus provenance.

# fixtures/regressions/case-0142-refund-ambiguity.yaml
id: case-0142
created: incident-link-in-ticket
failure_mode: wrong_tool_selection
severity: high
origin:
  source: production_sample
  reviewed_by: support_lead
  reason: >
    Agent issued a refund for order 88213 when the user had asked about
    a different order in the same thread. Root cause: stale conversation
    history was dropped before the tool call.
input:
  messages:
    - role: user
      content: "cancel the second one and refund me"
  history_retained: true
context:
  system_prompt_version: "prompt-7"
  model_config: "config-b"
  retrieved_chunks: []
  tool_mocks:
    order_lookup: {returns: ["88213", "88214"]}
expected:
  tool_calls: []
  behavior: ask_clarifying_question
  must_mention: ["88213", "88214"]
  must_not: ["refund initiated"]
assertions:
  - kind: no_tool_called
    tool: refund_create
  - kind: contains_all
    values: ["88213", "88214"]
  - kind: llm_judge
    rubric: "asks which order the user means, without guessing"
    threshold: 0.8

Two habits keep the set healthy. First, give each case a stable ID and never renumber; reports and dashboards will reference it for years. Second, version the fixture with the artifacts it depends on, so a prompt change that intentionally alters behavior carries the corresponding fixture update in the same pull request. Testing research makes the same argument for ordinary code: test cases must co-evolve with production code, and obsolete tests that no one updates stop revealing faults and start adding maintenance cost Identify and Update Test Cases When Production Code Changes.

That co-evolution rule has a sharp edge for AI systems. When the fix is a policy change, not just an output change, the expected behavior must be signed off by the control owner before it is encoded. Otherwise a "bug fix" case quietly encodes a behavior nobody approved.

Wire the fixture into the release gate

The gate is the point of the whole exercise. A fixture that nobody runs is a document. The regression case should execute on every change to the prompt, the model, the retrieval index, the tool definitions, or the orchestration code, and it should fail the build when observed behavior departs from expected behavior.

The simplest wiring is a test that loads every fixture, replays it against the candidate configuration, and asserts:

# tests/test_regression_fixtures.py
import glob, yaml
from eval.harness import replay, assert_behavior

def test_all_regression_cases():
    cases = [yaml.safe_load(open(p)) for p in glob.glob("fixtures/regressions/*.yaml")]
    assert cases, "regression fixture directory is empty"
    for case in cases:
        result = replay(case)          # mocks tools, restores history, sets prompt version
        assert_behavior(case["assertions"], result)   # raises with case["id"] in message

Non-determinism complicates this, and the honest answer is that it is a statistical problem, not a testing-trick problem. An agent that passes today can fail the same case tomorrow. Three controls reduce flakiness to something manageable:

  1. Run each case several times and require a pass rate, not a single pass. A case failing 3 of 5 runs is a real failure; one failing 1 of 5 is noise worth watching.
  2. Separate deterministic assertions (schema, tool name, citation presence) from judged ones. Deterministic checks should pass every time; judged checks get a threshold.
  3. Keep the fixture small and trusted. Regression cases protect specific failures; broad benchmark suites are slower-moving and should not gate individual merges.

Gating on a pass rate also gives you a way to prove the fix before drawing another sample. The workflow is: fix, run the specific case N times, observe N of N passes, merge. The case now stands as the evidence that the failure was addressed, and the gate as the guarantee it will not return. When one component of a composed system is swapped, regression testing selection research shows the same logic: you test the modules that matter and can do so at a fraction of the cost of full revalidation, which is exactly why recording the dependency in the fixture pays off Regression Test Selection for Updated Capability Modules.

Two operational details matter more than they seem. First, run the gate where a failure is cheap: inside the pull request, not after a deploy. Second, record which model, prompt, retrieval configuration and data-pipeline version produced the result, so a regression failure can be attributed to a specific change rather than a vague "it got worse" Bluepes on AI testing and release checks.

Stop sampling what the fixture already covers

Once the gate is live, change how you sample. Draw batches to find new classes of failure, not to re-confirm known ones. Before drawing, filter out inputs that a fixture already covers (hash the normalized input plus the active prompt version) so review time goes to uncovered behavior. This is closer to how acceptance sampling treats a lot: if the sample result is inconclusive, a second stage is taken, and the plan is designed so the second stage only triggers when the first does not decide What is Double Sampling? (NIST/SEMATECH Engineering Statistics Handbook). The principle transfers: spend your review budget on decisions that are still open, not on lots you already decided.

The sample-size math also tells you where to spend effort. Estimating a rare failure rate is expensive: at 50% proportion, a 5-point margin costs 384 samples, and confidence climbs with every added decimal Sample Size Calculator. A 95% confidence interval for a proportion requires the standard error to be small relative to the estimate, and the width shrinks with the square root of n Confidence interval for a proportion. That means doubling sample size buys decreasing precision while doubling fixture count buys linear coverage of known failure modes at zero marginal variance. The fixtures are simply the better investment for defects you have already seen.

When you do draw, use a filter and a target. Keep the sample if you think a new failure class exists (new feature, new traffic source, model swap); skip routine sampling when only the fixture pass rate has moved. And keep a small holdout that fixtures cannot capture, because the long tail will always produce inputs no one wrote a test for.

A worked example, end to end

Suppose a weekly review of 400 sampled support-agent outputs surfaces six failures. One is the refund-ambiguity case above. Here is the loop, with numbers.

  1. Capture. Pull the full trace: two order IDs in history, the refund tool call fired against the wrong ID, the final message. Sanitize: no customer name, no payment details, mocked order lookup returning 88213 and 88214.
  2. Label. Expected behavior: ask which order, mention both IDs, call no refund tool. Failure mode: wrong_tool_selection. Reason field: stale history dropped before tool call.
  3. Store. Commit case-0142 to the fixture directory, linked to the incident. Reviewer signs off the requested behavior.
  4. Gate. The test runs on every prompt and model change. It fails now (the bug is live) and on any reintroduction.
  5. Prove. After the fix, run the case five times; require five passes. Merge. The case is now permanent.
  6. Adjust sampling. The 400-item weekly draw now filters cases covered by fixtures. The next review finds new failure modes rather than rediscovering refund ambiguity.

Six new permanent cases a week over a quarter is 78 gates. Assume each fixture costs 20 seconds per full release run; 78 cases add about 26 minutes to a release, which is cheaper than a single incident review. The economics favor fixtures as soon as a defect is likely to recur.

Where inference operations fit

None of this depends on which model you use, which is the point. The fixture suite should run against whatever candidate configuration is under review, and the models change constantly. That is a reason to standardize the access layer rather than the model: if every case replays through one endpoint, swapping the model under test is a configuration change, not a rewrite. Quanta Inference exposes text, image, video, audio and embedding models through a single OpenAI-compatible API key, so the harness holds one client and the regression run compares configurations cleanly.

The gate itself is a small piece of infrastructure. If you would rather have a specialist stand it up with you, Quanta takes a description of the site, app, document or plan and builds it with you.

FAQ

FAQ

How big should the regression fixture set be?

Start with the cases you have reviewed this quarter and let it grow by one to six per week. There is no universal target, because regression cases protect specific failures rather than estimate a rate; a few dozen well-chosen cases around your most consequential paths do more than hundreds of happy-path ones. Review the set quarterly and retire cases whose underlying feature or policy no longer exists.

Should I use exact string matching for expected outputs?

Almost never for the final answer, because the same correct behavior has many phrasings. Assert on structure and behavior instead: which tool was called, whether a refusal appeared, whether citations are present, and whether the JSON matches its schema. Reserve exact matching for fields that genuinely have one correct value, such as a status code or an order ID.

How do I handle a case that fails intermittently?

Run it several times per release and gate on a pass rate rather than a single run. A case failing three of five runs is a real regression; one failing one of five is worth watching but probably noise from sampling in the model itself. Record the run counts in the test output so drift in flakiness is visible over time.

What happens when the fix changes intended behavior?

That is a policy change, not a bug fix, and the fixture must be updated in the same pull request with sign-off from whoever owns the control. Never silently rewrite an expected value to make a build green. Keep the old case in history and add a new one that documents the new rule, so the change is traceable.

Do fixtures replace benchmarks?

No, they serve different purposes. A benchmark compares candidate systems against each other and moves slowly; a regression case pins one known failure and should run on every change. Use both: benchmarks to choose a model, fixtures to protect behavior after the choice is made. The fixture set is production infrastructure, not a one-off file.

Sources

Sources

  1. How To Calculate The Sample Size Given The Confidence ...
  2. Sample Size - Population Proportion
  3. What happens when a production LLM failure is turned into ...
  4. AI Agent Regression Testing From Production Failures | Arthur
  5. Identify and Update Test Cases When Production Code ...
  6. Regression Test Selection for Updated Capability Modules ...
  7. AI testing in EdTech: regression and release checks
  8. Sample Size Calculator
  9. Confidence interval for a proportion
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles