Skip to main content

Function / Tool Calling Basics

Introduction

In the last three lessons you learned to make a model return structured data you can trust — valid JSON in exactly the shape you asked for. That's powerful, but the model is still only describing things. It can fill out a form; it can't do anything.

Tool calling (also called function calling) is the leap from describing to doing. You hand the model a list of functions it's allowed to use — get_weather, search_orders, send_email — and mid-conversation it can decide: "to answer this, I need to call get_weather with city="Tokyo"." Your code runs the function, hands back the result, and the model finishes its answer using real data.

This one capability is the foundation of everything 'agentic' — retrieval, browsing, calculators, database lookups, booking systems. Every agent you've heard of is, at its core, a model running a tool-calling loop.

In this lesson you'll learn:

  • The single most important mental model: the model never runs your code — it only requests calls
  • Why tool calling is just structured output, pointed at a function
  • The full tool-calling loop, step by step (with a widget to walk through it)
  • How to define a tool and run the round-trip in code (Claude + OpenAI)
  • How to steer it: tool_choice, parallel calls, and strict mode

The One Idea: The Model Doesn't Run Anything

Here is the mental model that everything else hangs on — and the one most beginners get wrong:

The model does not execute your functions. It cannot. It only outputs a request to call one — a name and some arguments — as structured JSON. Your code decides whether and how to actually run it.

When you give Claude or GPT a get_weather tool, the model has no network access, no Python interpreter, no database connection. What it produces is literally this:

{ "name": "get_weather", "input": { "city": "Tokyo" } }

That's it — a piece of JSON saying "I'd like you to call this." Whether that becomes a real API request, a mocked value, a permission prompt, or nothing at all is 100% up to your code. The model proposes the call; your program disposes.

Why this matters for you:

  • Security and control live entirely on your side. The model can ask to call delete_account, but it can't make it happen — you choose what each tool actually does (and whether to require a human confirmation first).
  • Debugging gets simpler once this clicks: a 'tool call' is just JSON the model emitted. Wrong arguments? That's a prompt/schema problem, not a mysterious runtime bug.
  • It's why tool calling is safe by default: nothing runs that you didn't write.

It's Structured Output, Aimed at a Function

You already know how this works under the hood — you just learned it.

In the structured-output lessons, the model used constrained decoding to emit JSON matching a schema you defined. Tool calling is that exact machinery, with one twist: the schema describes a function's arguments, and the model also picks which function.

So a tool call is two structured decisions:

  1. Which tool? (or none — just answer normally)
  2. What arguments? — emitted as JSON that conforms to that tool's input schema

That's why tool arguments are as reliable as structured output: with strict/constrained tool schemas, the input is guaranteed to match the shape you specified (recall JSON Mode & Response Schemas). The model can still choose wrong values — schema guarantees shape, not correctness — but it can't hand you malformed arguments.

The practical upshot: a good tool is a good schema plus a good description. The whole next lesson is about designing them well.

The Tool-Calling Loop

Tool calling isn't a single request — it's a loop between your code and the model. Here's the whole thing, start to finish:

  1. You → model: the user's message plus your list of tool definitions.
  2. Model → you: instead of a final answer, it returns a tool-call request and stops. The API flags this explicitly — stop_reason: "tool_use" (Claude) or finish_reason: "tool_calls" (OpenAI).
  3. You execute: your code reads the requested name + arguments and runs the real function.
  4. You → model: you append the result to the conversation and call the model again.
  5. Model → you: now holding real data, it writes the final answer — or requests another tool, and the loop repeats.

The loop can run many times (search, then calculate, then…) until the model has what it needs. Step through one round below:

An infographic titled 'The Tool-Calling Loop' showing a 5-step round trip between the model and your code, colour-coded: green steps are YOUR CODE, purple steps are the MODEL. Step 1 (your code): you send the user message plus your tool definitions. Step 2 (model): it returns a tool call, get_weather(city=Tokyo), and stops with stop_reason tool_use instead of answering. Step 3 (your code): your code runs the real get_weather function, hitting a weather API. Step 4 (your code): you append the result as a tool_result tagged by id. Step 5 (model): now holding real data, it writes the final answer. A dashed note says the model may instead request another tool, repeating steps 2 to 4. A legend states the model emits JSON and runs nothing, while your code does the real work. A bottom banner reads: the model only ever returns JSON, your code does every real action; the shape is guaranteed, correctness is your job.
Tool-Loop Console — you play the runtime. A user asks “which is warmer, Tokyo or London?” and the model emits two parallel get_weather tool-call requests as JSON (stop_reason: tool_use) — it has run nothing. You must Run or Deny each call, then return the results, at which point the model writes the comparison and stop_reason flips to end_turn. Deny a call and the model can only say it cannot compare, because the model proposes but your code disposes — the trust boundary that makes tool calling safe by default and the loop every agent runs.

Defining a Tool

A tool definition is three things: a name, a description, and an input schema (JSON Schema for its arguments). The description is not a code comment — it's the prompt the model reads to decide when and how to use the tool, so it earns real care (that's the next lesson).

# A tool definition — Claude (Anthropic) format
tools = [
    {
        "name": "get_weather",
        "description": (
            "Get the current weather for a city. "
            "Call this whenever the user asks about weather, temperature, or rain."
        ),
        "input_schema": {                 # ← JSON Schema for the arguments
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name, e.g. 'Tokyo' or 'Sao Paulo'",
                }
            },
            "required": ["city"],
        },
    }
]

OpenAI uses the same pieces, named slightly differently — and its newest Responses API flattens the shape (no nested function key):

# The same tool — OpenAI Responses API format
tools = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "Get the current weather for a city. Call this when the user asks about weather.",
        "parameters": {                   # ← OpenAI calls the schema 'parameters'
            "type": "object",
            "properties": {
                "city": {"type": "string", "description": "City name, e.g. 'Tokyo'"}
            },
            "required": ["city"],
            "additionalProperties": False,
        },
        "strict": True,                   # guarantee the args match the schema
    }
]
# (The older Chat Completions API nests these fields under a "function" key.)

The Round-Trip in Code

Now the full loop in code. Watch the four moves: send with tools → detect the tool request → run the function → send the result back, then read the final answer.

import anthropic

client = anthropic.Anthropic()

def get_weather(city: str) -> dict:                 # YOUR real function
    return {"temp_c": 17, "condition": "light rain"}   # (a real call would hit an API)

messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]

# 1. Send the message WITH the tools
resp = client.messages.create(
    model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=messages,
)

# 2. Did the model ask for a tool?
if resp.stop_reason == "tool_use":
    call = next(b for b in resp.content if b.type == "tool_use")
    # 3. Run the real function with the model's arguments (already parsed for you)
    result = get_weather(**call.input)              # call.input == {"city": "Tokyo"}

    # 4. Send the result back, tagged with the SAME tool id
    messages.append({"role": "assistant", "content": resp.content})
    messages.append({
        "role": "user",
        "content": [{
            "type": "tool_result",
            "tool_use_id": call.id,
            "content": str(result),
        }],
    })
    final = client.messages.create(
        model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=messages,
    )
    print(final.content[0].text)
    # → "It's 17°C with light rain in Tokyo right now."

The OpenAI flow is the same five steps with three dialect differences worth memorising:

ClaudeOpenAI
"I want a tool" signalstop_reason == "tool_use"finish_reason == "tool_calls"
Arguments arrive asa parsed dict (call.input)a JSON string you must json.loads()
You return the result asa tool_result blockrole: "tool" (Chat) / function_call_output (Responses)

That JSON-string gotcha bites everyone once: OpenAI hands you arguments as text, so parse it before calling your function.

# OpenAI (Chat Completions): the result-handling differences
import json

msg = resp.choices[0].message
if msg.tool_calls:                                  # finish_reason == "tool_calls"
    call = msg.tool_calls[0]
    args = json.loads(call.function.arguments)      # ← arguments is a STRING; parse it
    result = get_weather(**args)

    messages.append(msg)                            # the assistant's tool request
    messages.append({                               # the result, tagged by id
        "role": "tool",
        "tool_call_id": call.id,
        "content": json.dumps(result),
    })
    # ...call the model again to get the final answer

Steering It: tool_choice, Parallel Calls, Strict

Three controls turn tool calling from a demo into something production-ready.

tool_choice — who decides whether to call a tool?

  • auto (default) — the model decides per turn: call a tool or just answer. Right for almost everything.
  • any (Claude) / required (OpenAI) — the model must call some tool (it can't reply in plain text). Use when a tool call is the whole point (e.g. "always extract into this function").
  • Force a specific tool{"type":"tool","name":"get_weather"} (Claude) / {"type":"function","name":"get_weather"} (OpenAI). Handy for tests or a fixed first step.
  • none — tools visible but forbidden this turn.

Parallel tool calls. When a request needs several independent lookups, the model can return multiple tool calls at once (e.g. weather for Tokyo and London). Run them all, return all results. Force one-at-a-time with parallel_tool_calls=False (OpenAI) / disable_parallel_tool_use=True (Claude).

Strict mode (OpenAI). strict: true on a tool guarantees the arguments conform to your schema via constrained decoding — the structured-output guarantee, applied to tool arguments. Make it your default.

🧪 Try It Yourself

Predict the loop. A user asks your assistant: "Compare the weather in Tokyo and London and tell me which is warmer." Your assistant has exactly one tool: get_weather(city).

  1. How many times must the model hand control back to your code before it can answer? (Hint: it needs two facts.)
  2. Will it likely use parallel tool calls or sequential ones — and why?
  3. At which step does the model finally write the comparison sentence?

→ It needs get_weather for two cities, so expect two tool calls — often parallel, since neither depends on the other → your code runs both → you return both results → then the model writes the comparison. The model never compares until it's holding both real numbers. Wire this up with mocked weather values and watch stop_reason flip from tool_use to end_turn on the final turn.

Mental-Model Corrections

  • "The model runs the tool." No — it only emits a JSON request; your code runs everything. The entire security model depends on this.
  • "The arguments are always right." The shape is guaranteed (with strict schemas); the values are not. The model can pass city="Tokoyo" or invent a parameter — validate inputs before acting on anything that matters.
  • "OpenAI gives me a dict." It gives you a JSON string in argumentsjson.loads() it first. (Claude gives a parsed dict.)
  • "I can skip returning a result." You can't — every tool call the model makes must get a matching result (by id) appended before the next request, or the API errors. If the call failed, return an error message as the result; don't drop it.
  • "Forcing a tool is always safer." Forcing (any/required) removes the model's option to just answer — great for extraction, wrong for a general assistant that should sometimes reply in plain text.

Key Takeaways

  • Tool calling = the model requests a function; your code runs it. The model only ever outputs structured JSON (name + arguments).
  • It's structured output pointed at a function — the same constrained-decoding guarantee, now also choosing which tool.
  • The loop: send message + tools → model returns a tool call (stop_reason/finish_reason) → you execute → return the result by id → model answers (or calls another tool).
  • Dialect differences: tool_use vs tool_calls; parsed dict vs JSON-string arguments; tool_result vs role:"tool".
  • Steer with tool_choice (auto/any/force/none), parallel calls, and strict schemas. Always validate arguments — shape is guaranteed, correctness isn't.
  • This loop is the foundation of every agent. Next: designing tool schemas the model actually uses well.