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.