Intermediate

Building Tool-Using Agents

Lesson 2 of 4 Estimated Time 55 min

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:

  1. Send user message with available tools
  2. Model decides if and which tools to invoke
  3. Execute the tools
  4. Return results to the model
  5. 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

  1. Build a Calculator Tool: Create an agent with tools for arithmetic operations (add, subtract, multiply, divide) that can solve mathematical word problems.

  2. 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.

  3. Error Handling: Add timeout logic and validation to your tool definitions. Test how the agent handles invalid inputs and tool failures.

  4. Tool Composition: Create an agent that chains multiple tools together, such as fetching user data, then calculating statistics from that data.

  5. Real API Integration: Implement one tool that calls an actual public API (weather, news, cryptocurrency prices) with proper error handling and rate limiting.

  6. Analysis: Compare the performance of agents with different tool sets. Measure how tool availability affects agent success rates and response quality.