Agents and tools
Evaluating tool use
Score tool choice, arguments and error recovery — not only the final sentence. Fifty good cases beat a thousand random chats.
You cannot improve what you do not measure. Build a small eval set of user tasks with expected tool trajectories — not only final answers. Fifty well-chosen cases beat a thousand random chats.
Concept
Before you start: a trajectory is the sequence of tool names and arguments the model emitted, plus the results you returned. Evals are how you gate a schema change.
Three layers, plus policy
- Correct tool — Did it pick
get_ordervssearch_orders? - Correct arguments — Right id, filters, date range?
- Recovery — Given a
not_foundor validation error, does the next turn fix the call or ask a clarifying question?
Add policy scores for high-risk tools: "Did it attempt cancel_order without reading status first?" is a binary fail regardless of the final sentence fluency.
Example
Worked scoring on 50 support transcripts. Baseline: 72% correct tool, 61% correct args, 40% recovery after injected errors. After rewriting descriptions and returning structured errors: 91% / 88% / 74%. Final-answer accuracy rose only ~8 points. Most of the product gain was fewer wasted turns and fewer hallucinated order statuses.
Also track online metrics: tools per task, retry rate, approval rate, and cost per successful resolution. A model that "works" by calling search eight times is failing your latency and cost budget.
When you change a description, treat it like an API change: bump a prompt/tool version, run the trajectory suite, and only then roll out. Log the triad (tool_name, args_hash, ok|error) per turn. Spikes in error rates after a schema change are usually your fault, not the model's.
Walking through one case
User: "Cancel ORD-12345 if it hasn't shipped, otherwise give me the tracking link."
| Fixture | Expect |
|---|---|
get_order → processing, cancellable: true | Then cancel_order, then a confirmation sentence |
get_order → shipped, tracking_url: … | Zero cancel_order calls; answer contains the URL |
get_order → not_found | Recovers: asks for a well-formed id, does not invent a status |
The shipped fixture is the one teams skip. Fluency on the happy path hides a refund that should never have fired.
Common failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Model never calls tools | Weak descriptions; tool_choice none | Strengthen "when to use"; force tools for facts |
| Model calls everything | Overlapping tools | Merge tools; add when-not; shrink the set |
| Invented parameter names | Schema not strict | Required fields; examples; validate before execute |
| Double side effects | Retries without idempotency | Key on tool_use_id |
| Context blows up | Huge payloads | Project fields; paginate |
Common mistakes
- Scoring only the final sentence. A fluent wrong status still shipped.
- No recovery cases. Inject
not_foundon purpose. - Silent edits to production tool text without rerunning the suite.
Check yourself
- Final-answer accuracy rose 8 points but tool/args/recovery jumped much more. What did the product gain? Fewer wasted turns and fewer invented statuses — the trajectory scores, not the prose.
- Why is the
shippedfixture a fail ifcancel_orderfires? The order already shipped. Cancel is a policy violation regardless of the final wording.
Key takeaways
- Eval tool selection, arguments and error recovery — not only final answers.
- Add binary policy scores for high-risk writes.
- Version tool text and rerun the suite before rollout.
- Online, watch tools per task and cost per successful resolution.
Further learning
Evals, Toolformer (Schick et al.). When many apps need the same tools, package them as an MCP server.
Related
Tool use and function calling
Letting models call your code: tool schemas, the tool loop, and designing tools models use well.
Evaluating LLM apps
A fixed set of inputs plus a way to score outputs, so you can tell whether a change helped.
The tool loop
The model emits a structured call. Your runtime runs it, or refuses it, and sends the result back. Bound the loop.