Runtype
GuidesGuide

Why your agent calls the wrong tool, and how to fix it in the tool layer

Two tools that sound alike are a schema problem, not a prompt problem. Names, descriptions, argument enums, result shapes and a selection eval set.

Last updated 6 min read

Tool selection is decided by the tool schemas the model reads, so change them rather than the system prompt. Name tools so no two overlap, write descriptions that state the trigger condition and what the tool is not for, constrain arguments with enums and patterns, return results that tell a wrong call apart from an empty one, and delete tools you keep writing rules about.

A model choosing a function is doing retrieval over a short list of names and descriptions. When two entries in that list read as near-synonyms, the choice is close to a coin flip, and a paragraph of guidance three thousand tokens away shifts the odds without settling them.

My agent picks the wrong function to call

The support agent has search_orders and get_order. A customer writes in with order A-10482 and a complaint. The model calls search_orders with the customer's email, gets three orders back, takes the first one, and issues the refund against it. The final message names A-10482 because the model wrote the summary from the conversation rather than from the tool result.

Both descriptions were one line long. search_orders said "Search orders" and get_order said "Get an order details". Nothing in either one told the model that an order id in the message is the signal to stop searching, and nothing in the argument schema of search_orders refused an email address in a field named query.

The usual response is another paragraph in the system prompt: "When the user provides an order ID, always use get_order." Accuracy improves for a week, then a new tool arrives with a similar name, or the conversation gets long enough that the rule sits far behind the current message, and the failure returns in a slightly different shape. The rule lives in the wrong place. It belongs in the description of the tool it governs, where the model is reading at the moment it decides.

Two other symptoms come from the same root. An agent that calls a lookup tool over and over is usually reading an empty array as an ambiguous failure, which is covered in why agents get stuck in tool-call loops. An agent that picks badly only after you crossed some threshold of tools is a catalog-size problem, treated in how many tools an agent can handle.

Fix selection in the tool layer, in seven steps

1. Print the tool list exactly as the model receives it

Serialize the tools array your client sends to the provider and read it end to end. Not your source code, not the docs page, not the MCP server's README: the JSON on the wire, with names, descriptions and parameter schemas, in order.

Two things usually surface immediately. Descriptions that were written for a human reviewer ("wrapper around the orders service"), and pairs of tools whose combined text gives no rule for choosing between them. Everything below is a change to that JSON.

2. Give each description a trigger, an exclusion and an example

A description that works has three parts: the condition that should make the model reach for this tool, at least one condition under which it must not, and one concrete instance. Name the competing tool by its exact tool name in the exclusion, because that string is in the model's context and resolves cleanly.

Before, the two tools that caused the refund:

[
  { "name": "search_orders", "description": "Search orders." },
  { "name": "get_order", "description": "Get an order details." }
]

After, with the selection rule moved out of the system prompt and into the schemas:

[
  {
    "name": "get_order_by_id",
    "description": "Fetch one order by its order id. Use this whenever the message contains an order id in the form A-12345, even if the user also gives an email. Do not use it to find an order when no id is present; call find_orders_by_customer instead. Example: 'order A-10482 arrived damaged' -> get_order_by_id({order_id: 'A-10482'}).",
    "parameters": {
      "type": "object",
      "additionalProperties": false,
      "required": ["order_id"],
      "properties": {
        "order_id": {
          "type": "string",
          "pattern": "^A-[0-9]{5}$",
          "description": "Order id as printed on the receipt.",
          "examples": ["A-10482"]
        }
      }
    }
  },
  {
    "name": "find_orders_by_customer",
    "description": "List a customer's recent orders when you have an email address or a customer id and no order id. Do not use it when an order id is present, and do not use it to check refund eligibility; that is check_refund_eligibility. Example: 'my last order never arrived, I am ada@example.com'.",
    "parameters": {
      "type": "object",
      "additionalProperties": false,
      "required": ["identifier_type", "identifier"],
      "properties": {
        "identifier_type": { "type": "string", "enum": ["email", "customer_id"] },
        "identifier": { "type": "string", "minLength": 3, "examples": ["ada@example.com", "cus_8812"] },
        "limit": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 }
      }
    }
  }
]

3. Rename until no two names are substitutable

search_orders and get_order share a noun and differ by a verb whose meanings overlap in English. Rename on the access path rather than the verb: get_order_by_id and find_orders_by_customer cannot be swapped without the name reading wrong.

Three naming rules hold up under model changes. One verb per operation across the whole catalog, so get, fetch and read never coexist. The lookup key in the name whenever a resource has more than one. A namespace prefix per system, such as billing_ or crm_, so a tool from a newly connected MCP server cannot collide with an existing one.

4. Make the wrong call fail validation, not the answer

An argument schema is enforcement. A free-text query: string accepts an email, an order id, a customer name and a sentence, so the model can route a request to the wrong tool and still produce a well-formed call that the API answers with something plausible.

Constraints that carry real weight: enum for anything with a closed set of values, pattern for identifiers with a stable format, additionalProperties: false so an invented field is rejected instead of silently dropped, required on the fields that define the operation, and examples on any field whose format a model would otherwise guess. When a customer id and an order id can both be strings, either give them distinct patterns or make the field pair explicit, as identifier_type plus identifier does above.

5. Return results a model can act on

A tool result is context. A bare [], a null, or an HTTP error body forwarded verbatim leaves the model to infer what happened, and the common inference is "that call did not work, try another tool", which produces both the wrong-tool retry and the loop.

Separate the three outcomes and say what to do next in each:

{
  "status": "empty",
  "count": 0,
  "searched": { "identifier_type": "email", "identifier": "ada@example.com" },
  "next_step": "No orders for this address. Ask the user for the order id or an alternate email. Do not retry this tool with the same arguments."
}
{
  "status": "error",
  "code": "upstream_timeout",
  "retryable": true,
  "retry_after_seconds": 2,
  "next_step": "Retry get_order_by_id once. If it fails again, tell the user the order system is unavailable."
}
{
  "status": "wrong_tool",
  "code": "id_looks_like_order_id",
  "next_step": "The identifier A-10482 is an order id. Call get_order_by_id with it."
}

The third shape is worth building where two tools stay confusable for reasons you cannot remove. Validate the argument at the boundary, refuse the call, and name the correct tool in the refusal. Recovery then costs one extra call rather than a wrong write.

6. Delete tools instead of describing around them

Every disambiguating sentence you add is evidence that two tools should be one, or that one of them should be gone. Merge when two tools hit the same resource and differ only by lookup key or filter, and hand the difference to an enum argument. Split a tool when a single name covers both a read and a write, since a wrong selection there has a side effect.

Removal is the cheapest fix available and the one teams skip. Tools nobody has called in a month, admin operations the agent should never reach, and duplicate integrations left behind after a migration all sit in the list, consuming attention on every request. Trimming the catalog raises accuracy on the tools that remain, which is the argument developed in how many tools an agent can handle.

7. Score selection as its own metric

Build a case set where each case pins the expected first tool and the tools that must not appear, then run it after every schema edit, prompt change and model upgrade. Selection accuracy moves independently of answer quality, so an aggregate answer score will hide a regression here for weeks.

CaseMessageExpected first callForbidden
C-01"Order A-10482 arrived damaged"get_order_by_idfind_orders_by_customer
C-02"My last order never came, I am ada@example.com"find_orders_by_customerget_order_by_id
C-03"A-10482, and my email is ada@example.com"get_order_by_idfind_orders_by_customer
C-04"Can I get a refund on A-10482?"check_refund_eligibilitycreate_refund
C-05"What is your return window?"noneany order tool

C-03 and C-05 carry most of the value. The first pins behavior when both signals are present, which is where a vague description fails. The second checks that the agent will answer from its own knowledge rather than calling something, a failure that grows as the catalog grows.

Assertion styles for the full call sequence, including arguments and ordering, are in how to test tool calling. The tool definitions themselves, whether hand-written or exposed through an MCP server, are where each of these cases is decided.

Where this gets easier

Because Runtype records the arguments and the result of every tool call in the execution trace, tool-selection accuracy becomes a number you read per tool rather than a hypothesis you form from bad answers: you can count the runs where find_orders_by_customer was called with an order id sitting in the message, promote those recorded executions into eval cases, and re-run the suite after each schema edit to confirm the rename or the added enum actually moved the metric.

Frequently asked questions

Why does adding instructions to the system prompt only half fix tool selection?
The system prompt and the tool list compete for the same attention, and the tool list wins on the calls where selection actually happens, because the model reads the schema at the moment it decides. A prompt rule also applies to every turn, so it degrades as the conversation grows and the rule drifts further from the current message. A disambiguating sentence inside the description of the tool it disambiguates sits next to the decision instead.
Should overlapping tools be merged into one tool with a mode parameter?
Merge when the two tools hit the same backend resource and differ only in a lookup key or a filter, since one tool with an enum argument turns a selection problem into an argument problem, which schema validation can catch. Keep them separate when the side effects differ. A read and a write behind one enum means a wrong enum value spends money.
How do I know whether a fix worked?
Score selection separately from answer quality across a fixed case set, and record the first tool called per case along with the full call sequence. A change that raises answer scores while first-call accuracy drops usually means the model is compensating with extra calls, which costs tokens and latency and will fail differently under a model upgrade.