Lesson 3 of 3 · Mastering Function Calling & Tool Use
Handling Tool Calls
reading13 min
You have defined your tools and the model is returning tool_calls. Now you need to build the machinery that receives those calls, executes the right function, handles errors, and feeds results back to the model. This is where function calling becomes a real engineering problem.
The Tool Call Response Structure
When the model wants to call a function, the response looks like this:
Python
choice = response.choices[0]# Check if the model wants to call toolsif choice.finish_reason == "tool_calls": for tool_call in choice.message.tool_calls: print(f"Call ID: {tool_call.id}") # "call_abc123" print(f"Function: {tool_call.function.name}") # "get_order_status" print(f"Args: {tool_call.function.arguments}") # '{"order_id": "ORD-78901"}'
Three critical fields:
id: A unique identifier for this specific call. You must return this ID when sending the result back.
function.name: Which function the model wants to call.
function.arguments: A JSON string (not an object -- you must parse it) containing the arguments.
Arguments Are a JSON String
The arguments field is a JSON string, not a parsed object. You must call json.loads() on it. This is the single most common source of bugs in function calling implementations. If you try to access tool_call.function.arguments["order_id"], you will get a character from the string, not the value.
Building a Function Dispatcher
In production, you need a dispatcher that routes tool calls to the correct function:
Python
import json# Registry of available functionsFUNCTION_REGISTRY = { "get_order_status": get_order_status, "search_products": search_products, "create_support_ticket": create_support_ticket,}def handle_tool_calls(response): """Process all tool calls from a model response.""" tool_messages = [] for tool_call in response.choices[0].message.tool_calls: function_name = tool_call.function.name call_id = tool_call.id # Parse arguments try: arguments = json.loads(tool_call.function.arguments) except json.JSONDecodeError as e: # Model produced invalid JSON -- rare but possible tool_messages.append({ "role": "tool", "tool_call_id": call_id, "content": json.dumps({"error": f"Invalid arguments: {e}"}) }) continue # Look up the function function = FUNCTION_REGISTRY.get(function_name) if not function: tool_messages.append({ "role": "tool", "tool_call_id": call_id, "content": json.dumps({"error": f"Unknown function: {function_name}"}) }) continue # Execute the function try: result = function(**arguments) tool_messages.append({ "role": "tool", "tool_call_id": call_id, "content": json.dumps(result) }) except Exception as e: tool_messages.append({ "role": "tool", "tool_call_id": call_id, "content": json.dumps({"error": str(e)}) }) return tool_messages
Returning Results to the Model
After executing the function(s), you send the results back by appending to the conversation:
Python
def complete_tool_calls(messages, tools, response): """Execute tool calls and get the model's final response.""" # Add the assistant's tool_call message to history messages.append(response.choices[0].message) # Execute all tool calls and get results tool_messages = handle_tool_calls(response) messages.extend(tool_messages) # Send back to the model for synthesis follow_up = client.chat.completions.create( model="gpt-4.1", messages=messages, tools=tools ) return follow_up
Always Include the Original tool_call Message
When sending results back, you must include the assistant's original message (with the tool_calls) in the conversation history, followed by the tool result messages. The model needs to see the full sequence: it asked for data, you provided data, now it can respond. Skipping the assistant message breaks the conversation flow.
Error Handling Patterns
What happens when a function call fails? Do not hide the error from the model. Return it as a tool result:
Python
try: result = get_order_status(order_id=arguments["order_id"]) content = json.dumps(result)except OrderNotFoundError: content = json.dumps({ "error": "order_not_found", "message": f"No order found with ID {arguments['order_id']}" })except DatabaseError: content = json.dumps({ "error": "service_unavailable", "message": "The order system is temporarily unavailable." })
The model handles errors gracefully when you give it structured error information. It might tell the user "I could not find that order number -- could you double-check it?" or "Our order system is temporarily down -- please try again in a few minutes."
Do
Return errors as structured JSON in the tool result. The model is excellent at translating error codes into helpful, human-friendly messages. Include enough detail for the model to suggest next steps.
Don't
Raise exceptions that crash your application. Do not return empty strings -- the model will be confused. Do not hide errors by returning fake data -- the model will present false information to the user.
The Complete Conversation Flow
Here is the full flow in one piece:
Python
def chat(user_message, conversation_history, tools): conversation_history.append({"role": "user", "content": user_message}) response = client.chat.completions.create( model="gpt-4.1", messages=conversation_history, tools=tools ) # Loop: the model might need multiple rounds of tool calls while response.choices[0].finish_reason == "tool_calls": # Add the assistant's tool request conversation_history.append(response.choices[0].message) # Execute all tool calls tool_results = handle_tool_calls(response) conversation_history.extend(tool_results) # Get next response (might be another tool call or final answer) response = client.chat.completions.create( model="gpt-4.1", messages=conversation_history, tools=tools ) # Final text response assistant_message = response.choices[0].message.content conversation_history.append({"role": "assistant", "content": assistant_message}) return assistant_message
Notice the while loop. The model can request multiple rounds of tool calls before producing a final answer. For example, it might first look up an order, then check the shipping carrier, then check the delivery schedule -- three separate tool calls before answering.