Tool Use & Function Calling
Tool use lets LLMs request function execution instead of guessing answers. Define tools with schemas, validate arguments, execute safely, and return results to the model.
Recipe
tools = [{"type": "function", "function": {
"name": "get_order",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}, "required": ["order_id"]},
}}]
response = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)Working Example
"""tool_use.py - safe tool execution loop."""
from __future__ import annotations
import json
from pydantic import BaseModel, ValidationError
from openai import OpenAI
client = OpenAI()
class GetOrderArgs(BaseModel):
order_id: str
def get_order(order_id: str) -> dict:
orders = {"ORD-123": {"status": "shipped", "total": 49.99}}
return orders.get(order_id, {"error": "not found"})
TOOL_REGISTRY = {"get_order": (GetOrderArgs, get_order)}
tools = [{"type": "function", "function": {
"name": "get_order",
"description": "Look up order by ID",
"parameters": GetOrderArgs.model_json_schema(),
}}]
messages = [{"role": "user", "content": "What is the status of order ORD-123?"}]
for _ in range(5):
resp = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
msg = resp.choices[0].message
if not msg.tool_calls:
print(msg.content)
break
messages.append(msg)
for call in msg.tool_calls:
name = call.function.name
schema_cls, fn = TOOL_REGISTRY[name]
try:
args = schema_cls.model_validate_json(call.function.arguments)
result = fn(**args.model_dump())
except ValidationError as e:
result = {"error": str(e)}
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})Gotchas
- Executing unvalidated args - SQL injection via tool params. Fix: Pydantic validation; parameterized queries.
- Overpowered tools - model can delete data. Fix: read-only tools; human approval for writes.
- No iteration limit - infinite tool loop. Fix: max 5-10 iterations.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| OpenAI tool calling | GPT models | Claude (use Anthropic tool format) |
| LangChain @tool | LangGraph agents | Raw SDK preference |
| MCP | Standardized tool servers | Simple single-app tools |
FAQs
OpenAI vs Anthropic tools?
OpenAI: tools in chat.completions. Anthropic: tools param with tool_result blocks.How many tools?
5-10 focused tools beat 50 vague ones.Tool descriptions matter?
Yes - clear descriptions improve selection accuracy.Parallel tool calls?
Execute all tool_calls in one response; return all results.How do I test tools?
Unit test each tool function; integration test the loop with mocked LLM.Dangerous tools?
Sandbox code execution; require human approval for writes.Tool not called?
Improve description; add examples in system prompt.Wrong tool selected?
Narrow tool descriptions; reduce tool count.Structured tool args?
Pydantic model as JSON schema for validation.How do I log tool calls?
Log tool name, args, result, and latency for observability.Rate limit tool APIs?
Semaphore on external API calls tools make.MCP vs inline tools?
MCP for shared tools across apps; inline for app-specific.Related
- LangGraph & Agent Loops
- Anthropic Claude SDK
- Structured Output
- Model Context Protocol (MCP)
- Evaluation & Guardrails
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.