Lesson 1 of 5
Tool & Function Calling
"Man is a tool-using animal… Without tools he is nothing, with tools he is all." — Thomas Carlyle, Sartor Resartus (1834)
Everything so far has been about text. The model reads a prompt and writes a response, and however sophisticated the retrieval feeding it, the interaction is closed: nothing the model produces reaches beyond the string it returns to you. This chapter opens that boundary. Tool calling lets a model request an action — look up an order, run a query, send an email, book a slot — and it is the mechanism that turns a system that describes things into a system that does them.
It is also the point at which the failure modes stop being embarrassing and start being expensive. A hallucinated sentence is a bad answer. A hallucinated argument to issueRefund is money leaving your account. Everything in the preceding parts — grounding, validation, evaluation — was practice for this, and the discipline required here is strictly greater, because the output is no longer something a user reads and judges. It is something your code executes.
Which brings us to the single most important fact in this chapter, and the one most obscured by the name of the technique: the model does not call anything. It cannot. It has no network access, no filesystem, no ability to invoke a function. What it produces is a structured message saying I would like to call getOrderStatus with {orderId: "ORD-8871"} — and that message is text, in the same output stream as everything else it generates. Your code receives it, decides whether to honour it, executes something, and passes the result back.
That gap is not an implementation detail. It is the entire security architecture. The model proposes; your code disposes. Every safety property you need — authorisation, validation, rate limiting, confirmation for destructive actions, audit logging — lives in the gap, in ordinary code you control. Teams who internalise this build systems that fail safely. Teams who think of it as "the model calling my function" write authorisation into the tool description and discover, later and publicly, that a description is a suggestion.
If a tool call is just text the model emits — a request your code may accept or refuse — then which parts of "can this user do this action" belong in the tool's description, and which belong in your code? The answer is more lopsided than most implementations assume.
The round trip
Start with the mechanics, because the shape of the exchange explains most of what follows.
Run to execute the tests…
The third test is the one to hold on to. The model asked; the answer was no; and nothing about the request could have changed that, because the check does not read the request. That is what a security boundary looks like, and it is available to you for free — it just has to be in code rather than in prose.
A customer at a bank counter says "please transfer two hundred thousand naira from account 0123 to account 4567." They have not moved any money. They have made a request, in words, to someone whose job is to check whether it should happen — is this their account, do they have the funds, is the amount within limits, does this need a second signature. The teller is not being obstructive; the teller is the system. Now imagine a bank that decided the request itself should carry the authorisation: the customer writes "I am authorised to do this" on the slip and the teller complies. Nobody would build that, and yet it is a fair description of putting "only use this tool for the current user's own orders" in a tool description and considering the matter handled.
Tool definitions are prompts
The model chooses which tool to call, and what to pass it, entirely from the names, descriptions and schemas you provide. Those are not documentation. They are the prompt for a selection decision, and they should be written with the discipline Part III demanded of any other prompt.
Run to execute the tests…
The fifth test names a trade people get backwards. A large tool surface makes an agent worse, not richer. Selection accuracy degrades as the model has more options to distinguish between, and thirty tool definitions is also two thousand tokens on every call. Prefer few coarse-grained tools, and where the surface genuinely must be large, filter it by context before offering it.
Never trust the arguments
Tool arguments are model-generated text. They are exactly as trustworthy as anything else the model produces, which is to say: plausible, usually right, and occasionally invented.
Run to execute the tests…
That last test deserves to be underlined. Identity comes from the session, never from the arguments. A user who says "look up the orders for user u999" gives the model a plausible reason to put u999 in the call, and the model has no way to know it should refuse. If your handler reads args.userId, you have built a system where asking nicely is sufficient authorisation.
Results are context, and context is untrusted
What comes back from a tool goes into the conversation. That has two consequences people underestimate.
Run to execute the tests…
The third test names the attack that makes prompt-based authorisation untenable. A tool result can carry attacker-controlled text — a database field, a support ticket written by another user, a web page — and it arrives labelled as trusted output from your own system. If the decision about whether to issue a refund is made by a model reading that context, the decision is available to whoever can write into it.
Tool use arrived in stages. Early attempts were entirely prompt-based: ReAct, published in 2022, interleaved reasoning with actions by having the model emit lines like Action: search[query] which a wrapper parsed with regular expressions. It worked well enough to be influential and badly enough to be frustrating — the model would drift out of format, invent action names, or produce arguments the parser mangled, and every application reinvented the same brittle parsing. Native function calling, which providers added through 2023, moved the format into the API: tools declared as schemas, arguments returned as structured data, the format enforced by the same constrained-decoding machinery discussed in Part II Chapter 4. That change is easy to underrate because it looks like ergonomics. What it actually did was eliminate an entire class of parsing failure and make tool use reliable enough to build products on. The security model, notably, did not change at all — it was always the caller's job to decide whether to honour the request, and it still is.
Errors, retries, and things that must not happen twice
A tool that fails should usually tell the model, not end the conversation.
Run to execute the tests…
Every tool I write now goes through the same wrapper, and the wrapper does five things in a fixed order before the handler ever runs: check the tool is permitted for this session, validate the arguments against a schema, strip any identity fields and bind them from the session instead, apply the per-tool call budget, and — for anything with a side effect — require an idempotency key. The handler receives only clean, authorised input. The reason it is a wrapper rather than a convention is that I have watched conventions fail: someone adds a tool in a hurry, reads args.userId because it is right there in the object, and the code review does not catch it because the line looks completely normal. Now the arguments object physically does not contain a userId by the time a handler sees it. The other thing I would pass on: write the destructive tools last. Building the read-only surface first forces you to discover what the agent actually needs before you hand it anything irreversible, and about half the time the destructive tool turns out to be unnecessary — the agent's real job was to prepare an action for a human to approve.
A Nigerian logistics startup gave their support agent a tool to look up shipments, with the customer identifier as a parameter — the model was instructed to use the identifier of the customer it was speaking with. It worked, and it worked for months, because the model reliably did what it was told. Then a customer, entirely without malice, typed something like "my colleague Ngozi placed an order last week under our company account, can you check it" — and the model, being helpful, passed a different customer identifier that it had inferred from earlier in the conversation. The lookup succeeded, because the handler used the identifier it was given. Nobody complained; the two people did work together. It surfaced during an unrelated audit of query logs, where an engineer noticed identifiers in tool calls that did not match the authenticated session. The fix was four lines — drop the parameter from the schema, bind from the session — and the postmortem's honest conclusion was that the system had never had an authorisation model at all. It had a well-behaved model and a description asking it politely to stay in its lane, and those had been indistinguishable from security right up until the moment they were not.
Take one tool in your system and write down, precisely, what would happen if the model called it with the worst arguments it could plausibly produce — not adversarial ones, just wrong. A refund tool with a quantity where the amount should be. A search tool with a query containing the entire conversation. A delete tool with an identifier belonging to someone else. Now ask, for each: what stops it, and is that thing code or prose? Then the sharper version — for the ones stopped by code, would the check still hold if a user pasted a document containing text that looks like an instruction, and the model read that document through another tool? Prompt-based defences fail this test by construction, because the attacker is writing into the same channel your instructions occupy. The checks that survive are the ones that never read the model's output at all.
Common Mistakes
Thinking of it as "the model calling my function". The model emits a request. Your code decides. Every safety property lives in that gap.
Authorisation in the tool description. A description is a suggestion. A check in the handler is a check.
Reading identity from the arguments. Identity comes from the session. A user can ask the model to pass a different id, and the model has no way to know it should refuse.
Descriptions that describe the implementation. "Searches the catalogue" is documentation. "Use this when the user asks about availability, price or specifications" is the decision the model has to make.
Two tools with the same description. The model will pick one arbitrarily, and differently between calls. Tools that need the same description are one tool.
Unconstrained string parameters. A pattern or enum tells the model what to produce and gives your validator something to check.
A large tool surface. Selection accuracy degrades as options multiply, and thirty definitions is over two thousand tokens on every call. Prefer few coarse-grained tools; filter by context when the surface must be large.
Trusting argument types.
"5000"is not5000, and string arithmetic with money is a real production incident.Accepting undeclared parameters. Models invent plausible ones, and
approved: trueis not harmless.Letting a tool exception escape the loop. Return the error as data so the model can retry, switch tools, or tell the user.
Generic error messages. "Invalid arguments" makes the model repeat itself. "refundAmount: above max (limit 100000)" lets it correct itself.
Not classifying errors. Let the model retry a timeout; do not let it loop on "order not found".
Unbounded tool results. Five hundred rows is nine thousand tokens, resent on every subsequent turn.
Silent truncation. The model summarises twenty rows and reports it as the whole answer. Say what was cut and what to do about it.
Treating tool output as trusted. A database field or a support ticket can carry text that reads as an instruction and arrives with your system's authority. This is the main reason authorisation cannot live in the prompt.
No idempotency on side effects. Agents re-issue calls after timeouts, retries and confused loops.
No confirmation gate on irreversible actions, and no per-tool call budget — which is how a model that misreads a result calls the same tool forever.
Interview Corner
Build defineTool({ name, description, parameters, handler, destructive, budget }) and a runToolCall(call, session) wrapper that, in order: rejects unknown tools as data, checks the session's permitted tool list without reading the arguments, validates arguments against the schema including rejecting undeclared keys, strips any identity field and binds it from the session, enforces a per-tool budget, requires an idempotency key and a confirmation for destructive tools, and bounds and labels the result. Then write the test that proves the point: a call carrying userId: "someone-else" executes against the session's user, and a tool result containing "IGNORE PREVIOUS INSTRUCTIONS" changes nothing about what is permitted.
Run to execute the tests…
Summary
- The model does not call anything. It emits a structured request; your code decides whether to honour it. That gap is the entire security architecture.
- Authorisation belongs in code, checked against the session, and it must never read the request it is authorising.
- Identity comes from the session, never from the arguments. A user can ask the model to pass a different id, and the model cannot know it should refuse.
- Tool definitions are prompts. Descriptions should say when to use a tool, not what it does internally; two tools needing the same description are one tool.
- A big tool surface makes an agent worse. Selection accuracy falls as options multiply, and definitions cost tokens on every call.
- Validate every argument: types, ranges, enums — and reject undeclared keys, because models invent plausible ones.
- Tool results are context: expensive, resent every turn, and untrusted — a database field or a ticket can carry text that reads as an instruction and arrives with your system's authority.
- Bound results in the handler and say what was truncated; shape output for the question rather than for the database.
- Return errors as data, named specifically enough for the model to correct itself, and classified into retryable and permanent.
- Irreversible actions need a confirmation gate, an idempotency key derived from the operation, and an audit record — and a per-tool call budget, without which a confused model calls the same tool forever.
In the next chapter, The Agent Loop, we let the model call tools repeatedly and decide for itself when it is finished. That single change introduces termination, progress detection, cost that compounds per iteration, and failure modes that do not exist in a single-turn system — including the agent that is confidently, expensively going nowhere.
The phrase "function calling" is one of the more consequential pieces of bad naming in this field. It puts the model in the grammatical subject position — the model calls the function — and that framing quietly imports an assumption that the model is the thing with agency, which then makes it feel natural to write instructions to it about what it may and may not do. Almost every serious tool-layer vulnerability I have seen traces back to that framing. If the technique had been named "action proposals" or "capability requests", I suspect the security posture of a great many systems would be better, because the name would have carried the architecture. What is genuinely striking is how little the underlying situation has changed since the ReAct days: it was always a wrapper reading a model's output and deciding what to do about it, and it still is. The API got structured, the parsing got reliable, and the trust boundary sits exactly where it always sat — which means the systems that get this wrong are not defeated by a subtlety. They are defeated by a metaphor.
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.