Tool Use¶
Tool use / function calling is the primary mechanism for connecting LLMs to the real world. Interviewers test this because it's where most production systems actually break.
Q1: Walk me through the complete tool use cycle in the OpenAI API.¶
Show answer
Four steps — the model never executes tools itself; your code does:
Step 1: Send request with tool definitions
Step 2: Model returns a response with tool_calls instead of content
Step 3: Your code executes each tool call and collects results
Step 4: Append assistant message + tool results, call the API again for the final answer
import json, os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def search_docs(query: str) -> str:
return f"Found 3 results for '{query}': [result1, result2, result3]"
tools = [{"type": "function", "function": {
"name": "search_docs",
"description": "Search the documentation",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]},
}}]
messages = [{"role": "user", "content": "How do I reset my password?"}]
# Step 1 & 2: Initial call
response = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
msg = response.choices[0].message
# Step 3 & 4: Execute and return
if msg.tool_calls:
messages.append(msg) # MUST append the assistant message first
for tc in msg.tool_calls:
result = search_docs(**json.loads(tc.function.arguments))
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
final = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
print(final.choices[0].message.content)
The most common bug: forgetting to append the assistant message before appending tool results. The API validates message ordering strictly.
Q2: What's the difference between tool_choice: "auto", "required", and a specific function?¶
Show answer
tool_choice |
Behaviour |
|---|---|
"auto" (default) |
Model decides whether to call a tool or respond directly |
"none" |
Model cannot call any tools — respond with text only |
"required" |
Model must call at least one tool |
{"type": "function", "function": {"name": "..."}} |
Model must call this specific function |
Use "required" when you need the model to always return structured data (extraction pipelines). Use a specific function when you need deterministic tool selection — useful for extraction where you always want the same schema filled.
Q3: How do you handle tool execution errors and feed them back to the model?¶
Show answer
Return error information as the tool result — the model can then reason about the failure and either try again, use a different tool, or explain the issue to the user.
import json, os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def risky_tool(query: str) -> str:
try:
return expensive_database_call(query)
except TimeoutError:
return "ERROR: Database query timed out after 5 seconds"
except ValueError as e:
return f"ERROR: Invalid query format — {e}"
# In your tool execution loop:
for tc in msg.tool_calls:
result = risky_tool(**json.loads(tc.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result, # error string is valid — model will handle it
})
Never raise an unhandled exception during tool execution — the loop breaks and you lose the conversation state. Always return a string result, even if that string is an error message.
Q4: What is parallel tool calling and how do you handle it?¶
Show answer
When a request could benefit from multiple independent tool calls, the model returns multiple tool_calls in a single response. You execute them all and return all results before the next API call.
import json, asyncio, os
from openai import AsyncOpenAI
aclient = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
async def execute_tool(name: str, args: dict) -> str:
if name == "search":
return await async_search(args["query"])
elif name == "get_weather":
return await async_weather(args["city"])
return f"Unknown tool: {name}"
async def run_with_parallel_tools(messages: list) -> str:
response = await aclient.chat.completions.create(
model="gpt-4o", messages=messages, tools=tools
)
msg = response.choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg)
# Execute all tool calls in parallel
results = await asyncio.gather(*[
execute_tool(tc.function.name, json.loads(tc.function.arguments))
for tc in msg.tool_calls
])
for tc, result in zip(msg.tool_calls, results):
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
final = await aclient.chat.completions.create(model="gpt-4o", messages=messages)
return final.choices[0].message.content
Always append tool results in the same order as the tool_calls list — the model matches them by tool_call_id.
Q5: How do you design tool schemas for reliability?¶
Show answer
Tool schema quality directly affects how reliably the model calls your tools. Guidelines:
-
Descriptions are prompts — the
descriptionfield is model-readable. Be specific: "Search the product documentation for answers about billing, plans, and account management" beats "Search docs." -
Use enums for constrained inputs — if only specific values are valid, enumerate them in the schema so the model can't hallucinate an invalid value.
-
Keep tool count small — every additional tool is another surface for the model to misuse. Remove tools that overlap in purpose.
-
Name functions unambiguously —
search_billing_docsis better thansearchwhen there are multiple search tools.
tool = {
"type": "function",
"function": {
"name": "create_support_ticket",
"description": "Create a new support ticket. Use ONLY after collecting the customer's issue description and contact email.",
"parameters": {
"type": "object",
"properties": {
"issue_description": {
"type": "string",
"description": "Verbatim description of the customer's issue, at least 20 characters"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "Priority based on business impact"
},
"customer_email": {
"type": "string",
"description": "Customer's email address for follow-up"
}
},
"required": ["issue_description", "priority", "customer_email"],
"additionalProperties": False,
}
}
}
Previous: LangGraph | Next: LoRA and QLoRA