---
title: "Tool Calling"
description: "The model never runs anything — it returns JSON and stops; your code executes and reports back. One tool call, two round trips."
source: https://vizlearn.app/ai/agents/tool-calling
---

# Tool Calling

Tool calling (also called function calling) is how an LLM uses external code: you send the conversation plus tool schemas, and instead of answering, the model can return a structured JSON request naming a tool and its arguments. Your application runs the function, appends the result as a message, and calls the API again so the model can finish.

Here's the mental model that makes everything else obvious: the model is a brilliant consultant who works entirely by mail and keeps no files. Every letter you send must contain the whole case history plus a menu of services you're willing to perform. The reply is either an answer or a work order — "run `get_weather` on Tokyo, please" — and then the consultant just sits there. They never stand up. You run the errand, write the outcome into the next letter, and mail the whole file again. The consultant burns the case file after every reply. Once you see tool calling as two letters instead of one magic phone call, every confusing detail (the stop, the second request, the role names) snaps into place.

## How Tool Calling Works

Three parties, with the application mediating each exchange in this client-tool example. Your application sits in the middle: on one side is the model, across the network behind an HTTP API; on the other side is the tool — which is nothing exotic, just a function in your codebase, `def get_weather(city): ...`. There is no wire between the model and the tool. The model cannot reach your function any more than a letter can cook dinner.

The protocol is a loop:

```python
def ask(question: str) -> str:
    messages = [{"role": "user", "content": question}]
    while True:
        resp = claude.create(messages=messages, tools=TOOL_SCHEMAS,
                             tool_choice=CHOICE)
        messages.append(assistant(resp.content))
        if resp.stop_reason != "tool_use":
            return text_of(resp)
        results = []
        for call in tool_uses_of(resp):
            try:
                results.append(TOOLS[call.name](**call.input))
            except Exception as e:
                results.append(f"error: {e}")
        messages.append(tool_results(results))
```

Walk the moving parts. `TOOL_SCHEMAS` is documentation, not code: each schema is a name, a natural-language description, and a parameter spec. The model picks tools by *reading the descriptions* — write them like you'd explain the tool to a new colleague, because that's literally how they're used. The response's `stop_reason` is the hinge of the whole mechanism. `"end_turn"` means "I'm done, here's your answer." `"tool_use"` means "I stopped on purpose: there's a work order in my reply." At that moment nothing is running anywhere — the HTTP call is over, the model is idle, and the next move belongs entirely to your code.

One line in that loop carries a lesson of its own. `TOOLS[call.name]` is a dispatch map — a dictionary from a tool's name to the function that runs it. Give the agent a tenth tool, or a hundredth, and you register one more entry and write one more function; the loop never gains a branch. The model's growing reach lives entirely in the schema list and the handler table, never in the control flow. That keeps the dispatch loop stable as tools are added, though each tool still needs an implementation, schema, validation, and policy. It is also why the same loop runs whether the agent can do one thing or a thousand.

When you run the function and send back the result, two details matter. First, the result travels inside a `tool_result` block that rides a **user-role** message. That sounds wrong and is exactly right: from the model's point of view, tool output is just more input from the outside world — the same direction as everything else you've ever told it. Second, the new request contains *everything*: the original question, the model's own tool_use reply, and the result. This client-managed Messages API interaction supplies its history on each call; other APIs or hosts can retain conversation state. You're not "continuing" anything; you're mailing the full case file again, one letter thicker.

This lesson is the mechanism underneath agents. The [ReAct Loop](https://vizlearn.app/ai/agents/react-loop) lesson shows what gets built on top — Thought → Action → Observation as a reasoning pattern. That lesson compresses the tool plumbing into a single "runtime" box; this one opens the box.

## Tool Calling Step by Step

The default scenario: the user asks **"What's the weather in Tokyo right now?"**, and you've offered one tool, `get_weather(city)`. Trace it on paper:

1. **Setup.** `messages = [user: "What's the weather in Tokyo right now?"]`. One message, plus the `get_weather` schema waiting in `TOOL_SCHEMAS`.
2. **Build request #1.** The POST body is assembled from scratch: 1 message + the tool schemas.
3. **Round trip #1.** The request crosses the network. The model reads the question, reads the schema description, and decides it can't know today's weather from training data.
4. **The response is a work order.** Content: `{"name": "get_weather", "input": {"city": "Tokyo"}}`. And the field to read: `stop_reason: "tool_use"`. The model wrote *text describing a call*. It executed nothing.
5. **Record it.** The assistant's tool_use reply is appended to `messages` — the work order is now part of the conversation history.
6. **Check the stop.** `stop_reason == "tool_use"`, so the code's guard — `if resp.stop_reason != "tool_use"` — is false: the early return is skipped and the loop continues toward the tool. The model is idle. Your code is on the clock.
7. **Parse.** Your runtime lifts `name` and `input` out of the JSON: `get_weather(city="Tokyo")`. Data became a call because *you* turned it into one.
8. **Execute.** Your process runs the function. The model is not connected, not consulted, not aware.
9. **Return.** The function hands back a plain string: `18°C · rain`.
10. **Record the result.** `messages.append(tool_result(...))` — a user-role message. The history is now: question, work order, outcome.
11. **Build request #2.** All three messages + the schemas again. The envelope got visibly fatter; nothing was remembered for you.
12. **Round trip #2.** Same endpoint, same shape, bigger payload.
13. **The response is an answer.** "It's currently 18°C with rain in Tokyo. You might want an umbrella if you're heading out!" — and `stop_reason: "end_turn"`. The 18°C is a quotation of your function's return value, not a memory.
14. **Record it, exit the loop.** The guard is true; `return text_of(resp)`.
15. **The bill.** 2 API calls, 1 tool run. That's what one tool call costs: two letters and one errand.

## Why Two Round Trips?

Because this client-tool protocol completes the model response at tool_use and requires the application to submit the result in another request. When the model decides it needs the weather, the only thing it can do is *end its turn and say so* — there's no callback channel, no "hold the line while your app fetches." The request-response cycle closes, and reopening it is a brand-new API call carrying the full conversation.

This has consequences you should actually budget for. Latency: one tool call costs two full model invocations plus your tool's own runtime, serially. Tokens: the entire history (and the schemas) is re-sent and re-read on every round trip, so long conversations pay for their past repeatedly — this is exactly what prompt caching exists to soften. And every retry is not a freebie: in the error scenario, one inexact argument (`"Springfield"` against a service that only knows `"Springfield, Missouri"`) turns 2 API calls into 3. Errors are recoverable *because* they're just messages, but recovery is priced in round trips.

The flip side is the elegant part: because everything is messages, the model needs no special machinery to handle tool output, tool errors, or even multiple tools — it's all just context. A failed call with `is_error: true` is read the same way a successful one is, which is why the model can read an error that names the valid keys and retry with one of them on the next turn — in this lesson's recorded error run, the corrected `"Springfield, Missouri"` argument came, verbatim, out of the error text.

## Edge Cases

- **No tool needed.** Offering tools doesn't force their use. Ask "What does HTTP stand for?" with the weather tool attached and you get `end_turn` on the very first response: 1 API call, 0 tool runs. The model chooses based on whether the question needs the world.
- **The tool throws.** Don't crash, and don't swallow it: catch the exception, stringify it, and send it back as a `tool_result` with `is_error: true`. The model treats the failure as information — in this lesson's recorded error scenario it retries with the exact key the error message listed.
- **`tool_choice: "required"` in the lesson UI.** This is the demo’s normalized label; Anthropic’s native forced-any shape is `{"type":"any"}`, not this literal string. Flip the lesson's tool_choice toggle on the no-tool question and watch a parameter outvote the model: the forced-use setting requires a tool request in that round, so the model is forced to invent an argument for a question that needs none — a wasted run whose result buys nothing. `auto` lets the model judge; `required` (and its cousin, forcing one specific tool) takes that judgment away. Useful when your pipeline depends on structured output; absurd when it doesn't. One more sharp edge: `required` is per-request, and if your code keeps sending it, the model is never allowed to answer — every round must be another tool call. The lesson's app flips back to `auto` after the forced round; yours should too. Check your model before relying on forcing at all: Claude's newest models (Opus 5.5, Sonnet 5.5, Fable 5.1) reject `{"type":"any"}` and `{"type":"tool"}` with a 400 error, while Opus 5, Fable 5 and OpenAI's `"required"` still force. On those models, use `auto` with an instruction to call the tool, and `strict: true` when you need schema-valid input. Because this lesson records claude-opus-5-5, its forced rounds are simulated: the recorder tells the model, in the prompt, that a text-only reply is not allowed. That is weaker than real forcing — one of three recorded samples answered in text anyway.
- **Parallel tool calls.** A response may contain *several* tool_use blocks at once — the lesson's parallel scenario animates it: ask about Tokyo *and* San Francisco and the recorded model batches two blocks into one reply. Your code runs both, and both `tool_result` blocks ride ONE user message, matched to their blocks by `id`. Two lookups cost two tool runs but only one extra round trip — batching is the model's lever on latency.
- **Hallucinated calls.** The model can emit a tool name that doesn't exist or arguments that don't validate. Your runtime is the type checker: validate before executing, and report mismatches back as error results, not stack traces.
- **Unbounded loops.** `while True` is honest for a lesson and reckless in production — a model that keeps requesting tools will keep costing round trips. Cap iterations and fail loudly.

## Common Mistakes

- **Believing the API runs your function.** It can't. The "call" in tool calling is a *request for* a call. For this client-side tool, the application must validate, authorize, and dispatch the request; merely receiving it is not execution. Provider-hosted server tools are a different arrangement.
- **Dropping messages from the history.** The next request must include the assistant's tool_use message *and* your tool_result, in order, ids matched. Send the result without the work order it answers and the API will reject the mismatch.
- **Assuming the model remembers round trip #1.** It doesn't. If you send only the new result, the model has no idea there was ever a question. Statelessness isn't a quirk to work around; it's the contract.
- **Wrong role on the result.** In the Anthropic API, `tool_result` rides a **user** message. (OpenAI Chat Completions uses different fields: `finish_reason: "tool_calls"` and a dedicated `role: "tool"` message — same dance, different costume.)
- **Schemas written like code comments.** The description and schema are important tool guidance, alongside any instructions and conversation context. "Gets weather" teaches it nothing; "Returns current temperature and conditions; the city must match the service's listing exactly, e.g. 'Springfield, Missouri'" — or better, an enum of valid keys in the parameter spec — would have spared this lesson's error scenario its extra round trip.

## A Note on Simplification

The model decisions shown here — which tool to call, with what arguments, and every final answer — are recorded verbatim from real claude-opus-5-5 runs, one selected per scenario and left unedited. The `required` cells simulate forcing with a prompt constraint, because this model rejects a forced `tool_choice`. `get_weather` reads from a three-city table instead of a live API, but it still executes: every result string on screen is the function's actual return value, including the exception in the error scenario. The demo leaves out multiple tool types, streaming, and prompt caching. The Python listing is explanatory pseudocode: helpers must preserve call IDs and error flags, and production code must distinguish other stop reasons, validate and authorize calls, and enforce budgets. The recorded demo resets required to auto after the first round, which the compact listing’s CHOICE placeholder does not expand. For this client-tool Messages API, the protocol shape is: the stop, the JSON work order, the user-role result, and the full-history resend are exactly how production tool calling works.
