Skip to content
← All posts
Tool Design 8 min read

Why Your Agent Picks the Wrong Tool, and How to Fix It

Most agent tool-selection failures are interface design failures. Here is how we name, scope and gate tools so the model stops guessing.

An agent with eighteen tools calls search_orders when the user asked about a refund, gets an empty result, then calls it again with slightly different arguments, then apologizes and makes something up. The logs look fine. Every individual call succeeded. The agent simply chose wrong, three times in a row, and the user got nonsense.

Almost every time we have debugged this, the fix was not a better model or a longer system prompt. It was the tool surface. Two tools whose descriptions overlapped by 80%. A name that meant something different to the model than it did to the team that wrote it. An argument the model had no way to know, so it invented one. A tool that should never have been callable in that conversation state at all.

Tool selection is an interface problem. The model is a caller reading your API docs with no ability to ask a follow-up question, no access to your codebase, and one shot to guess right. If a competent contractor with only your tool descriptions in front of them would pick the wrong tool, the model will too — just faster and more confidently.

The four failure modes worth naming separately

Lumping all of this under "the agent is unreliable" makes it unfixable. Split it up, because each mode has a different remedy.

  • Ambiguity. Two or more tools plausibly answer the request. get_customer and lookup_account both exist because two teams shipped independently. Remedy: merge or disambiguate in the description with an explicit "use this instead of X when…" clause.
  • Under-specification. The tool is the right one but the model cannot fill the arguments. It needs a location_id the user never mentioned. Remedy: make the argument optional with a documented default, or give the model a cheap resolver tool.
  • Over-reach. The tool exists but should not be reachable now — a write action before confirmation, or a billing mutation in a read-only support session. Remedy: gate the tool list by state, not by prompt instruction.
  • Naming drift. The name encodes internal jargon. sync_channel means marketplace listing sync to us and something else entirely to a model trained on the whole internet. Remedy: rename for the caller, not for the codebase.

When we instrument agents, we tag each wrong call with one of those four. After a few hundred traces the distribution tells you exactly where to spend the week. In our experience ambiguity and over-reach dominate early; under-specification shows up later, once the easy wins are done.

Write the description for the caller, not for the repo

A tool description is the entire contract. The model cannot read your implementation, your tests, or the Slack thread where you decided what "active" means. Everything it knows comes from the name, the description, and the argument schema.

Our house rule: every description states what it returns, when to use it, and when not to use it. That third clause does the heavy lifting and almost nobody writes it.

{
  "name": "get_inventory_at_location",
  "description": "Returns on-hand and committed quantity for one SKU at one store location. Use for questions about stock at a specific store. Do NOT use for company-wide totals across stores — use get_inventory_summary instead. Do NOT use to change stock; that is adjust_inventory.",
  "parameters": {
    "type": "object",
    "properties": {
      "sku": { "type": "string", "description": "Exact SKU. If the user gave a product name, call find_sku first." },
      "location_id": { "type": "string", "description": "Store location UUID. Omit to use the session's current location." }
    },
    "required": ["sku"]
  }
}

Three things are doing work there. The negative clauses point at the specific sibling tools that get confused with this one. The sku description tells the model what to do when it has a name rather than an ID, which kills a whole class of hallucinated identifiers. And location_id is optional with a documented server-side default, so the model never has to guess a UUID.

That last point is worth generalizing: never require an argument the model cannot derive from the conversation. If it is required and unknowable, the model will fabricate it or pick the wrong tool that does not need it. Push it into session context on the server side instead.

Shrink the tool list before you improve the descriptions

Selection accuracy degrades with the size of the candidate set, and it degrades fastest when candidates are similar. Twelve well-separated tools behave far better than twenty-five with fuzzy boundaries. Before writing a single word of description, run a merge pass.

  1. List every tool with its one-line purpose in a single document. If you cannot fit them all on one screen, that is the finding.
  2. For each pair whose purposes read similarly, ask: could one tool with an enum argument replace both? get_open_invoices and get_paid_invoices should be one tool with a status parameter.
  3. Delete tools that exist only because an endpoint existed. Agents do not need parity with your REST surface.
  4. For anything left over that is genuinely two things, add reciprocal "do not use this, use X" clauses to both descriptions.
  5. Re-run your eval set and compare wrong-tool rate before and after. If it did not move, the problem is elsewhere — probably state gating.

The enum consolidation in step two is the single highest-leverage change we make. It converts a selection decision, which the model is mediocre at, into an argument decision, which it is good at. Selecting between two similarly-named tools is a semantic coin flip; filling status: "paid" from the sentence "which invoices have been paid" is near-trivial.

Gate the tool list by state, not by prompt instruction

"Only call issue_refund after the user confirms the amount" is a prompt instruction, and prompt instructions are advisory. The model will follow it most of the time, which is the worst possible reliability profile for a money-moving action.

Compute the tool list per turn instead. We keep a small function that takes the conversation state — authenticated user, role, current entity, pending confirmations, feature entitlements — and returns the allowed tools. If there is no pending refund awaiting confirmation, issue_refund is simply not in the payload. The model cannot call what it cannot see.

This pairs naturally with the entitlement gating we already run for multi-tenant SaaS platforms. A tenant without the marketplace module should not have marketplace tools in the agent payload, for the same reason they should not see the nav item. On Retailer OS the tool surface a cashier's assistant sees and the one a head-office user sees are different sets computed from the same role and entitlement data that drives the UI.

The model cannot call what it cannot see, and that is a far stronger guarantee than any sentence in a system prompt.

Make the wrong call cheap to recover from

You will not get selection to 100%. Design for the residual. The difference between a tolerable agent and an infuriating one is mostly what happens after a wrong call.

Errors should teach

An empty result set and a validation failure are different events and should read differently. {"error": "not_found"} gives the model nothing. {"error": "No SKU matches 'blue widget'. This tool needs an exact SKU — call find_sku with the product name first."} gets a correct retry on the next turn most of the time. Write error strings as instructions to the caller, because that is what they are.

Read-only by default, writes behind confirmation

Split every mutating tool into a preview and a commit. prepare_refund returns the computed amount, the affected order lines and a token. commit_refund takes only that token. A wrong preview costs a sentence of apology; a wrong commit costs money and trust. This is the same reasoning behind putting confidence scores on model output so a human reviews it rather than trusting it — on BuildAEC the vision takeoff proposes quantities with per-line confidence and an estimator commits them, and the agent tooling follows the same shape.

Cap the loop

Two identical calls with identical arguments means the agent is stuck. Detect it, stop, and hand back to the user with what you have. Uncapped retries are how a support conversation turns into a large token bill and a wrong answer arrived at slowly.

Build the eval set out of your own wrong calls

Tool selection is one of the easiest things in agent engineering to evaluate, because the label is discrete. For a given input and state, there is a correct tool and a correct set of arguments. No judge model required for the first-order check.

We keep a table of cases: user message, conversation state, expected tool name, expected arguments. Every production wrong call becomes a row. The suite runs on every change to a description, a name, or the gating function, and reports wrong-tool rate and wrong-argument rate separately, because they have different fixes.

The separation matters. A rising wrong-argument rate with a flat wrong-tool rate means your schema descriptions drifted. The reverse means your tool boundaries blurred, usually because someone added a tool last week without a reciprocal negative clause. Rename a tool and half your descriptions can become stale in one commit — the suite is what catches that.

Where this advice does not apply

If you have three tools and they do obviously different things, stop reading and ship. Negative clauses, enum consolidation and state-gated payloads are overhead, and overhead on a three-tool agent buys nothing. We have shipped features where the whole tool surface was one search call and one draft call, and the correct engineering decision was to add no structure at all.

The advice also does not apply when the model should not be choosing. Plenty of flows described as agentic are a fixed sequence with a language model at one step. If the order of operations is known at design time, write the sequence in code and call the model for the part that needs judgement. That is faster, cheaper, and cannot pick the wrong tool because there is no choice to make — the same reasoning we apply when deciding whether a feature needs an LLM at all.

And a real tradeoff on the enum consolidation: a tool with five enum values and branching argument requirements is harder for the model to fill correctly than five narrow tools. Somewhere around three or four values, depending on how much the required arguments differ per branch, consolidation starts costing more in wrong arguments than it saves in wrong tools. Your eval suite tells you where that line sits for your surface. Ours has moved as models improved, in both directions.

Start with the merge pass. It is an afternoon of work, it needs no model changes, and on every agent surface we have inherited it removed more wrong calls than any amount of prompt engineering that followed. If you want a second pair of eyes on a tool surface that is misbehaving in production, tell us what it is doing.

Frequently asked

How many tools can an agent reliably choose between?

There is no fixed number, because separation matters more than count. Twelve tools with clear, non-overlapping boundaries behave better than twenty-five similar ones. Rather than guessing a limit, build an eval set of real inputs with expected tool names and measure wrong-tool rate as you add tools. When it starts climbing, merge or gate before adding more.

Should tool descriptions live in the prompt or in the schema?

In the schema. Function-calling APIs pass tool definitions through a dedicated channel that models are trained to attend to, and keeping the contract next to the arguments means it stays in sync with the code. Use the system prompt for policy and tone, not for per-tool routing rules that belong in a description field.

How do I stop an agent calling a write action too early?

Do not instruct it — remove the tool. Compute the allowed tool list per turn from conversation state, so a commit action only appears once a preview has run and the user has confirmed. Split every mutation into prepare and commit, where commit accepts only a token returned by prepare. Prompt instructions are advisory; an absent tool is enforced.

What should a tool return when the agent calls it wrongly?

An error message written as an instruction to the caller. Say what was wrong, and name the tool or argument to use instead. "No SKU matches that name — call find_sku first" recovers on the next turn; "not_found" causes a retry loop. Also detect two identical consecutive calls and stop, rather than letting the loop run.

Do better models remove the need for careful tool design?

They reduce it, they do not remove it. Stronger models handle ambiguity between similar tools better, but they cannot invent an ID they were never given, and they cannot know a business rule that exists only in your head. Under-specified arguments and missing state gating remain failure modes regardless of which model you point at them.