Tool Calling Basics
The model emits a structured request to call a function; your application executes it and feeds the result back — the model never runs anything itself.
What tool calling actually is
Tool calling (also "function calling") is a contract between the model and your code. You send the model a list of tool definitions — a name, a description, and a JSON schema for parameters. Instead of answering in prose, the model may respond with a tool call: the name of a tool plus arguments that conform to the schema.
That is the entire mechanism. The model produces text that happens to be a well-formed JSON object; the provider parses it into a typed tool_call block. Nothing has been executed yet. Your application inspects the call, decides whether to run it, runs it, and returns the result to the model as a tool result message. The model then continues — either calling another tool or producing the final answer.
Compare this with the escalation in Choosing the Right Abstraction: tool calling sits one step above structured output. Use it when the model must decide which of several actions to take and with which arguments. If there is only one action and it always runs, call the function yourself and skip the model.
The round trip
A single tool-using turn is a fixed sequence of seven steps. Every framework, from raw HTTP to LangGraph, is a wrapper around this loop.
Notice that the model appears twice and the executor appears once. The executor is your code: it owns credentials, network access and side effects. The model only ever sees strings going in and strings coming out.
The model never executes anything
This is the single most important idea in the module. A tool call is a request, not an action. The model has no socket, no filesystem, no database handle. When people say "the agent deleted the table", what happened is: the model emitted {"name":"run_sql","arguments":{"query":"DROP TABLE users"}} and an application, written by a human, ran it without checking.
Consequences follow directly. Argument validation (Argument Validation) is your job. Permission checks (Tool Permissions and Least Privilege) are your job. Retries and timeouts (Tool Errors, Retries and Timeouts) are your job. The model can be asked to be careful, but "asked" is not a control — it is a suggestion to a probabilistic process.
The upside is symmetrical: because you own execution, you can put any policy you like between the model and the world — dry-run mode, approval gates, rate limits, sandboxing — without changing the model or the prompt.
- Tool call = model output that parses as
{name, arguments}. Nothing more. - Execution, side effects and credentials live exclusively in the application.
- Every safety property of a tool-using system is enforced at the dispatch boundary, not in the prompt.
A minimal dispatch loop
Below is the whole thing with no framework. TOOLS maps names to Python callables; SPECS is what the model sees. The loop runs until the model stops requesting tools. In production you add a step budget, validation, tracing and error handling — see Budgets, Limits and Termination — but the skeleton does not change.
1import json2 3def get_weather(city: str) -> dict:4 return {"city": city, "temp_c": 21}5 6TOOLS = {"get_weather": get_weather}7SPECS = [{8 "name": "get_weather",9 "description": "Current temperature for a city.",10 "parameters": {11 "type": "object",12 "properties": {"city": {"type": "string"}},13 "required": ["city"],14 },15}]16 17def run(messages: list[dict], max_steps: int = 8) -> str:18 for _ in range(max_steps):19 reply = chat(messages, tools=SPECS) # provider API call20 messages.append(reply)21 if not reply.get("tool_calls"):22 return reply["content"] # final answer23 for call in reply["tool_calls"]:24 fn = TOOLS[call["name"]] # KeyError => unknown tool25 args = json.loads(call["arguments"])26 result = fn(**args) # the APP executes, not the model27 messages.append({28 "role": "tool",29 "tool_call_id": call["id"],30 "content": json.dumps(result),31 })32 raise RuntimeError("step budget exhausted")Key points
- A tool call is structured text the model emits; the application parses, validates and executes it.
- The round trip is request → model → tool selection → arguments → execution → result → model → answer.
- Tool results go back into context as messages; the model reasons over them like any other input.
- All safety, permission and reliability controls live at the dispatch boundary in your code.
- The loop needs a step budget; without one a confused model can call tools forever.
- Tool calling is one step above structured output on the abstraction ladder — use it only when the model must choose an action.
Tool-calling simulator
{
"name": "get_weather",
"description": "Current weather for a city. Use for questions about
present conditions, not forecasts.",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name, e.g. 'Berlin'" },
"units": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}When to use — and when not to
- The model must pick between several actions or data sources based on the request.
- Arguments depend on natural-language input that deterministic parsing cannot handle reliably.
- You need a typed interface between the model and existing APIs instead of parsing free text.
- The task requires live data (weather, inventory, tickets) the model cannot know.
- There is exactly one action and it always runs — call it directly and pass the result in the prompt.
- The output is a fixed-shape record with no side effects — use Structured Outputs instead.
- Latency budget is tight: every tool call adds at least one extra model round trip.
- The action is irreversible and there is no approval path — fix the control plane first.
Failure modes
- Model picks the wrong tool because two descriptions overlap (see Tool Schemas).
- Arguments parse as JSON but violate semantics — negative quantity, path outside the sandbox.
- Tool result is huge (a whole CSV) and blows the context window on the next turn.
- No step budget: the model alternates between two tools indefinitely.
- Executor trusts
call["name"]and dispatches viaevalorgetattron arbitrary objects.
Tradeoffs
Cheap to build, but each call is a full model round trip; debuggability is high because every call and result is a discrete message you can log.