Building Tool-Using Agents
Building Tool-Using Agents
Tool-using agents represent a fundamental advancement in AI system design, enabling language models to interact with external systems, APIs, and functions. This lesson explores how to build agents that can intelligently select and invoke tools to solve complex problems that require real-world knowledge and actions.
Why Tool-Using Agents Matter
Standard language models operate purely through text generation. Tool-using agents expand their capabilities by:
- Accessing Real-Time Data: Query APIs, databases, and knowledge bases
- Taking Actions: Execute calculations, create resources, modify systems
- Combining Intelligence: Use LLM reasoning with deterministic tool execution
- Error Recovery: Handle tool failures and retry with alternative approaches
This architecture transforms agents from passive information providers into active participants in workflows.
Function Calling Fundamentals
Modern LLMs like Claude support function calling through structured prompting or dedicated function calling APIs. The agent receives a list of available tools with descriptions and parameters, then decides which tools to use based on user requests.
Defining Tool Schemas
Tools must be precisely defined so the model understands when and how to use them:
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get current weather for a location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit"
}
},
"required": ["location"]
}
},
{
"name": "calculate_route",
"description": "Calculate driving route between two locations",
"input_schema": {
"type": "object",
"properties": {
"origin": {"type": "string"},
"destination": {"type": "string"},
"avoid_highways": {"type": "boolean"}
},
"required": ["origin", "destination"]
}
}
]
Clear descriptions are critical. The model must understand:
- What the tool does
- What parameters it accepts
- When it should be called
- What to expect in return
Basic Tool-Using Loop
The fundamental pattern involves:
- Send user message with available tools
- Model decides if and which tools to invoke
- Execute the tools
- Return results to the model
- Model generates final response
def process_tool_call(tool_name, tool_input):
"""Execute the actual tool based on name and input."""
if tool_name == "get_weather":
location = tool_input["location"]
unit = tool_input.get("unit", "fahrenheit")
# In real implementation, call actual API
return f"Weather in {location}: 72°F, partly cloudy"
elif tool_name == "calculate_route":
origin = tool_input["origin"]
destination = tool_input["destination"]
return f"Route from {origin} to {destination}: 45 minutes, 32 miles"
return "Unknown tool"
def run_agent(user_message):
"""Run agent loop with tool calling."""
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
# Check if we're done
if response.stop_reason == "end_turn":
# Extract final text response
for block in response.content:
if hasattr(block, 'text'):
return block.text
break
# Process tool calls
if response.stop_reason == "tool_use":
# Add assistant response to messages
messages.append({"role": "assistant", "content": response.content})
# Process each tool call
tool_results = []
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
# Execute tool
result = process_tool_call(tool_name, tool_input)
# Add tool result
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
# Add tool results to messages
messages.append({"role": "user", "content": tool_results})
return "No response generated"
# Example usage
result = run_agent("What's the weather in San Francisco and how far is it to Los Angeles?")
print(result)
Building Custom Tools
Beyond predefined tools, agents often need custom tools specific to your application:
Database Query Tool
def create_database_tools():
"""Create tools for database operations."""
return [
{
"name": "query_users",
"description": "Query user database by criteria",
"input_schema": {
"type": "object",
"properties": {
"country": {
"type": "string",
"description": "Filter by country"
},
"min_age": {
"type": "integer",
"description": "Minimum age filter"
},
"limit": {
"type": "integer",
"description": "Maximum results"
}
},
"required": ["country"]
}
}
]
def query_users_impl(country, min_age=None, limit=10):
"""Implementation of user query tool."""
# In production, use actual database
users_db = [
{"id": 1, "name": "Alice", "age": 28, "country": "USA"},
{"id": 2, "name": "Bob", "age": 35, "country": "USA"},
{"id": 3, "name": "Charlie", "age": 22, "country": "USA"},
{"id": 4, "name": "Diana", "age": 45, "country": "Canada"},
]
results = [u for u in users_db if u["country"] == country]
if min_age:
results = [u for u in results if u["age"] >= min_age]
return json.dumps(results[:limit])
API Call Tool with Authentication
def create_api_tools():
"""Create tools for external API calls."""
return [
{
"name": "call_external_api",
"description": "Call external REST API",
"input_schema": {
"type": "object",
"properties": {
"endpoint": {
"type": "string",
"description": "API endpoint path"
},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT"],
"description": "HTTP method"
},
"params": {
"type": "object",
"description": "Query or request parameters"
}
},
"required": ["endpoint", "method"]
}
}
]
def call_external_api_impl(endpoint, method, params=None):
"""Implementation with error handling."""
import requests
api_key = os.getenv("API_KEY")
headers = {"Authorization": f"Bearer {api_key}"}
try:
if method == "GET":
response = requests.get(endpoint, params=params, headers=headers, timeout=5)
elif method == "POST":
response = requests.post(endpoint, json=params, headers=headers, timeout=5)
else:
return json.dumps({"error": "Unsupported method"})
response.raise_for_status()
return response.text
except requests.RequestException as e:
return json.dumps({"error": str(e)})
Error Handling and Resilience
Tool-using agents must gracefully handle failures:
Input Validation
def validate_tool_input(tool_name, tool_input):
"""Validate tool input before execution."""
if tool_name == "get_weather":
if "location" not in tool_input:
return False, "location parameter required"
if not isinstance(tool_input["location"], str):
return False, "location must be a string"
return True, None
return True, None
def safe_execute_tool(tool_name, tool_input):
"""Execute tool with validation and error handling."""
valid, error_msg = validate_tool_input(tool_name, tool_input)
if not valid:
return {"error": error_msg, "success": False}
try:
result = process_tool_call(tool_name, tool_input)
return {"result": result, "success": True}
except Exception as e:
return {"error": str(e), "success": False}
Retry Logic with Backoff
import time
def execute_with_retry(tool_name, tool_input, max_retries=3):
"""Execute tool with exponential backoff retry."""
for attempt in range(max_retries):
try:
result = process_tool_call(tool_name, tool_input)
return result, True
except Exception as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
time.sleep(wait_time)
continue
return str(e), False
Tool Chaining and Composition
Agents often need to invoke multiple tools in sequence, with outputs from one tool feeding into another:
def run_sequential_agent(user_message):
"""Agent that chains multiple tool calls."""
messages = [{"role": "user", "content": user_message}]
max_iterations = 10
iteration = 0
while iteration < max_iterations:
iteration += 1
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
tools=tools,
messages=messages,
system="""You are a helpful agent that uses tools to accomplish tasks.
Think step by step about what information you need.
Use tools strategically to gather data, then synthesize your answer."""
)
if response.stop_reason == "end_turn":
for block in response.content:
if hasattr(block, 'text'):
return block.text
break
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = process_tool_call(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
return "Max iterations reached"
Best Practices
1. Clear Tool Descriptions
Tool descriptions should be detailed but concise. Include:
- What the tool does
- When to use it
- Parameter constraints
- Expected output format
2. Input Constraints
Define allowed values and ranges:
"input_schema": {
"type": "object",
"properties": {
"quantity": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Number of items (1-100)"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high"],
"description": "Task priority level"
}
}
}
3. Resource Limits
Protect against tool abuse with limits:
def rate_limited_tool_call(tool_name, tool_input, calls_per_minute=10):
"""Implement rate limiting on tool calls."""
current_calls = get_call_count(tool_name)
if current_calls >= calls_per_minute:
return {"error": "Rate limit exceeded"}
record_call(tool_name)
return process_tool_call(tool_name, tool_input)
4. Logging and Monitoring
Track all tool invocations for debugging and analytics:
import logging
logger = logging.getLogger(__name__)
def logged_tool_call(tool_name, tool_input):
"""Log all tool calls."""
logger.info(f"Calling tool: {tool_name} with input: {tool_input}")
start_time = time.time()
try:
result = process_tool_call(tool_name, tool_input)
duration = time.time() - start_time
logger.info(f"Tool {tool_name} succeeded in {duration:.2f}s")
return result
except Exception as e:
duration = time.time() - start_time
logger.error(f"Tool {tool_name} failed after {duration:.2f}s: {e}")
raise
Key Takeaway
Tool-using agents leverage function calling to extend LLM capabilities beyond text generation. By defining clear tool schemas, implementing robust error handling, and properly composing tool calls, you create agents that can solve complex, real-world problems requiring both reasoning and action.
Exercises
-
Build a Calculator Tool: Create an agent with tools for arithmetic operations (add, subtract, multiply, divide) that can solve mathematical word problems.
-
Database Query Agent: Implement an agent that queries a mock database with multiple tools (filter_users, get_user_details, search_products) to answer complex questions.
-
Error Handling: Add timeout logic and validation to your tool definitions. Test how the agent handles invalid inputs and tool failures.
-
Tool Composition: Create an agent that chains multiple tools together, such as fetching user data, then calculating statistics from that data.
-
Real API Integration: Implement one tool that calls an actual public API (weather, news, cryptocurrency prices) with proper error handling and rate limiting.
-
Analysis: Compare the performance of agents with different tool sets. Measure how tool availability affects agent success rates and response quality.