How to Build a Tool-Using Agent with Claude: A Step-by-Step Guide
Learn how to build a tool-using agent with Claude API, from defining tools to handling tool calls, parallel execution, and best practices for production.
This guide walks you through building a tool-using agent with Claude API, covering tool definition, tool call handling, parallel tool use, and production best practices with Python code examples.
How to Build a Tool-Using Agent with Claude: A Step-by-Step Guide
Claude's tool use capability allows you to extend the model's functionality by giving it access to external tools, APIs, and functions. Instead of just generating text, Claude can decide when to call a tool, pass the right arguments, and then use the tool's output to continue the conversation. This opens up possibilities for building agents that can search the web, run code, manipulate files, or interact with any external service.
In this guide, you'll learn how to build a practical tool-using agent with Claude API, from defining tools to handling tool calls and optimizing for production.
Understanding Tool Use in Claude
Tool use (also called function calling) allows Claude to request the execution of a function you define. The flow works like this:
- You define tools and include them in your API request
- Claude analyzes the user's request and decides if a tool should be called
- If yes, Claude returns a
tool_usestop reason with the tool name and arguments - You execute the tool on your end and return the result
- Claude uses the result to generate a final response
Prerequisites
Before you start, make sure you have:
- An Anthropic API key (get one from console.anthropic.com)
- Python 3.8+ installed
- The Anthropic Python SDK installed:
pip install anthropic
Step 1: Define Your Tools
Tools are defined as JSON objects in the tools parameter of the Messages API. Each tool has a name, description, and an input schema (JSON Schema format).
Here's an example of defining a weather lookup tool:
import anthropic
client = anthropic.Anthropic()
weather_tool = {
"name": "get_weather",
"description": "Get the current weather for a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g., San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
}
Best practices for tool definitions:
- Use clear, descriptive names (snake_case recommended)
- Write detailed descriptions so Claude understands when to use the tool
- Define precise input schemas with proper types and constraints
- Mark required fields explicitly
Step 2: Send a Request with Tools
Include your tool definitions in the API request. Claude will automatically decide whether to use a tool based on the user's message.
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[weather_tool],
messages=[
{"role": "user", "content": "What's the weather like in Tokyo?"}
]
)
Step 3: Handle Tool Calls
When Claude decides to use a tool, the response will have a stop_reason of "tool_use" and the content will include a tool_use block. You need to check for this and execute the corresponding function.
def handle_tool_call(tool_name, tool_input):
"""Execute the tool and return the result."""
if tool_name == "get_weather":
location = tool_input["location"]
unit = tool_input.get("unit", "celsius")
# In production, call a real weather API here
return f"The weather in {location} is 22°{unit[0].upper()}, partly cloudy."
else:
return f"Unknown tool: {tool_name}"
Check if Claude wants to use a tool
if response.stop_reason == "tool_use":
for content in response.content:
if content.type == "tool_use":
tool_name = content.name
tool_input = content.input
tool_result = handle_tool_call(tool_name, tool_input)
print(f"Tool called: {tool_name}({tool_input})")
print(f"Result: {tool_result}")
Step 4: Return Tool Results to Claude
After executing the tool, you need to send the result back to Claude so it can continue the conversation. This is done by appending a tool_result block to the messages.
# Continue the conversation with the tool result
messages = [
{"role": "user", "content": "What's the weather like in Tokyo?"},
{"role": "assistant", "content": response.content}, # Contains tool_use block
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": content.id,
"content": tool_result
}
]
}
]
Get Claude's final response
final_response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[weather_tool],
messages=messages
)
print(final_response.content[0].text)
Output: "The weather in Tokyo is currently 22°C with partly cloudy skies. A pleasant day!"
Step 5: Parallel Tool Use
Claude can call multiple tools in a single response. This is useful when the user's request requires multiple independent actions.
# Example: User asks for weather in multiple cities
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[weather_tool],
messages=[
{"role": "user", "content": "Compare the weather in Tokyo, London, and New York"}
]
)
if response.stop_reason == "tool_use":
for content in response.content:
if content.type == "tool_use":
print(f"Calling {content.name} with {content.input}")
# Execute each tool call independently
When returning results for parallel calls, include all tool_result blocks in a single message:
results = []
for content in response.content:
if content.type == "tool_use":
result = handle_tool_call(content.name, content.input)
results.append({
"type": "tool_result",
"tool_use_id": content.id,
"content": result
})
messages.append({"role": "user", "content": results})
Advanced: Building a Complete Agent Loop
For production use, you'll want a loop that handles multiple tool calls until Claude provides a final answer. Here's a complete agent pattern:
def run_agent(user_message, tools, max_turns=5):
"""Run an agent loop with tool use."""
messages = [{"role": "user", "content": user_message}]
for turn in range(max_turns):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
# Check if Claude is done
if response.stop_reason != "tool_use":
return response.content[0].text
# Process tool calls
tool_results = []
for content in response.content:
if content.type == "tool_use":
result = handle_tool_call(content.name, content.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": content.id,
"content": result
})
# Add assistant response and tool results to messages
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
return "Agent reached maximum turns without final answer."
Usage
result = run_agent("What's the weather in Tokyo and London?", [weather_tool])
print(result)
Best Practices for Production
1. Error Handling
Always wrap tool execution in try-except blocks and return meaningful error messages to Claude:def safe_tool_call(tool_name, tool_input):
try:
return handle_tool_call(tool_name, tool_input)
except Exception as e:
return f"Error executing {tool_name}: {str(e)}"
2. Tool Result Size Limits
Keep tool results concise. Claude has context window limits, so large results can cause truncation. If you need to return large data, consider summarizing or paginating.3. Use Strict Tool Mode
For critical applications, enable strict tool use to ensure Claude only calls tools you've defined:response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[weather_tool],
tool_choice={"type": "any"}, # Forces tool use
messages=[...]
)
4. Combine with Prompt Caching
If you have large tool definitions, use prompt caching to reduce latency and costs:response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[weather_tool],
extra_headers={"anthropic-beta": "prompt-caching-2024-07-31"},
messages=[...]
)
Real-World Example: Multi-Tool Agent
Here's a more complex agent that combines a weather tool and a calculator tool:
calculator_tool = {
"name": "calculate",
"description": "Perform mathematical calculations",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Mathematical expression to evaluate, e.g., '2 + 2'"
}
},
"required": ["expression"]
}
}
def handle_tool_call(tool_name, tool_input):
if tool_name == "get_weather":
# Weather logic
return "22°C"
elif tool_name == "calculate":
import ast
try:
# Safe evaluation
result = eval(ast.literal_eval(tool_input["expression"]))
return str(result)
except:
return "Calculation error"
return "Unknown tool"
Key Takeaways
- Tool use enables agents: Claude can call external functions, making it possible to build agents that interact with the real world through APIs and services.
- Define tools clearly: Use descriptive names, detailed descriptions, and precise input schemas to help Claude choose the right tool.
- Handle the tool_use stop reason: Always check
response.stop_reasonand processtool_usecontent blocks to execute tools and return results. - Parallel tool calls are supported: Claude can call multiple tools in one response, which is great for independent operations like fetching data from multiple sources.
- Build a loop for complex tasks: For multi-step agents, implement a turn-based loop that continues until Claude produces a final answer or reaches a maximum turn limit.