AI agents

Tool Testing: How to Simulate Agent Calls in Staging

Antonio Evans10 min read

A split staging dashboard: on the left, a list of replayed conversation traces with pass or fail marks next to tool calls; on the right, a fuzzed JSON argument payload with a red error icon and a successful retry arrow.
Key points

Key points

  1. 1Replaying recorded conversations through staging with the current tool schema tells you whether tool selection, argument validity, and recovery changed before users find out.
  2. 2Fuzzing tool inputs (wrong types, missing fields, oversized payloads, plausible-but-invalid enums) surfaces failures that normal happy-path traces never exercise.
  3. 3Label the expected tool for each turn, then gate releases on selection accuracy, schema-pass rate, and recovery rate rather than on a single pass/fail suite.
  4. 4Agents are non-deterministic, so one green run proves little; run each case several times and judge the distribution.

Why replay beats hand-written test prompts

A tool is a contract between deterministic code and a non-deterministic model. Anthropic's engineering team frames it this way: a function like getWeather("NYC") behaves identically on every call, but an agent asked "Should I bring an umbrella today?" might call the weather tool, answer from general knowledge, or ask a clarifying question first (Anthropic). That variability is exactly why a handful of hand-written prompts from a demo notebook is not a test suite. It samples maybe ten plausible paths out of the thousands your users actually take.

Recorded production conversations have two properties synthetic prompts lack. First, they carry the messy context that precedes a tool call: three turns of clarification, a pasted error message, a half-finished sentence. Second, they carry the distribution of intents your real users have, including the long tail. The traffic-replay discipline from classical load testing applies directly: capture real requests, sanitize them, replay them against pre-production, and compare behavior to a baseline (Speedscale). Keploy's write-up of the same idea reports that synthetic staging data covered only around 40 percent of production behavior in their experience, versus about 95 percent with replayed traffic (Keploy).

For agents, the unit of replay is a turn, not a request. You do not need the whole HTTP envelope. You need the message history up to the point where the model chose a tool, and you need the record of which tool it chose and with what arguments.

What to capture per turn

FieldWhy it matters
Full message history (system, user, assistant, tool results)Selection depends on accumulated context, not the last user message alone
Tool call name and arguments as issuedThe baseline you will compare against
Tool result returned to the modelNeeded to simulate the next turn
Final model outputLets you check whether a different tool choice changed the answer
Model identifier and temperature usedA replay against a different model or sampling setting is a different experiment

Strip personally identifiable data before storing traces, and store the tool result alongside the call so you can drive the next turn without re-executing side-effectful tools.

Labeling the expected tool for each turn

Replay without ground truth measures nothing. For every captured turn you need an expected outcome, and it is not always a single tool name. Use one of four labels:

  • Correct tool plus required arguments. The canonical case.
  • Acceptable set. Several tools are defensible (a general search_knowledge_base versus a narrower lookup_policy_by_id). Store a set, score any member as correct.
  • No tool. The right answer is a clarifying question or a direct answer. These cases matter because agents over-call tools as often as they under-call them.
  • Must fail safely. Requests that should be refused, escalated, or answered with an error the user can act on.

Producing these labels is the expensive part. Three techniques reduce the cost. First, have a strong model propose a label for each turn and have a human review only the disagreements and a random sample. Second, reuse the trace's own tool call as the label when a downstream check confirms the call produced a correct final answer; that is a weak label but a cheap one. Third, mine recovery cases from incidents: any turn followed by a tool error and then a successful retry is a labeled recovery example for free.

Set a target before you start labeling. If you have 400 captured turns and your selection accuracy is somewhere in the high 80s, the confidence interval on that estimate is roughly plus or minus 3 points at 95 percent confidence, which is tight enough to detect a regression of 5 points. With 50 cases the interval is wider than any change you care about, so invest in volume before you tune thresholds.

Building the replay harness

The harness replays a conversation prefix, forces tool choice to auto, records what the model does, and scores it. Providers differ in how strict they can be: Cohere's strict_tools mode guarantees that generated tool calls follow the schema, with no hallucinated names or parameters and every required field present (Cohere). That does not remove the need for replay, because strictness constrains the shape of a call, not whether the right tool was chosen or whether the values are correct for the user's request.

A minimal replay loop looks like this:

def replay(case, tools, client, runs=3):
    calls = []
    for _ in range(runs):
        resp = client.chat(messages=case.history, tools=tools, tool_choice="auto")
        calls.append(extract_tool_call(resp))
    return score(case, calls)

Score in three independent dimensions rather than one pass/fail:

  1. Selection: did the model pick a tool in the acceptable set, or correctly call none?
  2. Arguments: do the arguments validate against the JSON schema, including required fields, types, and enum membership?
  3. Outcome: given the recorded tool result, does the final answer match the recorded answer on the facts you care about (order number, refund amount, date)?

Run each case at least three times. Sampling variance means a single pass per case will show flaky "regressions" every release. Report per-case pass rates, not just suite totals, so a case that swings between pass and fail is visible rather than averaged away.

What each dimension catches

DimensionTypical failureFix it points to
SelectionAgent picks search_contacts when it meant get_contact_by_emailNames and descriptions that overlap
Arguments"42" passed where an integer was expected; enum value outside the allowed setTighter schema, enums, format hints
OutcomeCorrect tool, wrong values, confidently wrong answerParameter descriptions, examples in the description
RecoveryLoops on the same failing callActionable error messages from the tool

Argument-level failures are the most common and the most fixable. Analysis of tool design failures notes that a parameter called user invites a name, an email, or an integer ID, and the model will guess; renaming it user_id removes the ambiguity (AI/TLDR). The same source describes Anthropic's finding that requiring absolute file paths eliminated a whole class of model errors, because the model stopped guessing the working directory.

Fuzzing tool inputs and tool definitions

Replay only covers inputs users actually produced. Fuzzing covers the inputs they will produce next month. Classic fuzzers are a poor fit here: the ToolFuzz authors report that running American Fuzzy Lop against an agent tool for several hours failed to find a single example that realistically represents an agent scenario, because random strings do not look like natural-language queries (ToolFuzz paper). Their method instead uses an LLM to generate queries that are natural but aimed at breaking the tool, combining fuzzing with model-based generation.

The published numbers are worth internalizing. ToolFuzz found 20 times more erroneous inputs than two prompt-engineering baselines, and in the expanded evaluation it identified 50 percent more unique errors while cutting the false discovery rate by 4.5 times. It found that all of the LangChain tools tested and the majority of Composio tools were erroneous (ToolFuzz, ICLR submission). Documentation, not model capability, was the dominant failure source.

You can build a useful fuzz layer without reproducing the paper. Generate four families of cases per tool:

  • Type confusion: pass a string where an integer is declared, a list where a string is declared, null for a required field.
  • Constraint violations: enum values that are plausible but wrong ("done" instead of "completed"), dates in the past when the schema requires a future date, strings below a minimum length.
  • Oversized payloads: a query that returns thousands of rows, a document far larger than the context can absorb. Design analysis notes that an agent calling list_all_records and receiving 800 rows has burned most of its context window on data it will never use (AI/TLDR).
  • Ambiguity probes: two tools whose descriptions overlap, with a query that could justify either. If the model splits 50/50 across runs, the descriptions are the problem.

Then feed the failing arguments back through the model rather than asserting directly. The question is not "does my validator reject a string where an integer belongs," which unit tests already answer, but "when the model's call is rejected, does it retry with a corrected value, or does it loop or give up."

Testing recovery, not just success

Recovery is the dimension most teams skip and the one users notice. The failure mode is well documented: a 400 Bad Request with {"error": "invalid input"} is fine for a human who can read docs or try again manually, but an agent has none of those fallbacks, and the best case is that it halts while the worst is that it retries the same broken payload indefinitely (Szymon Pacholski). The fix is error messages that are prescriptive: what failed, why, and how to fix it, including the allowed values and a hint.

Test this by injecting failures in staging rather than waiting for them. For each tool that can fail, create scripted variants of the same trace:

  • Return a validation error listing allowed values.
  • Return a 404 for a plausible-but-absent identifier ("City 'Tokyoo' not found").
  • Return a rate-limit error with a retry-after hint.
  • Return a timeout with no detail.
  • Return a partial success.

Then measure recovery rate: the fraction of injected failures where the agent's next action is a corrected retry, an alternative tool, or a clear message to the user, within a bounded number of steps. Cap the loop. An agent that eventually succeeds after 40 tool calls has recovered in a technical sense and failed in every practical one.

Two design patterns make recovery measurable. The first is an explicit failure-reporting tool so the agent has a structured exit path instead of looping (Pristren). The second is validation in the harness rather than the prompt: the runtime checks whether the result is empty, malformed, or out of range, and decides to retry or inject a corrective message (Pristren). A production write-up argues the same point bluntly, that the harness accounts for the bulk of agent reliability while validation logic, permission checks, and retry handling all live in orchestration code, not inside the LLM prompt (MLflow).

Recovery checklist

CheckPass condition
Error message names the offending fieldPresent in tool response
Error message lists allowed valuesPresent for enum-like fields
Agent retries with corrected argumentWithin 2 attempts on at least 80 percent of injected cases
Agent switches to an alternative tool when appropriateObserved in at least one case per tool pair
Agent escalates or answers honestly after bounded retriesNo unbounded loops
Destructive operations never auto-retriedVerified by trace inspection

That last row matters. A retry on issue_refund after a timeout is a double refund. Tools should be designed to be retry-safe, idempotent where possible, with explicit failure states, and write, delete, and financial operations should sit behind approval gates rather than being retried by the model (MLflow).

From results to a release gate

Turn the three dimensions into release criteria you can defend in a review. A workable gate looks like this:

  1. Selection accuracy on the labeled replay set must be at or above the last release, and no single high-traffic intent may drop by more than a set margin.
  2. Schema-pass rate on generated arguments must be 100 percent for required fields and types when the provider supports strict mode; otherwise, track it and treat a drop as a blocking regression.
  3. Recovery rate on injected failures must meet its floor, and unbounded-loop cases must be zero.
  4. Stability: cases that pass on one run and fail on another are counted separately and reported; an increase in flaky cases is a regression even if the mean holds.

When the gate fails, resist the urge to reword prompt instructions first. The observed evidence usually points at the tool interface. Vague names, overlapping descriptions, and loose schemas produce failures that a stronger model only partially masks (Machine Learning Mastery). Anthropic's guidance is to build an evaluation, then iterate the tool against it, using an agent to help analyze results and propose improvements (Anthropic). OpenAI's practical guide takes the same shape: set up evals to establish a baseline, meet the accuracy target with the strongest available model, then swap in smaller models and confirm they hold the target (OpenAI). Your replay suite is that baseline, and it is what lets you switch models on cost grounds without guessing.

Keep the suite honest with two habits. First, refresh traces on a schedule so the eval reflects current user behavior, and retire cases for flows you no longer support. Second, hold back a portion of freshly captured turns from tuning; if you optimize tool descriptions against every case you have, you will eventually fit the test set. A held-back slice, replayed only at release time, tells you whether improvements generalize.

If you are running these suites across several models to pick a cost and quality point, a single gateway helps: Quanta Inference (https://quantaintelligence.ai/inference) exposes text models behind one OpenAI-compatible key, so the same harness can replay a suite against each candidate without rewriting clients. And if the work of building and maintaining the labeled set, the harness, and the gate is more than your team has bandwidth for, Quanta Experts (https://quantaintelligence.ai/agents) hands the whole evaluation loop to a specialist who returns finished deliverables.

FAQ

FAQ

How many replayed conversations do I need before the results mean anything?

Start with enough cases to detect the regression size you care about. At roughly 400 labeled turns, the 95 percent confidence interval around a selection accuracy in the high 80s is about plus or minus 3 points, which is tight enough to catch a 5-point drop. Fewer than 100 cases will produce swings larger than any change you are trying to measure.

Should I test the model or the tool definition?

Both, but attribute failures carefully. When a replayed case fails, check whether the arguments validated and whether the expected tool was in an acceptable set. Schema violations and wrong-tool choices usually trace back to names, descriptions, and parameter definitions, while recovery failures usually trace to error message quality. Fix the interface before changing prompts.

How do I test tools that have side effects, like refunds or emails?

Record the tool result during capture and replay it as a fixture instead of re-executing the tool. For recovery tests, script the failure variants (timeout, validation error, rate limit) rather than inducing them in a live system. Destructive and financial operations should never be auto-retried in staging either, so keep approval gates active during replay.

What is the difference between replaying traffic and fuzzing tool inputs?

Replay covers inputs real users produced and checks whether current behavior matches the recorded behavior. Fuzzing generates inputs users have not produced yet, including malformed ones, to find where the tool documentation or schema breaks down. The ToolFuzz results suggest fuzzing finds many more erroneous inputs than prompt-based probing, so treat the two as complementary layers rather than alternatives (ToolFuzz paper).

How do I stop flaky replay results from blocking every release?

Run each case multiple times and score per-case pass rates instead of single outcomes, then set gates on the distribution (for example, at least two of three runs passing) and track the count of unstable cases as its own metric. Reduce temperature where the provider allows it to cut variance, and investigate any case that flips repeatedly, since instability usually signals an ambiguous tool description rather than bad luck.

Sources

Sources

  1. Writing effective tools for AI agents, using AI agents - Anthropic
  2. Traffic Replay: Production Without Production Risk
  3. Environment Replay - Real Production Traffic, Safely ...
  4. Parameter types for tool use (function calling) - Cohere Documentation
  5. Designing Tools for LLM Agents: Best Practices | AI/TLDR
  6. ToolFuzz - Automated Agent Tool Testing
  7. ToolFuzz: Automated Agent Tool Testing
  8. Error Handling for AI Agents: A Different Standard Than Classic Apps
  9. Tool Use in LLMs: Design Patterns for Reliable Agent Actions - Pristren
  10. AI Agent Tool Use Best Practices for Practitioners - MLflow
  11. AI Agent Tool Design: What Works and What Doesn't
  12. A Practical Guide to Building AI Agents - OpenAI
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