Skip to content

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

  1. Function calling is standard - All major LLMs support it
  2. Machine-readable, not text-parsing - Reliable and fast
  3. Can make multiple calls - Sequential or parallel
  4. Requires validation - Always check tools exist and arguments are valid
  5. Enable agentic behavior - Agents need tools to affect world
  6. Loop until done - LLM may call tools multiple times

-

Next Steps

-

Last Updated: August 9, 2026