Function Calling in AI Agents
Function calling is the mechanism by which an LLM requests structured tool invocations instead of producing free-form text. Understanding the underlying format helps you write better tool schemas, handle errors gracefully, and use advanced features like parallel calls.
What Is Function Calling?
When a model decides to use a tool, it doesn't simply write text — it outputs a structured tool_call object containing the function name and arguments in JSON. The host application validates, executes, and returns the result.
This structured format is reliable: the model can't hallucinate argument names, argument types are validated, and the execution environment stays in full control.
JSON Schema Tool Definition
Every tool is described by a JSON Schema object. The LLM uses this schema to understand what the function does and what arguments it accepts.
{
"name": "get_weather",
"description": "Get current weather for a given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country, e.g. 'Berlin, Germany'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"default": "celsius"
}
},
"required": ["location"]
}
}OpenClaw auto-generates this schema from Python type hints when you use the @tool decorator.
OpenAI vs Anthropic Differences
| Feature | OpenAI | Anthropic (Claude) |
|---|---|---|
| Tool definition format | tools[].function | tools[].input_schema |
| Tool result injection | role: "tool" | role: "user" with tool_result |
| Parallel calls | Supported natively | Supported natively |
| Forced tool usage | tool_choice: "required" | tool_choice: {type:"any"} |
OpenClaw normalises provider differences — you define tools once using the OpenClaw schema format and the framework translates to the correct provider API format.
Parallel Function Calls
Modern LLMs can request multiple tool calls in a single response. OpenClaw executes them concurrently using asyncio.gather:
from openclaw import Agent, tool
@tool(description="Get current weather for a city")
def get_weather(city: str) -> str:
# ... call weather API
return f"Weather in {city}: 22°C, partly cloudy"
@tool(description="Get current UTC time")
def get_time() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat()
agent = Agent(model="gpt-4o", tools=[get_weather, get_time])
result = agent.run(
"What's the weather in Tokyo and London right now, "
"and what's the current UTC time?"
)
# Both tools are called in parallel in one stepStructured Output Mode
Force the model to return a specific JSON format using response_format. Useful for extracting structured data from unstructured text:
from openclaw import Agent
from pydantic import BaseModel
class Article(BaseModel):
title: str
summary: str
tags: list[str]
sentiment: str # positive | neutral | negative
agent = Agent(model="gpt-4o", response_format=Article)
result = agent.run("Analyse this article: ...")
article = result.parsed # Typed Pydantic object
print(article.title, article.sentiment)Error Handling
OpenClaw catches common function calling errors automatically:
- Missing required field — Re-prompt the model with validation error details
- Wrong argument type — Coerce if safe, otherwise re-prompt
- Tool execution error — Return error message as tool result; let the model decide next step
- Hallucinated tool name — Reject and re-prompt with available tool list
- Function calling outputs structured JSON, not free text — this is what makes tool use reliable.
- Write clear, specific tool descriptions; the model uses them to decide when and how to call.
- OpenClaw abstracts provider differences — define tools once, run on any model.
- Parallel calls reduce latency for tasks that don't depend on each other.
- Use structured output mode (
response_format) to extract typed data from any text.
Writing Effective Tool Schemas
The quality of your tool schema description directly influences how reliably the LLM calls your tool. Follow these guidelines:
- Be specific about types and constraints. Use
type: integer,minimum: 0,enum: ["a","b"]rather than generic strings. The model respects JSON Schema constraints. - Describe the preconditions. Your description should mention when to use the tool. E.g., “Use this tool when the user asks about current weather; do not use for historical data.”
- Use illustrative parameter names.
user_emailis clearer thanemail;max_resultsis clearer thann. - Mark required vs optional. Required parameters prevent the model from omitting critical arguments; optional parameters should have clear defaults.
@tool(description="Search company knowledge base. Use for internal policy questions.")
def search_kb(
query: str, # The search query
top_k: int = 5, # Number of results to return (1-20)
domain: str = "all", # Filter: 'hr', 'legal', 'engineering', or 'all'
) -> list[dict]: ...