Originally published at heycc.cn. This is a mirrored copy — the canonical version is kept up to date at the source.
AI Agent Tool-Calling Patterns: Building Reliable Function Calling in 2026
Tool calling (also called function calling) is the moment an AI agent stops talking and starts doing. The model reads your tool definitions, decides one is relevant, emits a structured call, your code runs it, and the result flows back into the conversation. The mechanics are simple. Making them reliable — so the agent picks the right tool, fills in valid arguments, recovers from failures, and doesn't get hijacked by hostile data — is where most agent projects quietly break.
An agent that nails a scripted demo and then sends a malformed argument the first time a user phrases a request sideways is the failure mode every one of these patterns exists to prevent. They are drawn from Anthropic's tool-use documentation and OpenAI's function-calling guide (both linked in Sources below). The two providers converge on the same core ideas, with small but important differences in the knobs they expose — and the comparison table in Pattern 3 is the cross-provider synthesis you can't get from either doc set alone.
The loop, precisely
Both Anthropic and OpenAI implement the same five-step cycle. When you call the API with a tools parameter, the platform injects a special system prompt built from your tool definitions that instructs the model on how and when to call them. Anthropic documents this constructed prompt explicitly in its "Define tools" guide.
-
You send the request with the user message and the
toolsarray. -
The model emits a tool call. With Claude, the response carries
stop_reason: "tool_use"and contains one or moretool_usecontent blocks, each with anid, aname, and aninputobject matching the tool's schema. OpenAI returns analogoustool_calls. - Your application runs the tool using those inputs.
-
You send the output back. With Claude, you reply with a user message containing a
tool_resultblock whosetool_use_idmatches the original requestid. With OpenAI, you send a message withrole: "tool"and a matchingtool_call_id. - The model produces the final answer — or requests another tool, and the loop repeats.
The matching of IDs in step 4 is not optional bookkeeping. Anthropic's "Handle tool calls" docs are strict about ordering: tool_result blocks must immediately follow the assistant's tool_use message, and they must come first in the content array, before any text. If a turn produced parallel calls, all of the corresponding results must be submitted back together before the model continues. Drop one and the conversation is malformed.
Pattern 1: Descriptions are the product
If you take one thing from this article: the tool description is the most important lever you have. Anthropic's "Define tools" guide states it plainly — "Provide extremely detailed descriptions. This is by far the most important factor in tool performance." A good description explains what the tool does, when it should and shouldn't be used, what each parameter means, and any caveats or limitations. The documented guidance is to "aim for at least 3-4 sentences per tool description, more if the tool is complex."
Compare these two definitions for the same tool (both adapted from Anthropic's documented stock-price example):
Poor:
"Gets the stock price for a ticker."Good:
"Retrieves the current stock price for a given ticker symbol. The ticker must be a valid symbol for a publicly traded company on a major US exchange like NYSE or NASDAQ. Returns the latest trade price in USD. Use it when the user asks about the current or most recent price of a specific stock. It will not return any other company information."
The poor version leaves the model guessing about scope, valid inputs, and what comes back. The good version closes those gaps. You are effectively writing documentation for a colleague who will read it once and then act without asking follow-up questions. For complex, nested, or format-sensitive inputs, Anthropic also lets you attach an input_examples array of schema-valid examples alongside the description — examples reinforce the prose without replacing it.
Pattern 2: Make invalid calls impossible with strict mode
The most common reliability failure is a malformed argument: a missing required field, a string where a number belongs, a hallucinated enum value. Both providers solve this with strict mode, and both position it as the recommended choice for production agents (though neither literally phrases it as "on by default" — treat it as the default you reach for, not a flag flipped for you).
-
Anthropic: Set
strict: trueas a top-level property on a tool definition. It uses grammar-constrained sampling so the model can only emit schema-valid inputs, which the docs recommend specifically when you need to "validate tool parameters," "build agentic workflows," or "ensure type-safe function calls." The schema must setadditionalProperties: false. -
OpenAI: Enabling strict mode makes calls reliably adhere to the schema instead of being best-effort. The catch is that strict mode imposes constraints:
additionalPropertiesmust befalse, and every field inpropertiesmust be listed inrequired— optional fields are expressed by addingnullas an allowedtyperather than omitting them fromrequired.
Strict mode also pairs with forced tool use for a stronger guarantee. Anthropic's "Define tools" guide notes that combining tool_choice: {"type": "any"} with strict tools guarantees both that one of your tools will be called and that its inputs strictly follow your schema — a composition of two documented behaviors, not a single magic flag.
Concretely, the same tool definition looks different with and without strict mode — the difference is entirely in the schema, not the API call shape:
// Without strict mode: "age" is technically optional, and nothing
// stops the model from adding a field you never defined.
{
"name": "create_user",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name"]
}
}
// A malformed call this schema would NOT catch: {"name": "Ana", "age": "thirty", "role": "admin"}
// — wrong type on age, and an undeclared "role" field both slip through.
// With strict mode: every property is required, additionalProperties
// is locked, and an absent value uses null instead of omission.
{
"name": "create_user",
"input_schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": ["integer", "null"] }
},
"required": ["name", "age"],
"additionalProperties": false
},
"strict": true
}
// Grammar-constrained sampling means the model literally cannot emit
// a wrong-typed "age" or an undeclared "role" field — the call above
// is now impossible to generate, not just invalid if it were.
Pattern 3: Control whether a tool is called at all
tool_choice is how you steer the decision. The two providers expose nearly identical options, but the names and semantics differ in ways that bite if you assume they map one-to-one. This table is the cross-provider reference the individual docs don't give you:
| Intent | Anthropic tool_choice
|
OpenAI tool_choice
|
Subtlety to watch |
|---|---|---|---|
| Model decides freely (default) | auto |
auto |
Model may call zero, one, or several tools |
| Must call some tool | any |
required |
Forces at least one call — not zero |
| Must call one specific tool | {type:"tool", name:...} |
forced function (named) | Anthropic prefills the turn; see below |
| Never call a tool | none |
none (or omit tools) |
Default when no tools are supplied |
| Restrict to a subset | use a smaller tools[]
|
allowed_tools |
Anthropic has no subset flag; you trim the array |
One subtlety: with Claude, when tool_choice is any or tool, the API prefills the assistant turn to force the call, so the model will not emit any natural-language text before the tool_use block, even if you explicitly ask it to. If you want a spoken explanation and a specific tool, keep tool_choice: auto and instead instruct the tool in a user message ("...use the get_weather tool in your response").
To control concurrency, OpenAI exposes parallel_tool_calls. Per OpenAI's guide, setting it to false "ensures exactly zero or one tool is called" per turn — useful when downstream side effects must be serialized. Note the interaction: under tool_choice: "auto" with parallel_tool_calls: false you get zero or one call, but under tool_choice: "required" (which forces at least one call) the same flag pins you to exactly one. Anthropic's equivalent is disable_parallel_tool_use on tool_choice.
Restricting the tools[] array itself is the API-level lever; it's not a substitute for the tiered permission model (auto-run vs. human-approval vs. blocked) covered in AI agent safety guardrails — that tiering decides what an allowed tool is still permitted to do, not just which tools are visible to the model.
Pattern 4: Consolidate, don't proliferate
More tools is not better. Anthropic's tool-design guidance calls out the anti-pattern directly: don't build a separate tool for every action when one tool with an action parameter will do. A flat one-to-one mapping of REST routes to tools floods the selection space and forces the model to chain several calls for one logical task; "fewer, more capable tools reduce selection ambiguity."
The fix is consolidation:
- Instead of
create_pr,review_pr, andmerge_pr, expose one tool with anactionparameter. - Instead of
list_users,list_events, andcreate_event, expose a singleschedule_eventtool. - Instead of
get_customer_by_id,list_transactions, andlist_notes, expose oneget_customer_contexttool.
OpenAI frames the same principle quantitatively: its guide advises you to "aim for fewer than 20 functions available at the start of a turn at any one time" (it calls this a soft suggestion), and to use enums and nested object structure to make invalid states unrepresentable. For larger libraries, both providers point to tool search / deferred loading so rarely-used tools don't sit in context.
When you do keep many tools, namespace the names — github_list_prs, slack_send_message — so selection stays unambiguous as the library grows. Anthropic calls this "especially important when using tool search," where names are matched as text.
Pattern 5: Shape results for a context window, not a database
A tool's return value lands directly in the model's limited context, so verbose responses are expensive and distracting. Two rules:
- Return only high-signal fields. Anthropic advises designing tool responses to return semantic, stable identifiers (slugs or UUIDs) rather than opaque internal references, and to include only the fields the model needs to reason about its next step. The doc's words: "Bloated responses waste context and make it harder for Claude to extract what matters."
- Bound the size. Implement pagination, range selection, filtering, and truncation with sensible default parameters, so a single call can never dump an unbounded result set into the window. Pair this with the structure from Pattern 4: the same tool that consolidates several endpoints should also return a digest, not a full table.
If you are designing the schemas these tools return, our guide to structured outputs from LLM APIs covers the field-shaping decisions in more depth.
Pattern 6: Errors are instructions, not dead ends
A tool that throws should not return a bare "failed". Both providers stress that error content is a recovery signal for the model, not just a status code.
-
With Claude, return the error message in the
tool_resultcontentand setis_error: true. Anthropic's documented guidance is to "write instructive error messages" — instead of generic errors, "include what went wrong and what Claude should try next, e.g.,Rate limit exceeded. Retry after 60 seconds." The model incorporates that text and can adapt; the docs note Claude will retry an invalid call 2-3 times with corrections before apologizing to the user. -
With OpenAI, return the error as the
toolmessage content so the model sees what failed and why on the next turn.
The deeper reason this matters: a tool_result carrying a structured, actionable error keeps the loop alive. The model reads "the date must be in YYYY-MM-DD format" and reissues a corrected call, instead of dead-ending on an exception your code swallowed. Strict mode (Pattern 2) eliminates malformed-argument errors up front; instructive error messages handle the runtime failures strict mode can't — a 500 from upstream, a rate limit, an empty result set.
Debugging table: symptom to fix
When an agent misbehaves on tools, walk these in order — each rules out a class of failure before you reach for the next knob:
| Symptom | First fix to try | Pattern |
|---|---|---|
| Picks the wrong tool / no tool | Rewrite the description (scope + when-not-to-use) | 1 |
| Sends malformed / missing arguments | Turn on strict mode | 2 |
| Answers in prose when it should act | Force with tool_choice (any/required) |
3 |
| Calls three tools for one task | Consolidate into one tool with an action param |
4 |
| Context blows up after a tool call | Trim and paginate the tool's return value | 5 |
| Gives up the moment a tool errors | Return is_error with an instructive message |
6 |
Which doc backs which claim
The two doc sets in Sources are the spine of everything above. Anthropic's "Define tools" page is where the "by far the most important factor in tool performance" line and the "at least 3-4 sentences" rule live, along with the four tool_choice values (auto/any/tool/none) and the prefill behavior under any/tool; its "Strict tool use" page covers strict: true as a top-level field requiring additionalProperties: false; "Handle tool calls" covers the tool_result ID matching and is_error. On OpenAI's "Function calling" guide sit the "fewer than 20 functions" soft suggestion, the strict-mode requirement that every property appear in required with null-typed optionals, allowed_tools, and the parallel_tool_calls: false "ensures exactly zero or one tool is called" wording. This piece asserts no pricing, model IDs, or version numbers — but parameter names and defaults are exactly the kind of thing providers revise between doc releases, so treat a specific flag spelling as good as of the linked page, not forever.

Top comments (0)