What is function calling? How LLM tool calling works
Function calling lets a model request a tool call while your code runs it. How the loop works, how to design tool schemas, handle errors and keep it safe.
A language model cannot check the weather, query your database or send an email. It can only produce text. Function calling is the pattern that turns that text into action: the model writes a structured request, and your own code decides whether to carry it out.
TL;DR
In function calling, the model does not run anything. You describe your tools in the request, and the model replies with a structured tool call: a tool name and a set of arguments. Your application validates that call, runs the tool, and sends the result back so the model can write the final answer. Tool calling and function calling are two names for the same pattern. The model proposes, your code disposes.
How function calling works, step by step
The loop has five steps, and every provider's API follows the same shape even though the field names differ. Check your provider's current documentation for the exact request and response format, because those details change between versions.
Step 1: Describe the tools
Your request includes each tool's name, a plain-language description and a JSON schema for its arguments
Step 2: The model decides
It either answers in text or returns a tool call naming a tool and its arguments
Step 3: Your code validates and runs
You check the call against the schema, apply your own permission rules, then execute the real function
Step 4: Return the result
You send the tool's output (or its error) back to the model as a new message
Step 5: Final answer
The model reads the result and writes the reply, or asks for another tool call
Two consequences follow from this shape. First, the model never sees your code, your network or your credentials. It sees only the schemas you chose to expose and the results you chose to return. Second, every tool call is a decision point where your application can say no.
A runnable minimal example
The example below runs with no API key and no provider SDK. A stub function plays the model and returns a hard-coded tool call, so you can see the loop, the validation step and the step cap in about sixty lines. To adapt it, replace fake_model with a call to your provider's SDK, pass the tool schemas in the format that provider documents, and read the tool call from its response. Use a model name placeholder such as <model> in your own code and check the provider's documentation for the current identifiers.
import json
# 1. The tool: a plain function plus a schema that describes it to the model.
def get_weather(city: str, unit: str = "celsius") -> dict:
data = {"Sydney": 24, "Oslo": 6} # stand-in for a real lookup
if city not in data:
return {"error": f"unknown city '{city}'. Known cities: {sorted(data)}"}
temp = data[city] if unit == "celsius" else round(data[city] * 9 / 5 + 32)
return {"city": city, "temperature": temp, "unit": unit}
TOOLS = {
"get_weather": {
"fn": get_weather,
"schema": {
"name": "get_weather",
"description": "Current temperature for one city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
},
}
}
# 2. A stub "model". A real model reads the messages and the tool schemas, then
# returns either a tool call or final text. This one is hard-coded so the
# example runs with no API key.
def fake_model(messages, tools):
last = messages[-1]
if last["role"] == "user":
return {"type": "tool_call", "name": "get_weather",
"arguments": {"city": "Sydney", "unit": "celsius"}}
result = json.loads(last["content"])
return {"type": "text", "text": f"It is {result['temperature']} degrees {result['unit']} in {result['city']}."}
# 3. Your code validates the call, runs the tool, and feeds the result back.
def validate(call):
tool = TOOLS.get(call["name"])
if tool is None:
return f"no such tool '{call['name']}'"
schema = tool["schema"]["parameters"]
args = call["arguments"]
for field in schema["required"]:
if field not in args:
return f"missing required argument '{field}'"
for name, value in args.items():
spec = schema["properties"].get(name)
if spec is None:
return f"unexpected argument '{name}'"
if "enum" in spec and value not in spec["enum"]:
return f"'{name}' must be one of {spec['enum']}"
return None
def run(question, max_steps=5):
messages = [{"role": "user", "content": question}]
for _ in range(max_steps): # a hard cap, so a confused model cannot loop forever
reply = fake_model(messages, [t["schema"] for t in TOOLS.values()])
if reply["type"] == "text":
return reply["text"]
error = validate(reply)
result = {"error": error} if error else TOOLS[reply["name"]]["fn"](**reply["arguments"])
messages.append({"role": "tool", "content": json.dumps(result)})
return "stopped: step limit reached"
print(run("What is the weather in Sydney?"))Run it and it prints the temperature for Sydney. The point is the structure, not the weather. The validate function sits between the model and your tool, and the max_steps cap sits around the whole loop. Those two guards are the difference between a demo and something you can put in production.
Designing good tool schemas
The model chooses tools by reading their names and descriptions, so a schema is part of your prompt. Treat it that way.
- Name tools by what they do.
get_order_statusis clearer thanlookupororders_tool. - Describe when to use the tool, not only what it does. One sentence on when it applies and when it does not helps the model avoid unnecessary calls.
- Keep parameters narrow. Use enums for fixed choices, mark required fields as required, and avoid free-form strings where a structured value would do.
- One job per tool. A single tool with a
modeargument that switches between five behaviours is harder for a model to call correctly than five small tools. - Prefer idempotent tools. If a call is retried, running it twice should not charge a customer twice. Where a side effect is unavoidable, accept an idempotency key.
Vague
- Name:
data_tool - Description: "Gets data"
- Parameters: one free-text
querystring - Result: raw rows, any shape
Well designed
- Name:
get_order_status - Description: "Returns the shipping status for one order. Use when the customer asks where an order is."
- Parameters:
order_id(string, required) - Result: a small object with
statusandestimated_delivery
Errors, retries and limits
Models make mistakes with arguments: a missing field, a value outside an enum, an ID that does not exist. Plan for it.
1. Validate before you execute. Check required fields, types and allowed values against the schema, and apply your own business rules on top. 2. Return errors the model can act on. "unknown city 'Sidney'. Known cities: Oslo, Sydney" lets the model correct itself on the next step. "Error 500" does not. 3. Cap steps, time and cost. Set a maximum number of tool calls per request and a timeout per tool. A confused model will otherwise keep calling tools. Our guide to agent loops, stopping conditions and guardrails covers the limits worth setting. 4. Retry the transport, not the decision. Retry a failed network call, but do not silently re-run a tool the model has already seen fail without telling it.
Parallel and multi-step calls
Some providers let a model request several tool calls in one turn, and a model can also chain calls across turns, using the result of one as the input to the next.
Parallel calls suit independent lookups, such as fetching three customers' records at once. Run them concurrently, then return all results together. Sequential calls suit dependent steps, such as finding a customer ID and then fetching that customer's orders. Do not allow parallel calls for tools with side effects that depend on order, and decide explicitly what happens when one call in a batch fails.
Security: treat tool output as untrusted
A tool result goes back into the model's context, so anything an attacker can write into that result can try to steer the model. A web page, an email body or a database field can contain text that looks like an instruction. This is prompt injection, and it is the main security risk of tool use. Our explainer on prompt injection attacks and defences goes through the patterns that hold up.
Three habits reduce the risk:
- Least privilege. Give each tool only the access its job needs: read-only where possible, scoped to one tenant, with short-lived credentials.
- Approvals for side effects. Sending money, deleting data or emailing customers should pause for a human. See approval design for AI agents for when to ask.
- Never execute what the model wrote as code or SQL without a boundary. Pass arguments as parameters to fixed functions, not as strings to run.
Function calling vs MCP vs agents
These three terms describe different layers, and mixing them up causes most of the confusion.
Function calling
- The mechanism: how a model asks for a tool call
- Lives in the model API request and response
- Tools are defined per application
MCP
- The packaging: a standard protocol for tool servers
- Lets one tool server work with many clients
- Still uses tool calls underneath
Agent
- The behaviour: a loop that plans, calls tools and observes results
- Built from function calling plus state, limits and stopping rules
- Needs evaluation, not only a schema
If you have one application and a handful of tools, plain function calling is usually enough. If you want the same tools available to several applications or assistants, MCP removes the duplicated wrappers. Once the model is deciding what to do next across many steps, you are building an agent, and the agentic AI engineering learning path is the place to go next.
Testing tool use
Function calling fails in quiet ways: the model picks the wrong tool, passes a plausible but wrong argument, or calls a tool it did not need. Final-answer checks alone miss these. Measure the calls themselves:
- Right tool: did it choose the tool a correct solution needs?
- Right arguments: are the argument values correct, not just well-formed?
- No unnecessary calls: did it avoid calling tools when the answer was already in context?
- Recovery: after an error result, did it correct itself or repeat the mistake?
Start with about twenty realistic tasks and run each several times, since tool choice can vary between runs. The posts on evaluating AI agents beyond final-answer accuracy and starting agent evals small with pass@k show how.
Common mistakes
- Writing one huge tool with a mode switch instead of several small ones.
- Vague descriptions, so the model guesses when to call the tool.
- Returning entire database rows or documents when a small summary would do, which wastes context and leaks data.
- Trusting arguments because "the model wrote them".
- No step cap, no timeout, no budget.
- Giving the same credentials to every tool.
Frequently asked questions
What is function calling in an LLM? Function calling is a pattern where you describe tools to a model and the model replies with a structured request to call one of them, with arguments. The model does not execute anything. Your application validates the request, runs the tool and returns the result to the model.
Is function calling the same as tool calling? In practice, yes. Providers use both terms for the same idea. Some APIs name the fields "functions", others "tools", and the exact request and response shapes differ, so read your provider's current documentation before you write the integration.
Does the model actually run my function? No. The model produces text that names a tool and its arguments. Your code is the only thing that runs the function, which is why validation, permissions and approvals belong in your code.
What is the difference between function calling and MCP? Function calling is how a model asks for a tool call. The Model Context Protocol is a standard way to package tools so many clients can use the same tool server. MCP clients still pass tool calls to the model underneath.
How many tools can I give a model at once? There is no single safe number, and limits depend on the provider and model. The practical rule is that more tools mean more descriptions in context and more chances to pick the wrong one, so expose only the tools a task needs and test tool selection as you add them.
How do I stop a model from calling tools forever? Cap the number of steps per request, set a timeout on each tool and a cost budget for the whole run, and return clear errors so the model can correct itself instead of retrying blindly.
MCP and tool use
Your next step
Reading is the start. Turn it into skill: read the chapter, practise on something real, check the numbers, then follow a path.
More on AI agents in production
- What is MCP? Model Context Protocol explained for engineersMCP is the open standard that lets any AI agent use any tool server. How it works, its three primitives, transports, security and when plain function calls win.
- Build your own 24/7 personal AI agent in 2026Build a personal AI agent that runs 24/7: choose hosted (Dots, Muse, Grok Bot) or DIY, then wire up scheduling, memory, MCP tools, approvals and an audit log.
- AI agent loops: stopping conditions and guardrailsWhy agents loop forever and the guardrails that stop them: iteration limits, cost budgets, tool-call deduplication, progress checks and clear 'done' criteria.
Discuss this article
Got a question, a counter-example or a correction? Bring it to the community.
Prefer a feed reader? Follow by RSS.