How to Build a Tool-Using Agent with Claude: A Practical Guide
Learn how to build a tool-using agent with Claude API, including defining tools, handling tool calls, parallel tool use, and best practices for production deployment.
This guide walks you through building a tool-using agent with Claude, covering tool definition, execution, parallel tool calls, strict mode, and error handling with practical code examples.
How to Build a Tool-Using Agent with Claude: A Practical Guide
Claude's tool use capability is one of its most powerful features, allowing you to extend the model's functionality by giving it access to external APIs, databases, and custom functions. Whether you're building a customer support bot that can look up orders, a research assistant that can search the web, or an automation agent that can interact with your infrastructure, tool use is the key to unlocking Claude's full potential.
In this guide, you'll learn how to build a robust tool-using agent from scratch, covering everything from basic tool definitions to advanced patterns like parallel tool calls and strict mode.
Understanding Claude's Tool Use Architecture
Before diving into code, it's important to understand how tool use works under the hood:
- You define tools – You provide Claude with a JSON schema describing each tool's name, description, and parameters.
- Claude decides when to use them – Based on the conversation context, Claude may respond with a
tool_usestop reason and the arguments to call. - You execute the tool – Your application runs the tool function with the provided arguments.
- You return the result – You send the tool result back to Claude, which then continues the conversation.
Prerequisites
- An Anthropic API key (get one at console.anthropic.com)
- Python 3.8+ or Node.js 16+
- The Anthropic SDK installed (
pip install anthropicornpm install @anthropic-ai/sdk)
Step 1: Defining Your Tools
Tools are defined using JSON Schema. Each tool needs:
name: A unique identifier (use snake_case)description: Clear explanation of what the tool doesinput_schema: Parameters the tool accepts
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name, e.g., San Francisco, CA"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
},
{
"name": "query_database",
"description": "Query a SQL database for customer information",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL SELECT query to execute"
}
},
"required": ["query"]
}
}
]
Step 2: Implementing Tool Handlers
Next, you need to implement the actual functions that Claude will call. These are your "tool handlers":
def handle_get_weather(location: str, units: str = "celsius"):
# In production, call a real weather API
mock_data = {
"San Francisco, CA": {"temp": 18, "condition": "Foggy"},
"New York, NY": {"temp": 22, "condition": "Sunny"},
"London, UK": {"temp": 12, "condition": "Cloudy"}
}
data = mock_data.get(location, {"temp": 20, "condition": "Unknown"})
return f"Current weather in {location}: {data['temp']}°{units[0].upper()}, {data['condition']}"
def handle_query_database(query: str):
# NEVER execute raw SQL in production without sanitization!
# This is a mock example for demonstration
if "SELECT" not in query.upper():
return "Error: Only SELECT queries are allowed"
mock_results = [
{"id": 1, "name": "Alice", "email": "[email protected]"},
{"id": 2, "name": "Bob", "email": "[email protected]"}
]
return str(mock_results)
tool_handlers = {
"get_weather": handle_get_weather,
"query_database": handle_query_database
}
Step 3: The Core Agent Loop
Now let's build the main loop that processes Claude's responses and executes tools:
def run_agent(user_message: str, max_tool_rounds: int = 10):
messages = [{"role": "user", "content": user_message}]
for round_num in range(max_tool_rounds):
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
# Check if Claude wants to use a tool
if response.stop_reason == "tool_use":
for content_block in response.content:
if content_block.type == "tool_use":
tool_name = content_block.name
tool_input = content_block.input
print(f"🔧 Claude wants to use: {tool_name}")
print(f" Arguments: {tool_input}")
# Execute the tool
handler = tool_handlers.get(tool_name)
if handler:
try:
result = handler(**tool_input)
print(f" Result: {result[:100]}...")
except Exception as e:
result = f"Error executing {tool_name}: {str(e)}"
else:
result = f"Error: Unknown tool '{tool_name}'"
# Add the tool result to messages
messages.append({
"role": "assistant",
"content": response.content
})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": content_block.id,
"content": result
}
]
})
else:
# Claude is done using tools, return the final response
return response.content[0].text
return "Agent reached maximum tool call limit."
Step 4: Running Your Agent
Let's test it with a multi-step query:
result = run_agent(
"What's the weather in San Francisco? Also, find me the email address for customer ID 1."
)
print(f"\n🤖 Final response:\n{result}")
You'll see Claude first call get_weather, then query_database, and finally synthesize the results into a coherent response.
Advanced Patterns
Parallel Tool Use
Claude can call multiple tools in a single response, which is great for independent tasks. The SDK handles this automatically – just process all tool_use blocks in the response:
# In the agent loop, collect all tool calls first
tool_calls = [
block for block in response.content
if block.type == "tool_use"
]
Execute them all (potentially in parallel with asyncio)
results = []
for tool_call in tool_calls:
handler = tool_handlers.get(tool_call.name)
result = handler(**tool_call.input)
results.append({
"tool_use_id": tool_call.id,
"content": result
})
Then add all results back to messages
Strict Tool Use
If you want to force Claude to use a specific tool, use the tool_choice parameter:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"}, # Force this tool
messages=messages
)
Set tool_choice to {"type": "any"} to allow Claude to choose freely (default), or {"type": "auto"} to let Claude decide whether to use tools or not.
Tool Use with Prompt Caching
For production systems, enable prompt caching to reduce costs and latency:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
extra_headers={
"anthropic-beta": "prompt-caching-2024-07-31"
},
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": "You are a helpful assistant with tools.",
"cache_control": {"type": "ephemeral"}
},
{
"type": "text",
"text": user_message
}
]
}
]
)
Best Practices
- Write excellent tool descriptions – Claude relies on descriptions to decide when to use each tool. Be specific about when the tool should be called and what it returns.
- Handle errors gracefully – Always wrap tool execution in try/except blocks and return meaningful error messages so Claude can recover.
- Set reasonable limits – Always cap the number of tool call rounds to prevent infinite loops.
- Validate tool inputs – Never trust Claude's arguments blindly. Validate them before executing sensitive operations.
- Use streaming for UX – For chat applications, stream Claude's responses to show users what's happening in real-time.
Troubleshooting Common Issues
| Issue | Solution |
|---|---|
| Claude doesn't use tools | Improve tool descriptions; reduce the number of tools |
| Wrong tool called | Make tool names and descriptions more distinct |
| Infinite tool loops | Set max_tool_rounds; add validation to stop repetitive calls |
| Tool results ignored | Ensure you're returning results in the correct format |
Key Takeaways
- Tool use enables Claude to interact with external systems – Define tools with clear JSON schemas and descriptive names to help Claude choose correctly.
- The agent loop is straightforward – Send messages, check for
tool_usestop reason, execute tools, return results, and repeat until Claude responds with text. - Parallel tool calls are supported natively – Claude can call multiple independent tools in a single response, improving efficiency.
- Always implement safety guards – Validate inputs, limit tool call rounds, and handle errors gracefully to build production-ready agents.
- Prompt caching reduces costs – Enable caching for system prompts and tool definitions to optimize API usage in high-volume applications.