Function Calling¶
Overview¶
Function Calling is the standardized mechanism for agents to invoke tools through an LLM. Instead of generating text that describes what tool to use, the LLM directly outputs structured function calls.
This is the 2025 standard that makes agents practical.
The Evolution¶
Before Function Calling (2022-2023)¶
Agent: "I should search for information about AI trends.
I'll use a search tool."
Human has to parse text and extract:
- Tool name: "search"
- Arguments: {"query": "AI trends"}
Fragile: Text parsing breaks easily
Expensive: Extra reasoning to generate text
With Function Calling (2024-2026)¶
Agent thinking → LLM → Structured output:
{
"type": "function_call",
"function": "search",
"arguments": {"query": "AI trends"}
}
Reliable: Machine-readable format
Fast: Direct to execution
Flexible: One LLM call can make multiple calls
How Function Calling Works¶
3-Step Process¶
Step 1: Define Tools
tools = [
{
"name": "search",
"description": "Search the web for information",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search term"}
},
"required": ["query"]
}
}
]
Step 2: Send to LLM
response = client.messages.create(
model="claude-3-5-sonnet",
messages=[
{"role": "user", "content": "What are latest AI trends?"}
],
tools=tools
)
Step 3: LLM Returns Structured Call
# LLM response:
{
"type": "tool_use",
"name": "search",
"input": {"query": "latest AI trends 2026"}
}
-
Implementation: OpenAI Style¶
class OpenAIFunctionCalling:
"""OpenAI's function_call parameter"""
def __init__(self):
self.client = OpenAI()
self.tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
}
]
def call_with_functions(self, user_message: str):
"""Call LLM with function definitions"""
messages = [{"role": "user", "content": user_message}]
response = self.client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=self.tools,
tool_choice="auto" # Let model decide if/when to use tools
)
# Check if model wants to call a tool
if response.choices[0].message.tool_calls:
for tool_call in response.choices[0].message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
# Execute the tool
result = self.execute_tool(tool_name, tool_args)
# Send result back to model
messages.append({"role": "assistant", "content": response.choices[0].message})
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Get final response
final_response = self.client.chat.completions.create(
model="gpt-4",
messages=messages,
tools=self.tools
)
return final_response.choices[0].message.content
return response.choices[0].message.content
def execute_tool(self, name: str, args: dict):
"""Execute requested tool"""
if name == "get_weather":
location = args["location"]
unit = args.get("unit", "celsius")
return {"location": location, "temp": 22, "unit": unit}
Implementation: Claude (Anthropic) Style¶
class ClaudeFunctionCalling:
"""Claude's tool_use content block"""
def __init__(self):
self.client = Anthropic()
self.tools = [
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["location"]
}
}
]
def call_with_tools(self, user_message: str) -> str:
"""Call Claude with tool definitions"""
messages = [{"role": "user", "content": user_message}]
while True:
response = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=self.tools,
messages=messages
)
# Check if Claude wants to use a tool
has_tool_use = any(
block.type == "tool_use"
for block in response.content
)
if not has_tool_use:
# Claude gave final answer
return next(
block.text for block in response.content
if hasattr(block, "text")
)
# Process tool uses
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
# Execute tool
result = self.execute_tool(tool_name, tool_input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
# Add assistant response and tool results to messages
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
def execute_tool(self, name: str, input_data: dict):
"""Execute the requested tool"""
if name == "get_weather":
location = input_data["location"]
unit = input_data.get("unit", "celsius")
# Call weather API or return mock
return {
"location": location,
"temperature": 22,
"condition": "Sunny",
"unit": unit
}
# Usage
agent = ClaudeFunctionCalling()
response = agent.call_with_tools("What's the weather in Paris?")
print(response)
Function Calling Patterns¶
Pattern 1: Single Tool Call¶
User: "What's 5 + 3?"
LLM: "I'll use the calculator tool"
→ function_call: calculator(5 + 3)
→ Result: 8
→ Response: "5 + 3 = 8"
Pattern 2: Multiple Tool Calls (Parallel)¶
User: "Compare weather in Paris, London, and Tokyo"
LLM: "I'll get weather for all three cities"
→ function_call: get_weather("Paris")
→ function_call: get_weather("London")
→ function_call: get_weather("Tokyo")
→ All execute in parallel
→ Response: Comparison table
Pattern 3: Sequential Tool Calls¶
User: "Analyze stock price trend for AAPL"
LLM: Step 1 - Get historical data
→ function_call: get_stock_data("AAPL")
LLM: Step 2 - Calculate trend
→ function_call: calculate_trend(data)
LLM: Step 3 - Return analysis
Function Calling Best Practices¶
1. Clear Tool Descriptions¶
# Bad
{"name": "search", "description": "Search tool"}
# Good
{
"name": "search",
"description": "Search the web for current information. "
"Use for finding recent news, data, or facts. "
"Do NOT use for personal knowledge.",
"parameters": {...}
}
2. Constrain Parameters¶
# Bad
{
"name": "search",
"parameters": {
"query": {"type": "string"} # Could be 10K chars
}
}
# Good
{
"name": "search",
"parameters": {
"query": {
"type": "string",
"minLength": 1,
"maxLength": 500,
"description": "Search query (1-500 chars)"
}
}
}
3. Provide Examples¶
# Good
{
"name": "search",
"description": "Search for information...",
"examples": [
{"query": "latest AI trends", "expected_results": "Recent AI news"},
{"query": "Python asyncio", "expected_results": "Python async docs"}
],
"parameters": {...}
}
Function Calling Warnings¶
Warning 1: Hallucinated Tools¶
# LLM invents tools that don't exist
LLM: "I'll call translate_to_klingon()"
# Tool doesn't exist!
# Validate before executing
if tool_name not in available_tools:
return error_response("Unknown tool")
Key Lesson: Always validate tool names exist.
Warning 2: Invalid Arguments¶
# LLM calls with wrong arguments
LLM: search(query=123) # Should be string!
# Validate arguments
def validate_arguments(tool_name, arguments):
schema = tools[tool_name]["parameters"]
try:
validate(arguments, schema)
except ValidationError:
return False
return True
Key Lesson: Validate arguments against schema before execution.
Warning 3: Infinite Loops¶
# Tool calls tool that calls original tool
search() → calls parse() → calls search()
# Infinite loop!
# Track call depth
call_depth = 0
MAX_DEPTH = 10
def execute_tool(name, args):
global call_depth
if call_depth >= MAX_DEPTH:
raise ToolLimitError("Max depth exceeded")
call_depth += 1
Key Lesson: Limit call depth to prevent infinite loops.
Real-World Example: E-Commerce Agent¶
class ECommerceAgent:
def __init__(self):
self.client = Anthropic()
self.tools = [
{
"name": "search_products",
"description": "Search product catalog",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"category": {"type": "string"},
"max_price": {"type": "number"}
},
"required": ["query"]
}
},
{
"name": "get_product_details",
"description": "Get detailed product info",
"input_schema": {
"type": "object",
"properties": {
"product_id": {"type": "string"}
},
"required": ["product_id"]
}
},
{
"name": "check_inventory",
"description": "Check product availability",
"input_schema": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"size": {"type": "string"},
"color": {"type": "string"}
},
"required": ["product_id"]
}
}
]
def help_customer(self, customer_query: str) -> str:
"""Help customer find products"""
messages = [{"role": "user", "content": customer_query}]
while True:
response = self.client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=self.tools,
messages=messages
)
# Check for tool use
tool_use_blocks = [
block for block in response.content
if block.type == "tool_use"
]
if not tool_use_blocks:
# Final response
return next(
block.text for block in response.content
if hasattr(block, "text")
)
# Execute tools
tool_results = []
for tool_use in tool_use_blocks:
result = self.execute_tool(tool_use.name, tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result)
})
# Continue conversation
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
def execute_tool(self, tool_name: str, tool_input: dict):
"""Execute tool and return result"""
if tool_name == "search_products":
# Search in database
return self.search_db(tool_input)
elif tool_name == "get_product_details":
# Get full product info
return self.get_details(tool_input["product_id"])
elif tool_name == "check_inventory":
# Check inventory
return self.check_inv(tool_input)
# Usage
agent = ECommerceAgent()
response = agent.help_customer("I need a blue winter coat, size M, under $200")
print(response)
Comparison: Provider APIs¶
| Provider | Mechanism | Status | Key Docs |
|---|---|---|---|
| OpenAI | tool_use parameter |
Stable | Function Calling API |
| Claude | tool_use block |
Stable | Tool Use Guide |
| Gemini | function_calling mode |
Stable | Function Calling Docs |
| Llama | Server-side tool calling | Beta | LlamaIndex Docs |
Key Takeaways¶
- Function calling is standard - All major LLMs support it
- Machine-readable, not text-parsing - Reliable and fast
- Can make multiple calls - Sequential or parallel
- Requires validation - Always check tools exist and arguments are valid
- Enable agentic behavior - Agents need tools to affect world
- Loop until done - LLM may call tools multiple times
-
Next Steps¶
- Read Tool Interfaces - Designing good tool APIs
- Read Tool Composition - Chaining tools together
-
Last Updated: August 9, 2026