TL;DR
On each turn the model looks at the conversation and the available tool
descriptions and makes one decision: answer directly, or emit a tool call?
You don't script that choice — but you can shape it with good descriptions and
constrain it with the tool_choice setting (auto, any, a specific tool, or
none).
The idea
You never write "if the user asks math, call the calculator." The model decides, every turn, on its own. It weighs two things:
- The request — what is the user actually asking for?
- The tool descriptions — does any tool's description match that need?
If a description matches, the model emits a structured tool-use request instead of a normal text answer. If nothing fits, it just answers. This is why Lesson 2 mattered: the description is the input to this decision. Vague descriptions produce bad decisions; sharp ones produce good decisions.
Steering the decision with tool_choice
Most of the time you let the model decide. But sometimes you need to force the issue — and there's a setting for that. It controls whether the model is allowed to choose:
| tool_choice | What it does | When to use it |
|---|---|---|
| auto | Model decides each turn: answer or call a tool | The default — almost always what you want |
| any | Must call some tool, but the model picks which | You know a tool is needed but not which one |
| a named tool | Must call that exact tool this turn | Structured extraction — force one specific call |
| none | May not call any tool; text only | Temporarily disable tools without removing them |
Analogy
auto is letting your colleague decide whether to reach for a calculator. any
is saying "use a tool for this." A named tool is "use the calculator,
specifically." none is "no tools right now, just talk me through it." You're not
doing the work — you're setting how much freedom they have to pick.
Why descriptions still beat forcing
Forcing a tool with tool_choice is a blunt instrument. If you set any or a
named tool, the model must call it even when calling it makes no sense — so a
greeting like "hi" can trigger a pointless lookup. That's why the durable lever is
the description: with auto, a well-written description gets the tool called
at the right moments and skipped at the wrong ones, with no forcing at all.
Reach for tool_choice when you have a structural reason (you're extracting data
and always want the one tool), not as a patch for a fuzzy description.
Picking between several tools
Give the model one tool and the only question is whether to call it. Give it five
and the question becomes which one — answered the same way: the request matched
against each tool's description. So with several tools, description quality
compounds. If lookup_by_email and lookup_by_order_id both say "look up a
customer," the model can't choose well. Name what's distinct about each, and it
routes correctly:
| Tool | Description that routes correctly |
|---|---|
lookup_by_email | "Find a customer account using their email address." |
lookup_by_order_id | "Find an order using its order ID (format ORD-12345)." |
calculator | "Evaluate an arithmetic expression." |
Common mistake
Leaving tool_choice on any or a named tool "to be safe" forces a tool call on
every turn — including turns where the model should just reply. The result is
needless tool calls, extra latency, and odd behavior on simple messages. Default
to auto and fix decisions through descriptions; force only when the task truly
requires it.
Key takeaways
Key takeaways
- Each turn the model chooses: answer directly, or emit a tool-use call.
- The decision is driven by the request matched against tool descriptions.
tool_choiceconstrains that freedom:auto(default),any, a named tool, ornone.- With several tools, the model routes by description — keep them distinct.
- Prefer shaping behavior with good descriptions over forcing tools.
Go deeper: what a tool-use request actually looks like
When the model decides to call a tool, it doesn't run anything — it returns a structured tool-use block in its response, something like:
{ "type": "tool_use", "id": "toolu_01A...", "name": "calculator",
"input": { "expression": "47362 * 1989" } }
Your application sees that block, runs the matching function, and sends the result
back as a tool-result block tied to the same id. The model never executed
code — it asked, in a structured way your code can dispatch on. Two
important consequences:
- The model can request several tool calls in one turn (you'll see this in the
multi-tool lesson). Each gets its own
id. - Because the result is matched back by
id, you can run independent tool calls in parallel and return them together.
That request/result handshake is the atom the whole tool loop is built from — which is the next lesson.
Optional — designing a tool set, not just tools
As the tool count grows, treat the set as a system:
- Minimize overlap. Two tools that do almost the same thing force a coin-flip. Merge them, or sharpen their descriptions so the boundary is obvious.
- Name the unit of work. Prefer a few well-scoped tools (
lookup_account,create_ticket) over one mega-tool with amodeargument — the model reasons about named capabilities better than about flags. - Make results self-describing. A result of
{ "found": false }is clearer to the model than an empty array. Tell it what happened in words it can act on.
A good tool set is one where, for any request, the right call is the obvious one. That's a property of your descriptions and boundaries — not of the model.
