Tool Use Patterns: Function Calling, Structured Tools, Multi-Step Reasoning
Introduction
Tool use, or function calling, enables LLMs to interact with external systems: query databases, call APIs, execute code, and retrieve information. This capability transforms LLMs from text generators into autonomous agents. This article covers the essential patterns for defining, invoking, and chaining tool calls in production systems.
Defining Tools
Every tool needs a clear schema that the LLM can understand and the application can execute:
from openai import OpenAI
from pydantic import BaseModel
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "search_documents",
"description": "Search internal documents by keyword. Returns relevant snippets with metadata.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query, use specific terms for better results",
},
"max_results": {
"type": "integer",
"description": "Number of results to return (1-20)",
"minimum": 1,
"maximum": 20,
},
"filters": {
"type": "object",
"properties": {
"date_from": {"type": "string", "format": "date"},
"department": {"type": "string"},
},
},
},
"required": ["query"],
},
},
}
]
Key principles: use descriptive parameter names with clear descriptions, set proper type constraints, and provide defaults for optional parameters. The LLM uses these descriptions to decide which tool to call and with what arguments.
Function Calling Loop
The standard pattern is a loop: generate, check for tool calls, execute, and feed results back:
def tool_use_loop(messages: list, tools: list, max_turns=10):
for turn in range(max_turns):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto",
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content
for tool_call in message.tool_calls:
result = execute_tool(tool_call.function.name, tool_call.function.arguments)
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"content": str(result),
})
return "Max turns reached"
def execute_tool(name: str, args_json: str):
args = json.loads(args_json)
if name == "search_documents":
return search_documents(**args)
elif name == "calculate":
return calculate(**args)
raise ValueError(f"Unknown tool: {name}")
The LLM sees the tool result as new context and decides whether to call another tool or produce a final answer.
Multi-Step Reasoning with Tools
Complex tasks require multiple tool calls where later calls depend on earlier results:
def research_workflow(topic: str):
messages = [{"role": "user", "content": f"Research {topic} and write a comprehensive summary."}]
# Step 1: Search for information
response = client.chat.completions.create(
model="gpt-4o", messages=messages, tools=research_tools, tool_choice="auto"
)
# Execute search, get results
# Step 2: Verify facts using a different source
# Step 3: Structure the findings
# Step 4: Generate the summary
return final_summary
Structured Tools with Validation
Anthropic's tool use API supports structured tool definitions with JSON Schema and strict mode:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20260512",
max_tokens=1024,
tools=[
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"},
"units": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["location"],
},
}
],
messages=[{"role": "user", "content": "What is the weather in Tokyo?"}],
)
for block in response.content:
if block.type == "tool_use":
print(f"Calling {block.name} with {block.input}")
Error Handling in Tool Calls
Tools fail. Plan for timeouts, invalid arguments, and unexpected responses:
def safe_tool_execution(tool_call, timeout=10):
try:
with Timeout(timeout):
result = execute_tool(tool_call.function.name, tool_call.function.arguments)
return {"success": True, "result": result}
except TimeoutError:
return {"success": False, "error": "Tool execution timed out"}
except ValueError as e:
return {"success": False, "error": f"Invalid arguments: {e}"}
except Exception as e:
return {"success": False, "error": f"Unexpected error: {e}"}
When a tool fails, pass the error message back to the LLM so it can retry with corrected arguments or choose a different approach.
Conclusion
Tool use transforms LLMs from passive text generators into active problem-solvers. Define tools with clear schemas, implement a robust function-calling loop, chain tool calls for multi-step tasks, and always handle errors gracefully by feeding failures back into the conversation context.