Tool Calling / Function Calling
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This is the mistakes post, and the cover sets the tone bluntly: tool calling looks trivial in a demo and breaks in subtle, expensive ways in production. The failures are rarely clean crashes — the model calls the wrong tool because a description was vague, hallucinates an argument no one validated, runs a dangerous action that was never gated, or the loop hangs because a result was never returned.
The through-line for the whole post is two habits: treat tool definitions as part of your prompt, and treat tool arguments as untrusted input. Adopt both and most of these failure modes simply disappear.
The first mistake is underestimating how completely the model depends on tool descriptions. It chooses among tools using only their names, descriptions, and schemas — never the source code. A tool named 'process' with an untyped 'data' field is an open invitation for the model to call the wrong thing or pass garbage arguments.
The fix is to write each description as if briefing someone who will only ever read that one paragraph: state what the tool does, when to use it, and type every parameter. Tool descriptions are prompt engineering, not afterthought documentation — they directly determine which tool the model picks and how well it fills the arguments.
The compare diagram makes the first mistake tangible by putting a vague spec beside a clear one. On the left, a tool named 'handle' described as 'does stuff with data' with untyped parameters leaves the model guessing. On the right, 'refund_order' described as 'refund an order by its id,' with a typed JSON schema and enums, leaves no ambiguity about what it does or how to call it.
The contrast drives home that the name and description aren't human documentation — they're the model's entire interface to the tool. A clear spec is the cheapest, highest-leverage improvement you can make to a system that's calling the wrong tools or mangling arguments.
The second mistake is trusting the arguments the model generates. Because the model produces those arguments, it can hallucinate an order_id that doesn't exist, pass a negative quantity, or supply a file path that escapes your sandbox. The arguments are model output, which makes them untrusted input by definition.
The fix is to validate types, ranges, and existence before acting — exactly as you would for data submitted through a web form. A confident-looking tool call is not a guarantee of valid arguments. Treating every argument as hostile until checked is the single most important habit for tool calling that touches anything real, and it's why this slide pairs with a concrete validation example.
This code slide shows argument validation as concrete defensive code. The refund_order function first looks up the order and returns an error if the id doesn't exist — never trusting the model-supplied id. Then it checks that the amount is positive and not greater than the order total, returning an error if not — never trusting the model-supplied number. Only after both checks does it perform the real refund.
The pattern is to validate every argument against reality before acting, and to return a clear error string the model can read and react to rather than throwing. That error becomes the tool result, so the model can correct course on the next turn instead of the whole interaction crashing.
The third mistake is a protocol error rather than a logic one: every tool_call the model makes must receive a matching result message, keyed by its tool_call_id. Skip it and the conversation is malformed. Depending on the SDK, the API will reject the next request, or the model will sit waiting for an answer that never arrives.
The rule is simple and absolute: one call out, one result in, every time — and with the matching id. This bites most often when the model emits several parallel calls and the code handles only the first, or when an exception in your tool execution skips the append. The result message is not optional; it's how the loop closes.
The flow diagram makes the third mistake's fix visual: a tool_call with id call_01 leads to you running it and getting output, which you return as a tool result echoing tool_call_id call_01, after which the model can answer and the loop closes. Each node depends on the previous one, so dropping any link leaves the conversation hanging.
The diagram emphasizes the id appearing on both the call and the result. That repetition is the whole point — the result is only valid if it carries the id of the call it answers, which is also what makes parallel calls work, as covered in post 3.
The fourth mistake is the most dangerous: exposing irreversible actions without a guardrail. The model will eventually call any tool you give it, including delete_user, charge_card, or send_email. Combine that with the second mistake — hallucinated arguments — and a single bad call on a destructive tool becomes a real incident affecting real users or money.
The fix is layered defense: never wire an irreversible action straight through. Put scoped permissions, a confirmation step, or a human approval in front of it. This is exactly where the post-1 boundary pays off — because execution happens in your code, you can intercept any call and gate it before it ever runs.
This code slide shows the guardrail concretely. You maintain an allowlist of safe, read-only tools — search_docs, get_weather, get_order — that execute freely. Any tool not on that list routes through a human approval check before running and is denied if the reviewer says no. The arguments are parsed and passed along only after the gate is cleared.
The pattern is deny-by-default for anything that can cause real-world change, with explicit approval required to proceed. It turns the model's autonomy into a proposal rather than a command for the risky operations — precisely the posture you want when an agent has access to tools that spend money or delete data.
The fifth mistake is assuming a tool call always happens. Sometimes the model replies in plain text instead — either because it already knows the answer, or because your description didn't make the tool's purpose clear enough to trigger it. Code that blindly reaches for msg.tool_calls[0] will crash on those replies.
The fix is to always branch: if there's a tool call, run it; otherwise, take the text content as the answer. This 'no tool' path is also a useful diagnostic — if the model keeps answering directly when you expected a tool call, that's a signal your tool description needs to be clearer, looping right back to mistake one.
This code slide shows the correct branching pattern. Read the model's message, and if it has tool_calls, iterate over them — note the comment that there may be several, guarding against the parallel-call case — and run each. Otherwise, return the plain content as the answer.
The two-branch structure is the minimal correct way to consume a tool-calling response. It handles the happy path, the parallel-call path, and the no-tool path in a few lines, and it prevents the crash that comes from assuming a tool call is always present. It's small, but it's the difference between code that works in a demo and code that survives real inputs.
The checklist slide gathers all five fixes into a scannable pre-flight list. Write tool specs like a precise API doc. Treat every argument as untrusted input. Return a result for every tool_call_id. Gate irreversible actions behind approval. And handle the no-tool-call branch every time.
Run down this list and you avoid every failure mode in the post. It's ordered roughly by how early each one bites — vague specs and bad arguments hurt immediately, while the no-tool branch is the subtle one that crashes you on an input you didn't test. Together they turn fragile demo code into something safe to ship.
The CTA closes both the post and the day. The reader has now seen tool calling from five angles: what it is, why it changed everything, how the round-trip works, how to build it end to end, and how it fails in production. They have a complete, deployable mental model of the primitive that sits under agents, RAG, and copilots.
The teaser keeps the series momentum without promising a specific topic, pointing simply to the next entry in the 100 Days of AI. The reader leaves equipped to wire tool calling into real systems and, just as importantly, to make it robust enough to trust.