Tools & AI Agents

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.

The big question

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.

Tests — Run & Watch Them Pass

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.

Everyday analogy

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.

Tests — Run & Watch Them Pass

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.

Tests — Run & Watch Them Pass

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.

Rendering diagram…

Results are context, and context is untrusted

What comes back from a tool goes into the conversation. That has two consequences people underestimate.

Tests — Run & Watch Them Pass

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.

A little history

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.

Tests — Run & Watch Them Pass

Run to execute the tests…

The engineer's notebook

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.

In the real world

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.

Thought experiment

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 not 5000, and string arithmetic with money is a real production incident.

  • Accepting undeclared parameters. Models invent plausible ones, and approved: true is 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

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
How does function calling work — what is the model actually doing?
Model answer — You declare a set of tools with names, descriptions and parameter schemas, and those go into the request alongside the messages. When the model decides a tool would help, it doesn't call anything — it can't, it has no network or filesystem — it emits a structured message saying it would like to call this tool with these arguments. Your code receives that, decides whether to honour it, runs whatever it runs, and passes the result back as a new turn. Then the model continues with the result in context. The part I'd emphasise is that gap: the model proposes and your code disposes, so every check — is this session allowed this tool, are the arguments valid, does this need confirmation — happens in ordinary code you control. The tool definitions matter more than people expect, too, because the description is what the model uses to choose. It should say when to use the tool, not what the tool does internally.
Intermediate
Your agent has a tool that looks up a customer's orders. What can go wrong?
Model answer — The one that worries me most is identity. If the customer id is a tool parameter, the model supplies it, and a user only has to say "check my colleague's order" for the model to helpfully pass someone else's id — no malice required, and the handler happily complies. Identity has to come from the authenticated session and any identity field in the arguments gets discarded, not validated. Beyond that: argument types, since a number can arrive as a string and downstream arithmetic goes wrong silently; unconstrained strings where a pattern would have caught a malformed reference; and undeclared parameters the model invented because they sounded like they should exist. Then the result side — an unbounded query can put thousands of tokens into the context, which is resent every turn, so I'd bound it in the handler and say explicitly when it truncated rather than letting the model summarise twenty rows as if they were all of them. And I'd treat what comes back as untrusted, because a notes field or a support ticket can contain text that reads like an instruction and arrives with the authority of my own system. That last one is the reason authorisation can't live in the prompt.
Advanced
Design the tool layer for an agent that can take real actions — refunds, cancellations, account changes.
Model answer — A single wrapper every tool goes through, doing five things in a fixed order before any handler runs: check the session is permitted this tool, validate arguments against a schema, strip identity fields and bind them from the session, apply a per-tool call budget, and require an idempotency key for anything with a side effect. A wrapper rather than a convention, because conventions fail — someone adds a tool in a hurry and reads args.userId because it's sitting right there, and the review doesn't catch it because the line looks normal. If the arguments object physically can't contain a userId by the time a handler sees it, that bug is unwritable. On the surface itself: few coarse-grained tools rather than many fine-grained ones, since selection accuracy degrades measurably as options multiply and thirty definitions is over two thousand tokens on every call. Read-only tools built first — about half the time the destructive tool turns out to be unnecessary, because the agent's real job was to prepare an action for a human. For the ones that survive, a confirmation gate in code, an idempotency key derived from the operation rather than generated per attempt, and an audit record of the proposed call, the decision, and the actual effect — the proposal and the effect being different things is the whole point. Errors returned as data with the specific field named, classified into retryable and permanent so the model can recover from a timeout without looping on "not found". Results bounded and labelled as untrusted data. And I'd want evals specifically for tool selection and argument construction, separate from answer quality, because a system that picks the right tool with wrong arguments and one that picks the wrong tool are different bugs that a single end-to-end score can't tell apart.
Company lens — how the same topic gets asked
micro1Implement a tool-calling loop with validation. They watch whether you validate arguments and whether authorisation reads the session rather than the request.
GoogleMechanism and design: how tool schemas constrain decoding, why selection degrades with surface size, and how you would measure tool-choice accuracy.
MetaProduct surface: tool granularity, context-dependent tool filtering, and latency when several tools are called per turn.
AmazonBlast radius: confirmation gates, idempotency, audit trails, and what your evidence is that an agent cannot act outside a user's permissions.
Hands-on

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.

Tests — Run & Watch Them Pass

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.

Editorial reflection

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.