Back to the map
AgentsCore7h

Tool calling

The model asks; your code acts.

A model can only produce text, so by itself it cannot look anything up, do arithmetic you would trust, or change anything in the world. Tool calling is the arrangement that fixes this. You describe the functions available to it — their names, what they do, what arguments they take — and when the model wants one, its reply is a structured request to call it. Your code runs the function and sends the result back as another message.

Without it a model is confined to what it absorbed in training and whatever you pasted into the prompt, which rules out anything current, private or actionable. The detail that matters is that the model never runs anything itself; it only asks. What is permitted, what happens on failure, and what happens when the same call arrives twice are all your code's decisions, which is where the security and reliability story lives.

It builds on structured output, because the request to call a tool is a schema-constrained object. It is the single piece that turns a model into an agent: put it in a loop and you have the agent loop, standardise how the tools are described and you have MCP, and offering retrieval as a tool is how RAG is usually wired up now. Prompt injection is the risk it introduces.

The model asks for a tool; your code decides whether and how to run it.

In more detailYou describe functions with a schema, the model emits a structured request to call one, your code executes it and returns the result as another message. The model never runs anything itself. The engineering lives in the unglamorous parts: parallel calls, malformed arguments, tools that throw, and deciding what a failure should look like to the model.

Section 1 of 2, 3h

How tool calling works

By the end: You can give a model tools and handle the full round trip correctly.

  • What actually happens during a tool call
  • Describing a tool so the model uses it correctly
  • Executing the call and returning the result
  • The fact that the model never runs anything

Everything so far has ended with the model producing an answer. Tool calling is how it gets to do more than that: you describe the actions available, the model asks for one, and your own program carries it out and reports back. This section follows that round trip a step at a time, because the division of labour within it is what people misread, and misreading it leads to designs that assume the model can reach out into the world by itself.

The model asks, your code acts#

This is the point people misunderstand. The model cannot run code, call an API or touch a database. It can only produce text. Tool calling is a convention where part of that text is a structured request, which your program reads and acts on.

1. You send:     messages + a list of available tools
2. Model replies: "call get_weather with {city: 'Berlin'}"
3. YOUR code runs get_weather("Berlin")  ->  "18C, cloudy"
4. You send:     everything again, plus that result
5. Model replies: "It is 18 degrees and cloudy in Berlin."
Step 3 is your program. The model paused and waited for you.

Note steps 4 and 5. A tool call always needs a second request, because the model has to see the result before it can use it. One user question becomes at least two API calls.

Describing a tool#

A tool definition is a name, a description and a schema for its arguments. The description is not documentation for you. It is the only thing the model uses to decide whether this tool is the right one, so it should say when to use it, not just what it does.

{
    "name": "search_orders",
    "description": (
        "Find a customer's orders by email address. "
        "Use this when the user asks about order status, delivery "
        "or refunds. Returns at most 10 of the most recent orders."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "email": {
                "type": "string",
                "description": "The customer's email address",
            },
            "status": {
                "type": "string",
                "enum": ["pending", "shipped", "delivered"],
                "description": "Optional filter by order status",
            },
        },
        "required": ["email"],
    },
}
The enum is doing real work. It makes an invalid status value impossible rather than unlikely.

Vague descriptions cause tool confusion

If two tools have similar descriptions, the model picks the wrong one and you will blame the model. Say explicitly when each should be used, and when it should not. Of everything you can edit in an agent, this does the most good for the least effort.

The round trip in code#

messages = [{"role": "user", "content": "Where is my order?"}]

response = client.chat(messages, tools=TOOLS)

if response.stop_reason == "tool_use":
    for call in response.tool_calls:
        result = TOOL_FUNCTIONS[call.name](**call.arguments)

        messages.append(response.message)          # what the model asked
        messages.append({                          # what it got back
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

    response = client.chat(messages, tools=TOOLS)  # ask again, with results

print(response.text)

Both appends are required. The model's request has to stay in the history, or the result arrives with nothing to attach to and the model loses track of what it asked.

Where people go wrong

Forgetting to append the model's tool request to the history before appending the result. The conversation then contains a result with no matching request, and the model either repeats the call or gets confused about what it was doing.

Build this: Give a model two tools with deliberately similar descriptions and watch it pick the wrong one. Then rewrite the descriptions to say when each applies, and confirm the choice becomes reliable.

Open this section on its own page
Section 2 of 2, 4h

Failures, parallel calls and keeping results small

By the end: Your tools survive errors, run concurrently where possible, and do not flood the context.

  • Returning errors as results rather than raising
  • Several tool calls in one turn
  • Validating arguments before executing
  • Why tool output size matters

A tool that behaves in a demo behaves differently under real traffic: arguments arrive malformed, an API times out, a query comes back with half a megabyte of rows. This section covers what to do in each case, and the answer is usually not what an exception handler would normally do. Done well, these are recoverable situations the model works around on its own; done badly, one of them ends the run or quietly fills the window with output nobody will read.

An error is a result, not a crash#

When a tool fails, the natural instinct is to let the exception propagate. That ends the run. Usually the better move is to hand the failure back to the model as the tool's result, because the model can often recover: fix a typo in an argument, try a different approach, or tell the user what went wrong.

def run_tool(call):
    try:
        return {"ok": True, "data": TOOL_FUNCTIONS[call.name](**call.arguments)}
    except KeyError:
        return {"ok": False, "error": f"No tool named {call.name}"}
    except Exception as e:
        return {"ok": False, "error": f"{type(e).__name__}: {e}"}
The model reads the error text, so make it descriptive. 'Customer not found' is actionable; 'KeyError' is not.

Some failures should still stop everything. An authentication error will not be fixed by the model retrying, and letting it try ten times wastes money and time. Distinguish recoverable from terminal deliberately.

Several calls at once#

A model can request more than one tool in a single turn when the calls are independent. Run them concurrently, since they are usually network-bound, and then return every result together in one message.

results = await asyncio.gather(*(run_tool(c) for c in response.tool_calls))

messages.append(response.message)
for call, result in zip(response.tool_calls, results):
    messages.append({
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result),
    })

Return all results in one message

Splitting tool results across several separate messages quietly teaches the model to stop making parallel calls, because the pattern it sees no longer matches the pattern it produced. Your agent gets slower over a conversation and the cause is very hard to spot.

Validate before you execute#

The model generates arguments, so treat them as untrusted input. Schema constraints catch type errors, but they do not catch a valid-looking value that is wrong, and they certainly do not catch a destructive one.

def delete_records(query: str, confirm: bool = False):
    if not confirm:
        return {"ok": False,
                "error": "Destructive action needs confirm=true and human approval"}
    ...
Make the dangerous path require an extra deliberate step rather than trusting the model not to take it.

Keep results small#

Tool results go into the context and stay there for the rest of the conversation, being resent every turn. A tool that returns a 5,000-token payload has just made every subsequent turn more expensive, permanently.

  • Return the fields the model needs, not the whole database row.
  • Cap list results, and say so in the response, for example that ten of forty-two matches are shown.
  • Summarise long text before returning it, or return an identifier the model can use to fetch more.
  • Prefer several small focused tools over one that returns everything.

Where people go wrong

Letting a tool return whatever the underlying API returns. One verbose response inflates every later turn in that conversation, and the cost increase is invisible until you look at a trace.

Build this: Build three tools where one throws, one returns a very large payload and one is slow. Make the agent handle all three without crashing, without bloating the context, and without running them one after another.

Open this section on its own page

Where you use this

A topic is easier to care about when you can see what it is for. These projects lean on it at a named stage.

An agent that uses tools
  • Stage 1, One tool, and the schema that describes it: What actually crosses the wire: the tool definition going out, the tool_use block coming back.
  • Stage 3, A second tool, and the model choosing wrong: Several tool_use blocks in one turn, and keeping each result small enough to be worth returning.

Now actually do it

Reading a topic is not learning it. These are the hours that count, and each one tells you when you are finished.

1Make the model pick the wrong tool

3h

Define two tools with deliberately similar descriptions and watch it choose badly. Then rewrite the descriptions to say when each applies and confirm the choice becomes reliable.

Done when: You have seen tool confusion and fixed it with description text alone.

2Three hostile tools

4h

One throws, one returns a 5000 token payload, one takes 30 seconds. Make the agent handle all three without crashing, without bloating the context, and running the independent ones concurrently.

Done when: Context size after ten turns is bounded, and parallel calls demonstrably overlap.

7 hours of practice

Words used here

2 terms this topic teaches, each with a one-line definition if one of them stopped you.

Tool calling
Giving a model a set of described functions it may ask for, so that instead of answering it can emit a structured request that your code executes and feeds back.
Function calling
The original name for tool calling, still used in several APIs and in most older documentation. The same mechanism: a schema of available functions in, a structured invocation out.

Interview questions

8 questions, grouped by the section that teaches the answer. An answer you have read is not an answer you can give, so use Test yourself and say it out loud first.

How tool calling works

Walk me through what happens when a model calls a tool.

You send messages plus tool definitions. The model responds with a structured request naming a tool and arguments rather than a final answer. Your code executes that function, appends both the request and the result to the history, and calls the model again. The model then answers using the result. It never executes anything itself.

Then they push further

Where does the tool actually run?

In your application. The model only produces a structured request naming a tool and its arguments. Your code validates it, decides whether to run it, runs it, and sends the result back in the next request.

What do you check before running it?

That the tool exists and is allowed for this user, that the arguments pass the schema, and that the values make sense, such as an order id that belongs to this customer. The model's request is untrusted input.

What if the model asks for a tool that does not exist?

Return an error result that says so and lists what is available, and let it try again. If it keeps happening, the tool names or descriptions are probably confusing.

What gets marked up. Marked up for treating the model's tool request as untrusted input that must be validated and authorised. Weak answers imply the model runs the tool itself.

What makes a good tool description?

It says when to use the tool, not only what it does, and it says what the tool returns. The model uses the description to choose between tools, so vague or overlapping descriptions cause it to pick wrongly.

How many API calls does one tool-using question take?

At least two. One where the model requests the tool, one where it sees the result and answers. More if it chains several tools. This is why agents are slower and more expensive than a single completion.

Why use an enum in a parameter schema?

It constrains the argument to valid values at generation time, so an invalid status or category cannot be produced. It also removes a whole class of validation code from your handler.

Failures, parallel calls and keeping results small

A tool throws an exception. What should happen?

In most cases return the error to the model as the tool result, so it can recover, retry with different arguments or explain the problem. Terminal failures such as authentication errors should stop the run instead, because retrying cannot help.

How do you handle a model requesting several tools in one turn?

Execute them concurrently since they are usually network-bound, then return all results together in a single message. Splitting them across messages trains the model out of making parallel calls.

Why does the size of a tool result matter?

It enters the context and is resent on every subsequent turn, so a large result raises the cost of the whole remaining conversation. Return only what is needed and cap anything unbounded.

How do you stop an agent taking a destructive action?

Architecturally, not by instruction. Scope the tool's permissions, require an explicit confirmation argument, add a human approval gate, and make the operation idempotent so a repeat is harmless. Prompt instructions alone are not a security boundary.

Then they push further

Would you rely on the system prompt saying never delete anything?

No. Instructions make it rarer but they are not a control. The control is in code: do not give the agent a delete tool, or require human approval before one runs, and scope credentials so the action is impossible even if requested.

Which actions need a person in the loop?

Ones that are irreversible, costly, or visible to other people: payments and refunds above a threshold, deletions, messages sent to customers. Reads and reversible changes can usually run unattended.

What gets marked up. Strong answers put the control in permissions and code, not in the prompt, and reason about reversibility. Weak answers rely on instructions.

These questions on their own page, one link per question

Resources

Free to watch and read, picked for this topic.

For failures, parallel calls and keeping results small

Stop 15 of 50 on the AI Engineer route

0 of 50 topics done · 0h behind you, 603h to go

Next on this routeThe agent loopA while loop with tool dispatch and a stop condition.14h

What you have marked done is stored in this browser only. It does not reach a server, and clearing site data will clear it.