Tool use cheatsheet.
What it is
LLM decides to call a function with structured args. Your code runs it; result fed back.
user → LLM → tool_call → your code → tool_result → LLM → response
OpenAI
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["c", "f"]},
},
"required": ["city"],
},
},
}]
response = client.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": "Weather in Paris?"}],
tools=tools,
)
msg = response.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
args = json.loads(call.function.arguments)
result = get_weather(**args)
# Send result back
messages.append(msg)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
})
final = client.chat.completions.create(model="gpt-5", messages=messages, tools=tools)
Anthropic
tools = [{
"name": "get_weather",
"description": "Get current weather",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["c", "f"]},
},
"required": ["city"],
},
}]
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "Weather in Paris?"}],
)
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
result = get_weather(**block.input)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
}]})
final = client.messages.create(model="claude-opus-4-7", max_tokens=1024, tools=tools, messages=messages)
Parallel tool calls
OpenAI / Anthropic can return multiple tool_calls. Execute them in parallel:
import asyncio
async def execute_all(tool_calls):
tasks = [tool_funcs[c.function.name](**json.loads(c.function.arguments)) for c in tool_calls]
results = await asyncio.gather(*tasks)
return list(zip(tool_calls, results))
Loop until done
while True:
response = client.messages.create(model=..., messages=messages, tools=tools)
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
messages.append({"role": "user", "content": tool_results})
# response now has final answer
Tool schema tips
- Detailed descriptions: LLM picks better tool.
- Required vs optional: mark clearly.
- Enums: constrain values.
- Examples in description: “e.g., ‘Paris, FR’”.
- One tool, one purpose: don’t bundle.
Common tool ideas
search_web(query).read_file(path)/write_file(path, content).run_sql(query).send_email(to, subject, body).get_calendar(date)/create_event(...).query_vector_db(text, k).
Forcing tool use
# OpenAI
tool_choice = {"type": "function", "function": {"name": "get_weather"}}
tool_choice = "auto" # default
tool_choice = "none"
tool_choice = "required" # must use any tool
# Anthropic
tool_choice = {"type": "tool", "name": "get_weather"}
tool_choice = {"type": "any"}
tool_choice = {"type": "auto"}
JSON output via tool
Reliable structured output: define a tool whose only purpose is to return data:
tools = [{
"name": "output_data",
"description": "Output the result",
"input_schema": {
"type": "object",
"properties": {
"summary": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}},
},
"required": ["summary", "tags"],
},
}]
response = client.messages.create(
model="...",
tools=tools,
tool_choice={"type": "tool", "name": "output_data"},
messages=[...],
)
# response.content[0].input has the structured data
Validation
from pydantic import BaseModel, ValidationError
class WeatherArgs(BaseModel):
city: str
unit: str = "c"
try:
args = WeatherArgs(**json.loads(call.function.arguments))
except ValidationError as e:
return {"error": str(e)}
Don’t trust LLM args; validate.
Error handling
Pass errors back to LLM so it can recover:
try:
result = tool(**args)
except Exception as e:
result = {"error": str(e)}
# Pass result back; LLM may retry with corrected args
Loop guards
Cap iterations to prevent infinite tool loops:
for _ in range(10):
response = call_with_tools()
if not has_tool_calls(response):
break
else:
raise RuntimeError("Too many tool iterations")
Streaming + tools
OpenAI: tool calls appear as deltas. Anthropic: similar.
# Pattern: buffer tool args during stream, execute on complete
Common mistakes
- Bad schema → LLM hallucinates arg names.
- Not handling tool errors → loop fails.
- Mixing async exec with sync agent loop.
- Tools that take >30s → request timeout.
- Returning huge tool results → wastes context.
Read this next
If you want my tool-using agent template, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .