AI agents

Scoring Tool-Call Accuracy, Not Just Final Answers

Antonio Evans8 min read

A clean dashboard view: top panel shows a JSON fixture block with fields tool, arguments, turn and result.
Key points

Key points

  1. 1A tool call can be wrong even when the final answer looks right, because the model can reach a plausible answer through a skipped, misordered or mistyped action.
  2. 2A fixture should record each invocation as tool name, arguments, result and turn index so the run can be replayed and diffed.
  3. 3Score tool name, argument values and sequence position separately, then attribute each failed task to one dominant failure mode.
  4. 4Report the failure mix per model, because the fix differs: wrong tool points at routing and prompt, wrong arguments at schema and examples, wrong order at planning.

Why answer-only scoring hides agent failures

An agent that picks the right function 90% of the time can still fail a long task most of the time, because errors compound across steps: get the fourth call wrong in a twelve-step sequence and everything after it rests on a bad result as Splunk's agent evaluation writeup puts it. That compounding is the whole argument for scoring traces instead of final answers. A refund agent that looks up the order and then issues the refund without waiting for the result may still end with the customer saying "thanks," and an answer-only check will mark that run as a pass.

NVIDIA's evaluation guidance makes the same split: step-level process scoring identifies where chains break, while end-to-end outcome scoring verifies whether the final environment state matches the goal in its agent evaluation post. You need both, but most teams only build the second. The tool-call layer is where the diagnosis lives.

There is a second reason to record calls: structured outputs only guarantee shape. Schema-constrained generation ensures syntactic validity and reduces parse errors, but does not ensure value correctness per MLflow's structured outputs guide. A tool call that validates against its JSON Schema can still carry the wrong customer ID. Validation is a gate, not a grade.

The fixture format: one record per invocation

A fixture is a recorded run with enough detail to replay and score it. Resist the urge to store only the final answer. The useful unit is the invocation, and the second useful unit is the task, which groups invocations into a run.

{
  "task_id": "refund-partial-014",
  "input": "Customer says order 4821 arrived damaged and wants a partial refund.",
  "allowed_tools": ["lookup_order", "check_refund_policy", "issue_refund", "send_email"],
  "expected_invocations": [
    {"turn": 1, "tool": "lookup_order", "args": {"order_id": "4821"}},
    {"turn": 2, "tool": "check_refund_policy", "args": {"order_id": "4821", "reason": "damaged"}},
    {"turn": 3, "tool": "issue_refund", "args": {"order_id": "4821", "amount": 24.50}},
    {"turn": 4, "tool": "send_email", "args": {"template": "refund_confirmation", "order_id": "4821"}}
  ],
  "actual_invocations": [
    {"turn": 1, "tool": "lookup_order", "args": {"order_id": "4821"}, "result": {"status": "delivered", "total": 49.00}},
    {"turn": 2, "tool": "issue_refund", "args": {"order_id": "4821", "amount": 49.00}, "result": {"ok": true}},
    {"turn": 3, "tool": "send_email", "args": {"template": "refund_confirmation", "order_id": "4821"}, "result": {"ok": true}}
  ],
  "final_answer": "I've processed your refund."
}

Four fields do the work. Tool name supports routing scores. Arguments support value scores. Result captures what the environment returned, which matters for detecting ignored errors and for making the fixture replayable. Turn index supports order scores and lets you compare two runs of the same task position by position.

Two design choices save pain later. First, keep expected_invocations as a sequence of steps with alternatives where multiple valid paths exist, rather than one rigid script; τ²-bench grades the final database state rather than individual calls precisely because several call paths can reach the same goal per Splunk's summary of the benchmark. Second, store the result your mock tool returned, not the result the real system would return, so replay is deterministic.

If your harness already validates tool arguments with JSON Schema, reuse that machinery for fixtures. The Python jsonschema library raises a ValidationError whose validator field names the failed keyword and whose path and json_path locate the failing value documented in the jsonschema error guide. That gives you free attribution at field level: an argument failure on amount with validator type is a different bug from one with validator minimum.

Scoring: separate wrong tool, wrong arguments and wrong order

Score three dimensions per task, then roll up.

Tool selection score. For each expected step, check whether the recorded tool name matches any allowed name for that step. Count matches over matched steps.

Argument score. For steps where the tool name matches, compare arguments field by field. Prefer exact equality on scalar fields and set equality on arrays. For free-text fields, define an explicit normalizer (trim, lowercase, strip punctuation) and version it alongside the fixture, because a silently changed normalizer shows up as a phantom regression.

Order score. Compute the longest common subsequence between expected and actual tool-name sequences. Divide by expected length. This tolerates a missing step without cascading every later step into a failure, which a strict positional comparison does not.

Then attribute the task to one failure mode. Use a fixed precedence so two engineers scoring the same run reach the same verdict:

Failure modeTestWhat it usually meansFirst fix to try
Wrong toolA step's tool name is not in the allowed set for that stepRouting, tool descriptions, or too many similar toolsRewrite tool descriptions, remove overlapping tools
Wrong argumentsTool name matches but a required field is missing or the wrong type or valueSchema, enum coverage, examples in the promptTighten schema, add enums, add worked examples
Wrong orderAll names appear somewhere but the sequence divergesPlanning, missing dependency checksAdd explicit ordering rules and a wait-for-result step
Extra callAn invocation appears that has no expected counterpartLooping, redundant lookupsAdd a call budget and dedupe instructions
Missing callAn expected step never happens and no incorrect step replaces itEarly stop, premature answerRequire a terminal tool before answering

The precedence rule: if any step has a wrong tool, the task is a wrong-tool failure, even if it also has argument errors, because the argument comparison was never meaningful. If tool names all resolve but order diverges, mark it wrong-order before scoring arguments as a secondary metric. Record the secondary metrics anyway; they tell you whether fixing routing will leave you with an argument problem underneath.

Here is the comparator in a compact form. It is deliberately boring code, because an evaluation harness that is hard to read will not be maintained.

def score_task(expected, actual, allowed_by_turn):
    name_hits = sum(
        1 for e, a in zip(expected, actual)
        if a["tool"] in allowed_by_turn.get(e["turn"], {e["tool"]})
    )
    arg_errors = []
    for e, a in zip(expected, actual):
        if a["tool"] != e["tool"]:
            continue
        for key, want in e["args"].items():
            got = a["args"].get(key)
            if got != want:
                arg_errors.append({"turn": e["turn"], "field": key, "want": want, "got": got})
    order_score = lcs_len([e["tool"] for e in expected], [a["tool"] for a in actual]) / len(expected)
    mode = classify(name_hits, arg_errors, order_score, len(expected), len(actual))
    return {"tool_score": name_hits / len(expected), "arg_errors": arg_errors,
            "order_score": order_score, "failure_mode": mode}

Note the trap: zip silently truncates to the shorter list, so a run with four expected steps and two actual steps scores a perfect tool name rate on the two it attempted. Handle length mismatch before scoring, and count the missing steps explicitly. This is the single most common way a homemade harness flatters a model.

Running the suite: repeats, consistency and cost

One attempt per task is not a reliability measurement. Splunk's evaluation piece points out that public benchmark scores often overstate reliability because they rely on single-attempt success, and argues for pass^k style consistency metrics as a better predictor of production behavior in the same tool-calling article. The practical version: run each task k times, report the fraction of tasks that passed all k runs, and report the per-model failure-mode mix across all runs.

A worked example with numbers, all illustrative. Take 120 fixtures, run each 5 times, so 600 runs per model, and score them with the comparator above.

Model (example labels)Task pass rate, all 5 runsWrong toolWrong argsWrong orderExtra or missing call
Model A71%6%14%7%2%
Model B68%19%6%5%2%
Model C63%4%8%22%3%

Read the table as a decision rule, not a leaderboard. Model A and Model B are within three points on the headline number, but they need opposite fixes. Model A's dominant mode is wrong arguments, so the work is schema and examples: tighten field types, add enums for template, put one corrected example per tricky tool in the prompt. Model B's dominant mode is wrong tool, so the work is routing: shorten and disambiguate tool descriptions, remove tools that overlap, and check that the tool list itself is not the problem. Model C needs planning help, which is usually a prompt and loop-structure change, not a schema change.

For scale, the cost differences between models on a full function-calling benchmark run can be large: Splunk reports a full BFCL run costing $86.55 on one model and $298.47 on another, within eight points of each other on score in the same article. If you are running 600 runs per candidate model against a live API, prototype with fixtures and mocks, then confirm on a small live sample. Pin one API layer across models so the cost comparison is real; Quanta Inference gives you text and tool-capable models behind one OpenAI-compatible key, prepaid and priced in dollars, at https://quantaintelligence.ai/inference.

Feeding failures back: retries that teach the model something

A plain retry is the same prompt at the same temperature, roughly the same odds, as one practitioner write-up on retry loops puts it in this validation retry pattern. Feed the validation error and the model's own bad output back into the next prompt and ask it to fix that named thing. The write-up notes the tradeoff: it costs an extra call on a bad response and only works when the bad output is parseable enough to serialize back into the prompt, so cap attempts.

Apply the same idea to tool calls, with one addition: quote the schema error path, not just the message. The jsonschema error object carries validator, validator_value and json_path, so you can say "field amount failed type: expected number, got string" instead of pasting a wall of prose per the jsonschema error documentation. Models patch named, concrete mistakes better than opaque ones.

Watch for validator drift. A schema that passed for weeks can start failing without any change on your side, as one team documented when their structured output schema began failing validation overnight in this OpenAI community report. Keep the schema in version control, run a tiny canary fixture set on every deploy of your harness, and treat a sudden jump in argument failures as a possible validator or API change before you blame the model. The community thread is a user report, not an official changelog, so the operational takeaway is detection and versioning, not a claim about what changed.

A checklist for a trace-based suite you can maintain

Build in this order. Each item has a failure it prevents.

  1. Write ten fixtures by hand from real traces. Not synthetic prompts. Real traces carry the ambiguity that synthetic ones miss. Target the tools where support tickets already come from.
  2. Freeze the mock tools. Every fixture records the result each mock returned, so replays are deterministic and a score change means a model or prompt change.
  3. Version the normalizers. Argument comparison rules belong in code with a version string; log the version in every report row.
  4. Handle length mismatch explicitly. Assert on the number of invocations before comparing position by position.
  5. Attribute one mode per task. Use the precedence table above and write down the rule; inconsistent attribution makes the per-model table meaningless.
  6. Run k repeats. Report all-k pass rate, not the average of single attempts.
  7. Gate on the failure mode that matters for your product. If a wrong refund amount is a compliance issue, set the gate on argument score alone and accept a lower overall pass rate.

Two smaller checks that catch real bugs. First, validate tool arguments against their JSON Schema at record time and store the validation result in the fixture; a missing required field is then visible without re-running the model. Second, log token counts per task. An agent that passes but calls twelve tools where four were expected is wasting money, and the extra-call column in your table is where that shows up.

Treating agent behavior as schema compliance plus trace scoring changes what you compare. A function-calling benchmark measures individual calls, and call accuracy is necessary but not sufficient because a valid call still fails if the checks or updates it was supposed to trigger were skipped per NVIDIA's evaluation post. Your own fixtures close that gap for your toolset, because they encode the dependencies your domain cares about. If you would rather hand the harness build and the failure analysis to someone who does it for a living, a specialist can build it with you at https://quantaintelligence.ai/.

FAQ

FAQ

What is the minimum a tool-call fixture needs to contain?

Tool name, arguments, the result the tool returned, and a turn index, plus a task ID and the input. Without the result you cannot detect ignored errors; without the turn index you cannot score order; without the task ID you cannot roll up per-task failure modes.

How do I score a task where two different call orders are both valid?

Store allowed alternatives per step in the fixture and score a step as matched if the tool name is in that step's allowed set. τ²-bench handles the same problem by grading final database state rather than the call sequence per Splunk, which is worth adding as a second check alongside order scoring. Use that state check as a hard gate and keep the order score as a diagnostic, so a valid alternative path is never marked a failure.

Should I use an LLM as a judge to score tool calls?

Not for tool names and typed arguments. Exact comparison is cheaper, deterministic and auditable. Reserve a judge for the free-text final answer, and validate the judge separately, since judges can favor long and confident answers as Splunk's evaluation article warns.

Why does schema validation pass while the tool call is still wrong?

Schema validation checks shape, not values: schema-constrained generation guarantees syntactic validity and reduces parse errors but does not ensure value correctness per MLflow. A call with the right fields and the wrong customer ID validates cleanly, which is why you score arguments against expected values, not just against the schema.

How many repeats per fixture are enough?

Enough to see consistency, not just average success. Start with five runs per task and report the fraction of tasks that pass all five, then adjust the repeat count to your variance; the point is to catch flaky tasks that pass once and fail on retry.

Sources

Sources

  1. Evaluating AI Agents on Tool Calling and Planning
  2. How to Evaluate AI Agents From Tool Calls to Task ...
  3. Enforce, Validate, Observe: 3 LLM Structured Output Patterns ... - MLflow
  4. Handling Validation Errors - Python jsonschema
  5. When an LLM response fails validation, feed the error back ...
  6. Structured Output Schema Validation Changed Without Notice
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