Introduction: AI Without Hands and Feet
Imagine locking a brilliant person in a room with only a small slot. You pass questions through the slot, they answer. But they cannot leave the room, cannot touch anything, cannot access the internet. This is roughly the situation of an LLM without Tool Use.
Now imagine opening the door and saying: “Here are your tools — a calculator, a web browser, email, a database. Use them whenever you need to.” That is Tool Use.
In this episode, you will learn what Tool Use is, how it works, and how to build your own tools and give them to an Agent.
What Is Tool Use?
Tool Use (or Function Calling) is a mechanism that lets you define tools and give them to an LLM. When the LLM determines it needs a tool to answer a question, it tells you “I want to call this tool with these parameters.” You execute the tool, return the result, and the LLM uses that result.
Important point: The LLM does not execute the tool itself. It only decides which tool to call and with what parameters. Actually executing the tool is your code’s responsibility.
The Tool Use Cycle
The Tool Use cycle typically works like this:
- User asks a question
- LLM determines it needs a tool to answer
- LLM returns a “tool request” (tool name + parameters)
- Your code executes the tool
- Tool result is returned to the LLM
- LLM uses the result and gives the final answer
# Pseudocode for the Tool Use cycle
user_asks("What is the weather in Tehran right now?")
# LLM decides:
# -> I need to use the get_weather tool
# -> Parameter: city = "Tehran"
result = get_weather(city="Tehran")
# -> {"temp": 28, "condition": "Sunny"}
# LLM final answer:
# "The weather in Tehran is currently 28 degrees and sunny"
Defining Tools with JSON Schema
For the LLM to understand what tools are available, you need to define them in a specific format. This format is typically JSON Schema:
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name in English, e.g. Tehran"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Calculate a mathematical expression",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression, e.g. 2+2 or sqrt(16)"
}
},
"required": ["expression"]
}
}
}
]
Practical Implementation with the OpenAI API
Let us build a complete example. We will create a simple Agent that can both check the weather and perform calculations:
import json
from openai import OpenAI
client = OpenAI()
# Define tools
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name in English"
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Calculate a math expression",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression like 2+2"
}
},
"required": ["expression"]
}
}
}
]
# Actual tool implementations
def get_weather(city: str) -> dict:
"""Simulated weather API"""
fake_data = {
"Tehran": {"temp": 28, "condition": "Sunny"},
"London": {"temp": 15, "condition": "Rainy"},
"Tokyo": {"temp": 22, "condition": "Cloudy"},
}
return fake_data.get(city, {"temp": 20, "condition": "Unknown"})
def calculate(expression: str) -> str:
"""Simple calculator"""
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
# Map tool names to functions
tool_functions = {
"get_weather": get_weather,
"calculate": calculate,
}
def run_agent(user_message: str):
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message}
]
# Step 1: Send to LLM
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
message = response.choices[0].message
# Step 2: Check if any tool was called
if message.tool_calls:
messages.append(message)
# Step 3: Execute each tool
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"Tool called: {func_name}({func_args})")
# Execute the function
result = tool_functions[func_name](**func_args)
# Add the result to the conversation
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Step 4: Send back to LLM with tool results
final_response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
return final_response.choices[0].message.content
return message.content
# Test
print(run_agent("What is the weather in Tehran and what is 25 times 13?"))
In this example, when the user asks about both weather and math, the LLM may call both tools simultaneously! This is one of the cool features of Tool Use — parallel tool execution.
How Does the LLM Decide?
An important question: how does the LLM know it should use a tool?
Answer: from the tool descriptions and the user query. When the tool description says “Get current weather” and the user asks “How is the weather?”, the LLM recognizes the semantic match.
A few important tips:
- Write clear descriptions: “Get weather” is better than “weather”. But “Get current weather for a specific city including temperature and conditions” is best.
- Describe parameters well: For example, say “City name in English, e.g. Tehran” not just “city”.
- Control tool count: If you provide 100 tools, the LLM gets confused. Group related tools together.
Comparing Tool Use in OpenAI and Anthropic
Two of the biggest LLM providers both support Tool Use, but with some differences:
OpenAI (GPT-4o)
# OpenAI Tool Use
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=[{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"}
},
"required": ["city"]
}
}
}]
)
# Access the tool call
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name) # "get_weather"
print(tool_call.function.arguments) # '{"city": "Tehran"}'
Anthropic (Claude)
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "Get weather",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
}
},
"required": ["city"]
}
}],
messages=[{"role": "user", "content": "Weather in Tehran?"}]
)
# Access the tool call
for block in response.content:
if block.type == "tool_use":
print(block.name) # "get_weather"
print(block.input) # {"city": "Tehran"}
Key Differences
- Definition format: OpenAI uses
parameters, Anthropic usesinput_schema - Response structure: OpenAI returns tools in
tool_calls, Anthropic incontentwith typetool_use - Tool results: OpenAI uses role=tool, Anthropic uses role=user with type
tool_result - Parallel execution: Both support it
Building More Complex Tools
Tools can be much more complex. Let us build a database search tool:
database_tool = {
"type": "function",
"function": {
"name": "query_database",
"description": "Search the product database. "
"Can filter by name, category, "
"price, and availability.",
"parameters": {
"type": "object",
"properties": {
"search_term": {
"type": "string",
"description": "Search term for product name"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "books", "food"],
"description": "Product category"
},
"max_price": {
"type": "number",
"description": "Maximum price"
},
"in_stock_only": {
"type": "boolean",
"description": "Only show available products",
"default": True
}
},
"required": ["search_term"]
}
}
}
enum, the LLM can only select from the specified values. This greatly helps prevent invalid inputs.
The Tool Use Loop Pattern
Sometimes the Agent needs to call tools multiple times to reach an answer. For example, first search, then filter results, then calculate a price. For this scenario, you need a loop:
def run_agent_loop(user_message: str, max_iterations: int = 10):
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_message}
]
for i in range(max_iterations):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
message = response.choices[0].message
messages.append(message)
# If no tool call, the final answer is ready
if not message.tool_calls:
return message.content
# Execute all requested tools
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
result = tool_functions[func_name](**func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
return "Maximum iteration count reached"
This pattern is very important and is used in most Agent frameworks. max_iterations is a safety guard to prevent the Agent from falling into an infinite loop.
Best Practices
- Write tool descriptions in English: LLMs work better with English descriptions, even if the user speaks another language.
- Keep tools atomic: Each tool should do one specific thing. “get_weather” is good, “get_weather_and_send_email” is bad.
- Keep tool output simple: Only return necessary information. A huge JSON with 100 fields confuses the LLM.
- Return errors too: If a tool fails, tell the LLM so it can explain to the user or try a different approach.
- Set safety limits: Always have a
max_iterations. Always validate tool inputs.
Tool Use Security
Tool Use gives the Agent a lot of power, but with power comes responsibility:
eval() directly to execute user input. The calculator example above was just for demonstration — in production, always use safe libraries like numexpr or asteval.
- Validate inputs: Before executing any tool, check that the inputs are valid.
- Limit access: The Agent should not have access to everything. Follow the principle of least privilege.
- Log everything: Log every tool call. Essential for debugging and auditing.
- Human confirmation: For sensitive operations (like deleting data or making payments), get user confirmation.
Summary
Tool Use is a simple but powerful concept:
- You define tools with JSON Schema
- The LLM decides which tool to call and with what parameters
- You execute the tool and return the result
- This cycle can repeat multiple times
With Tool Use, the Agent transforms from a talk-only entity into an action-oriented one that can actually get things done.
The next episode is about Agent Memory — how an Agent can remember things and use past experiences. See you next time!