Skip to content

16 min read

Selection and argument filling

On this page

A language model, left to itself, produces text; it cannot read an orders table, issue a refund, or find out whether a parcel was delivered this morning. Tool use — the mechanism other vendors call function calling — is how that gap gets closed. You send the model, alongside the conversation, a list of functions it is allowed to invoke. It replies with the name of one of them and a JSON (JavaScript Object Notation) object of arguments; your code runs the real function and sends the result back; the model continues with the answer in hand. The model never executes anything itself, so every tool in that list is code you already wrote and are now exposing to a caller who has never seen it.

Each entry in the list is a tool definition, and it has three parts: a name, a free-text description, and an input_schema declaring the arguments, each of which may carry a short description of its own. The description is the subject of these seven chapters. It exists because the schema cannot say what a tool is for: a type can state that order_id is a string, and nothing in JSON Schema can state that this tool answers questions about shipments while its neighbour answers questions about money going back to the customer. Since the model has never seen your code, your database or your runbook, the description is the entire briefing it gets before deciding which function the user's sentence belongs to. You meet these strings wherever agents get built — hand-written in the tools array of a request to the Claude or OpenAI APIs, generated from decorators or docstrings by a framework, or published by somebody else's Model Context Protocol (MCP) server and loaded into your agent whole. Whatever produces them, they end up in the same place, and the three of them are not interchangeable.

What the model is looking at when it decides

The provider renders the tools array into the prompt ahead of the system prompt and the conversation, so a two-tool slice of a payments support agent arrives at the model looking roughly like this:

json
{
  "tools": [
    {
      "name": "search_orders",
      "description": "Look up a customer's orders by email, order number or date range. Returns order status, line items, shipment tracking and delivery estimates.",
      "input_schema": {
        "type": "object",
        "properties": {
          "order_id": {
            "type": "string",
            "description": "The order identifier. Only use this for order status questions, not refund or billing dispute questions."
          },
          "email": { "type": "string", "description": "Customer email address." }
        },
        "required": []
      }
    },
    {
      "name": "search_refunds",
      "description": "Search refunds by customer or order.",
      "input_schema": { "type": "object", "properties": { "order_id": { "type": "string" } } }
    }
  ]
}

Tokenisation flattens all of that into one sequence, which is why the intuition it is all in the context, so it does not matter which field I put the sentence in is so tempting. It is true about the bytes and wrong about the decision. What matters is not whether the model can attend to a string but at what point in its own output the string becomes relevant, and the model's output is generated in order.

When the model decides to call a tool it emits a block whose fields arrive one after another:

json
{
  "type": "tool_use",
  "id": "toolu_01A9…",
  "name": "search_orders",
  "input": { "order_id": "ORD-88213" }
}

The name is committed before a single token of input exists. By the time the decoder is generating the argument object — the point at which order_id's description is the most locally relevant text in the whole prompt — the routing decision is already several tokens in the past and is now part of the prefix conditioning everything that follows. A rule written into order_id's description says, in effect, by the way, you should not have called this tool, addressed to a model that has already called it. The most a model can do with that is decide the arguments are unfillable and start a second call, which some models occasionally do and most do not.

What committed means there is worth being exact about, because it is the load-bearing step and it is easy to hear as a figure of speech. A model produces one token at a time, and every token it produces is appended to the context conditioning the next one. There is no revision pass and no point at which an earlier field is reconsidered. Once the tokens spelling search_orders are in the output they have stopped being a decision under review; they are part of the text the model is now continuing, and everything after them is generated on the assumption that they are there. The rival tool's description has not disappeared from the context. It has changed role — from one of two candidates being weighed to a piece of background that the emitted name is simply inconsistent with — and continuing a sequence is something a model does far more readily than contradicting one it has just written.

The two orders involved are easier to see drawn than described. Everything is present in the context from the first token, and the strings still become decision-relevant at different points in the model's own output:

Flowchart: Whole prompt in context: names, descriptions, schemas, transcript → Rank the candidates on name and description; Rank the candidates on name and description → Emit name: search_orders; Emit name: search_orders → Fill the slots, reading this tool's parameter descriptions; Fill the slots, reading this tool's parameter descriptions → Emit input: order_id; Emit name: search_orders → The rival descriptions are now prefix, not candidates.

Selection, by contrast, is a comparison. At the moment of choosing, the model has in front of it every name and every description, plus the user's message, and it is effectively ranking them. This is the sense in which the description is the load-bearing string: it is the only text whose job is to win or lose that comparison.

The name takes part in the same comparison and is the densest text in the whole definition, since it is a handful of tokens sitting at the top of every rendered entry and repeated in every prior tool_use block in the transcript. Names inherited from the implementation — crm_v2_fetch, svc_billing_read, do_lookup — spend that leverage on information no user request will ever contain, and leave the description to recover ground the name gave away. A verb and a domain noun drawn from the vocabulary of the request costs exactly the same tokens and does part of the routing before the description is read, which is why search_refunds is worth more than refund_svc_query at identical length. Chapter 3 puts a number on what a rename buys on its own.

StringRead atCan changeCannot change
Tool nameSelectionWhich tool is called; strongest signal per tokenNothing about argument values
Tool descriptionSelectionWhich tool is called; whether no tool is calledLittle, once the name is emitted
Parameter descriptionArgument fillingFormat, units, allowed values, which fields get populatedWhich tool was chosen
required in the schemaArgument fillingWhether a field appears in the callWhether the model has a real value for it

The fourth row is the one that catches people out later, and chapter 4 spends its time there. Marking order_id as required does not mean the model must possess an order id; it means that if it calls the tool, the JSON must contain that key. A model with no id and a required field will produce a string, because producing a string is the only way to satisfy the constraint it can see.

What wins the comparison

On this agent, one week of logs recorded 4,180 tool calls, and 340 of them went to a tool that could not possibly answer the question that triggered it. The largest single cluster is users asking some variant of did my refund go through, which lands in search_orders rather than search_refunds.

The count and the rate are one measurement stated two ways, and the two ways travel very differently. Three hundred and forty misroutes against four thousand one hundred and eighty calls is a bit over eight calls in a hundred going somewhere that cannot answer, which is the routing accuracy of a little under ninety-two percent quoted later in this chapter. A rate in the low nineties reads as a system that mostly works and gets reported upward as such. A count in the hundreds reads as a system with a specific defect and gets somebody to open the tool definitions. Both are the same week of logs.

Those 340 misroutes were a comparison the description lost. search_orders describes itself well — email, order number, date range, status, shipments, tracking — and search_refunds describes itself in seven words. Against the query why was I charged twice, the longer, more specific description is simply a better match for a question about a charge on an order, and nothing in either string says otherwise. The rewrite that moved the number was in the tool descriptions, both of them:

json
{
  "name": "search_refunds",
  "description": "Search refunds, chargebacks and duplicate-charge investigations for a customer. Use this for any question about money going back to the customer: refund requested, refund not received, charged twice, disputed charge, cancellation credit. Returns refund status, amount, method, initiated and settled dates, and the originating order. For questions about whether an order shipped or when it will arrive, use search_orders instead."
}

A description that has to win a comparison carries four things: what the tool does, the situations that should route to it stated in the words a user would actually use, what it gives back, and where the neighbouring cases go instead. On this agent, rewriting both sides of the pair took the charged twice cluster from 22% correct to 96%, and overall routing accuracy from 91.9% to 98.7% across the same 4,180-call sample replayed offline. Anthropic's own write-up on writing tools for agents reports the same shape of result at a much larger scale: description edits alone, with no change to the underlying tools, moving SWE-bench numbers by margins that would normally require a model upgrade.

Two details in that rewrite are worth naming. The phrase charged twice appears verbatim, because the description is matched against the user's language and not against a taxonomy you invented; writing billing anomaly reconciliation would be accurate and useless. And the last sentence names the sibling tool, which is the move chapter 3 generalises — a description that only describes itself cannot resolve an overlap, because both descriptions can be individually correct and still leave the boundary undefined.

The same sentence does different work depending on where it sits

The claim that placement decides effect is testable, and on this agent it was tested before the rewrite shipped. One sentence — refund, duplicate-charge and disputed-charge questions belong to search_refunds; search_orders covers order status, shipments and delivery dates — was written into five different places, one variant at a time, and each was replayed against the same 340 misrouted calls at temperature 0 with the same model build.

Where the sentence was placedMisroutes remaining, of 340Tokens added per request
order_id parameter description on search_orders33631
search_orders description, phrased as an exclusion12133
search_refunds description, phrased as a claim8833
Both tool descriptions1466
System prompt9633

The first row is the control for the whole experiment: identical words, identical token cost, no measurable effect, because they are read after the name has been emitted. The two middle rows are the same sentence arriving at the right moment from one side of the pair or the other, and each recovers roughly two thirds of the errors on its own, with the positive claim on search_refunds doing slightly better than the prohibition on search_orders. Writing both is what gets to 14, and why that is more than the sum of the halves is chapter 3's subject.

Converting that middle column into shares makes the ordering easier to hold than the raw counts do. The parameter-description variant leaves all but a handful of the errors standing, so it recovers about one misroute in a hundred. The exclusion written into search_orders recovers a bit under two thirds. The positive claim written into search_refunds recovers a bit under three quarters. Both together leave a twentieth of the original errors, and the system-prompt variant sits between the two single-sided ones. Two things fall out of that ordering. Every variant read during the comparison does real work, and the only variant that does no work is the only one read after it, which is exactly the separation the experiment was built to isolate. And the two halves of a boundary are not redundant with each other — writing both recovers substantially more than either alone, which is what you would expect if the model is ranking two candidates rather than checking one against a rule.

The system prompt row is the one that surprises people, because global text feels like it should dominate. It works, and it works for the same mechanical reason the description does: it is read while the model is still ranking candidates, so it participates in the comparison. What it does not have is locality. A routing rule in the system prompt is one sentence per contested pair, held in a file that knows nothing about which tools are enabled for this tenant, and it goes stale the moment a tool is renamed or dropped from the block. The rule that scales is that anything true about one tool relative to its neighbours travels with the tool definition, so it is deployed, reviewed and deleted alongside the thing it describes.

A rule in the wrong direction fails the same way

The mirror-image mistake is easier to make and produces a failure that looks unrelated. search_orders also accepts a limit, whose parameter description read "Number of results", while the tool description carried the constraint: Returns at most 200 results per call. Over the same week, 61 of 680 search_orders calls asked for a limit above 200 — usually 500 or 1,000, on requests phrased as pull everything for this customer — and every one came back as a 400 invalid_request that the model then had to recover from, at a cost of two extra turns and roughly 1,800 tokens each time.

That is about one call in eleven, and the cost per occurrence is worth multiplying out, because this failure is the mirror image of the routing one and is priced completely differently. Two extra turns means two more requests, each carrying the whole conversation to that point, and the token figure quoted is the total across both. Sixty-one occurrences in a week comes to something over a hundred thousand tokens spent recovering from a rejection that fifteen words in the right field would have prevented. The routing failure earlier in this chapter cost correctness, and somebody eventually complained. This one costs turns and money and nothing else, which is why it can run for months with nobody filing anything.

The constraint was in the prompt, was unambiguous, and was read several hundred tokens and one whole decision before the slot it governs. By the time the decoder was choosing a number for limit, the locally relevant string said "Number of results" and nothing else. Rewriting that parameter description as Maximum number of results. Integer between 1 and 200; values above 200 are rejected. For larger result sets, page with cursor took over-limit calls to 1 in 680 the following week, without touching the tool description at all.

Both failures come out of the same fact and neither is repaired by the other's fix. A string does its work at the moment it is read, which makes a constraint on a value useless in the tool description and a precondition on the call useless in the parameter schema. The practical form of the rule is a question you can ask about any sentence before you place it: is this something the model needs in order to decide whether to call, or in order to decide what to send? The first belongs in the description, the second beside the value.

That rule has an edge worth stating, because it is not the same rule as never say anything twice. A fact can be genuinely relevant at both moments, and then it belongs in both places: a tool that accepts identifiers only from a particular source wants the provenance rule in its description, where it can stop the call, and a shortened restatement beside the argument, where it shapes the value. What the rule forbids is putting a fact in exactly one field and expecting it to act at the other moment. It also assumes the two moments are cleanly separable, which they are for a single tool call and are less so on a turn where the model emits several calls at once — the second call's name is emitted after the first call's arguments, so an early parameter description does sit in the prefix for a later selection. That is a narrow case and it rescues nothing above, where there was only ever one call to make.

Where placement stops being the answer

None of this means parameter descriptions are unimportant or that the model ignores them. They do a job the tool description cannot: they are where you say that date_from is ISO-8601 and inclusive, that limit above 200 will be rejected, that status accepts exactly four values, that amount is in minor units so £12.50 is 1250. Those are the strings that determine whether a correctly-routed call succeeds, and under-writing them produces a different failure — the right tool called with garbage — that no amount of description tuning will fix. The claim here is narrower than "parameter descriptions do not matter": it is that they cannot repair a routing error, because routing is over by the time they are read.

Nor is the tool description a substitute for the system prompt. Global policy — never take an action on an account without confirming the account holder — belongs in the system prompt, where it applies across every tool and is stated once. The description is for what is specific to this tool relative to its neighbours. The failure mode of confusing the two is a system prompt that recites the tool list in prose, which duplicates the tool block, drifts out of sync the moment someone disables a tool, and pays for the same information twice on every request.

And the description is not documentation. Nobody is reading it to learn the API; your engineers have the OpenAPI spec. Its only reader is a model making one decision under comparison, which is why it should be evaluated the way you evaluate a routing function and not the way you review a docstring. What that evaluation looks like is chapter 6.

Three strings, read at two moments. The name and the tool description compete for a decision that is over the instant the name is emitted; the parameter descriptions and the schema shape the object that follows. Every subsequent chapter in this topic is a consequence of that ordering — which is why a demonstration inside a description is so powerful, why an exclusion only works when the receiving tool claims what it hands over, and why a precondition has to be stated before the call rather than beside the argument.

The pair rewrite above did more than name the sibling, though, and the extra thing it did is where the next chapter starts. Descriptions frequently end with a worked example — a sample query, a sample id, a sample date range — and that example is not read as illustration. The model treats it as a demonstration of what a well-formed argument looks like and imitates it, which makes it the most powerful sentence in the string and the one most likely to be wrong six months from now.

Tool design — descriptions — read the current version at https://daviddominguez.dev/learn/en/tool-design-descriptions/selection-and-argument-filling

Back to top