Categories

Archives

A sample text widget

Etiam pulvinar consectetur dolor sed malesuada. Ut convallis euismod dolor nec pretium. Nunc ut tristique massa.

Nam sodales mi vitae dolor ullamcorper et vulputate enim accumsan. Morbi orci magna, tincidunt vitae molestie nec, molestie at mi. Nulla nulla lorem, suscipit in posuere in, interdum non magna.

Tool Use Patterns: Building LLM Agents That Can Take Action

Introduction: Tool use transforms LLMs from text generators into capable agents that can search the web, query databases, execute code, and interact with APIs. But implementing tool use well is tricky—models hallucinate tool calls, pass invalid arguments, and struggle with multi-step tool chains. The difference between a demo and production system lies in robust tool definitions, proper error handling, and intelligent orchestration. This guide covers practical tool use patterns: defining tools with clear schemas, implementing reliable tool execution, handling errors gracefully, building multi-tool workflows, and creating production-ready tool orchestration systems.

Tool Use
Tool Orchestration: Selection, Execution, Result Integration

Tool Definition and Registry

from dataclasses import dataclass, field
from typing import Any, Callable, Optional
from enum import Enum
import json

class ParameterType(Enum):
    """Types for tool parameters."""
    
    STRING = "string"
    INTEGER = "integer"
    NUMBER = "number"
    BOOLEAN = "boolean"
    ARRAY = "array"
    OBJECT = "object"

@dataclass
class ToolParameter:
    """A parameter for a tool."""
    
    name: str
    param_type: ParameterType
    description: str
    required: bool = True
    enum_values: list[str] = None
    default: Any = None

@dataclass
class Tool:
    """A tool that can be called by an LLM."""
    
    name: str
    description: str
    parameters: list[ToolParameter]
    handler: Callable
    requires_confirmation: bool = False
    timeout_seconds: int = 30
    
    def to_openai_schema(self) -> dict:
        """Convert to OpenAI function calling schema."""
        
        properties = {}
        required = []
        
        for param in self.parameters:
            prop = {
                "type": param.param_type.value,
                "description": param.description
            }
            
            if param.enum_values:
                prop["enum"] = param.enum_values
            
            properties[param.name] = prop
            
            if param.required:
                required.append(param.name)
        
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": {
                    "type": "object",
                    "properties": properties,
                    "required": required
                }
            }
        }
    
    def to_anthropic_schema(self) -> dict:
        """Convert to Anthropic tool schema."""
        
        properties = {}
        required = []
        
        for param in self.parameters:
            prop = {
                "type": param.param_type.value,
                "description": param.description
            }
            
            if param.enum_values:
                prop["enum"] = param.enum_values
            
            properties[param.name] = prop
            
            if param.required:
                required.append(param.name)
        
        return {
            "name": self.name,
            "description": self.description,
            "input_schema": {
                "type": "object",
                "properties": properties,
                "required": required
            }
        }

class ToolRegistry:
    """Registry of available tools."""
    
    def __init__(self):
        self.tools: dict[str, Tool] = {}
    
    def register(self, tool: Tool) -> None:
        """Register a tool."""
        self.tools[tool.name] = tool
    
    def get(self, name: str) -> Optional[Tool]:
        """Get a tool by name."""
        return self.tools.get(name)
    
    def list_tools(self) -> list[Tool]:
        """List all registered tools."""
        return list(self.tools.values())
    
    def to_openai_tools(self) -> list[dict]:
        """Get all tools in OpenAI format."""
        return [tool.to_openai_schema() for tool in self.tools.values()]
    
    def to_anthropic_tools(self) -> list[dict]:
        """Get all tools in Anthropic format."""
        return [tool.to_anthropic_schema() for tool in self.tools.values()]

# Example tool implementations
async def search_web(query: str, num_results: int = 5) -> dict:
    """Search the web for information."""
    # Simulated web search
    return {
        "results": [
            {"title": f"Result {i}", "snippet": f"Information about {query}"}
            for i in range(num_results)
        ]
    }

async def get_weather(location: str, units: str = "celsius") -> dict:
    """Get current weather for a location."""
    # Simulated weather API
    return {
        "location": location,
        "temperature": 22,
        "units": units,
        "conditions": "partly cloudy"
    }

async def calculate(expression: str) -> dict:
    """Evaluate a mathematical expression."""
    try:
        # Safe evaluation
        result = eval(expression, {"__builtins__": {}}, {})
        return {"result": result, "expression": expression}
    except Exception as e:
        return {"error": str(e)}

# Create and register tools
def create_default_tools() -> ToolRegistry:
    """Create registry with default tools."""
    
    registry = ToolRegistry()
    
    registry.register(Tool(
        name="search_web",
        description="Search the web for current information on any topic",
        parameters=[
            ToolParameter("query", ParameterType.STRING, "The search query"),
            ToolParameter("num_results", ParameterType.INTEGER, "Number of results to return", required=False)
        ],
        handler=search_web
    ))
    
    registry.register(Tool(
        name="get_weather",
        description="Get current weather conditions for a location",
        parameters=[
            ToolParameter("location", ParameterType.STRING, "City name or location"),
            ToolParameter("units", ParameterType.STRING, "Temperature units", required=False, enum_values=["celsius", "fahrenheit"])
        ],
        handler=get_weather
    ))
    
    registry.register(Tool(
        name="calculate",
        description="Evaluate a mathematical expression",
        parameters=[
            ToolParameter("expression", ParameterType.STRING, "Mathematical expression to evaluate")
        ],
        handler=calculate
    ))
    
    return registry

Tool Execution Engine

from dataclasses import dataclass
from typing import Any, Optional
import asyncio
import json
import traceback

@dataclass
class ToolCall:
    """A request to call a tool."""
    
    id: str
    name: str
    arguments: dict

@dataclass
class ToolResult:
    """Result of a tool execution."""
    
    call_id: str
    tool_name: str
    success: bool
    result: Any = None
    error: str = None
    execution_time_ms: float = 0

class ToolExecutor:
    """Execute tool calls safely."""
    
    def __init__(self, registry: ToolRegistry):
        self.registry = registry
    
    async def execute(self, call: ToolCall) -> ToolResult:
        """Execute a single tool call."""
        
        import time
        start = time.time()
        
        tool = self.registry.get(call.name)
        
        if not tool:
            return ToolResult(
                call_id=call.id,
                tool_name=call.name,
                success=False,
                error=f"Unknown tool: {call.name}"
            )
        
        try:
            # Validate arguments
            validation_error = self._validate_arguments(tool, call.arguments)
            if validation_error:
                return ToolResult(
                    call_id=call.id,
                    tool_name=call.name,
                    success=False,
                    error=validation_error
                )
            
            # Execute with timeout
            result = await asyncio.wait_for(
                tool.handler(**call.arguments),
                timeout=tool.timeout_seconds
            )
            
            execution_time = (time.time() - start) * 1000
            
            return ToolResult(
                call_id=call.id,
                tool_name=call.name,
                success=True,
                result=result,
                execution_time_ms=execution_time
            )
        
        except asyncio.TimeoutError:
            return ToolResult(
                call_id=call.id,
                tool_name=call.name,
                success=False,
                error=f"Tool execution timed out after {tool.timeout_seconds}s"
            )
        
        except Exception as e:
            return ToolResult(
                call_id=call.id,
                tool_name=call.name,
                success=False,
                error=f"Execution error: {str(e)}"
            )
    
    def _validate_arguments(self, tool: Tool, arguments: dict) -> Optional[str]:
        """Validate tool arguments."""
        
        # Check required parameters
        for param in tool.parameters:
            if param.required and param.name not in arguments:
                return f"Missing required parameter: {param.name}"
        
        # Check parameter types
        for param in tool.parameters:
            if param.name in arguments:
                value = arguments[param.name]
                
                if param.param_type == ParameterType.STRING and not isinstance(value, str):
                    return f"Parameter {param.name} must be a string"
                
                if param.param_type == ParameterType.INTEGER and not isinstance(value, int):
                    return f"Parameter {param.name} must be an integer"
                
                if param.param_type == ParameterType.NUMBER and not isinstance(value, (int, float)):
                    return f"Parameter {param.name} must be a number"
                
                if param.param_type == ParameterType.BOOLEAN and not isinstance(value, bool):
                    return f"Parameter {param.name} must be a boolean"
                
                if param.enum_values and value not in param.enum_values:
                    return f"Parameter {param.name} must be one of: {param.enum_values}"
        
        return None
    
    async def execute_batch(self, calls: list[ToolCall]) -> list[ToolResult]:
        """Execute multiple tool calls in parallel."""
        
        tasks = [self.execute(call) for call in calls]
        return await asyncio.gather(*tasks)

class RetryingToolExecutor:
    """Tool executor with retry logic."""
    
    def __init__(
        self,
        executor: ToolExecutor,
        max_retries: int = 3,
        retry_delay: float = 1.0
    ):
        self.executor = executor
        self.max_retries = max_retries
        self.retry_delay = retry_delay
    
    async def execute(self, call: ToolCall) -> ToolResult:
        """Execute with retries."""
        
        last_result = None
        
        for attempt in range(self.max_retries):
            result = await self.executor.execute(call)
            
            if result.success:
                return result
            
            last_result = result
            
            # Don't retry certain errors
            if "Unknown tool" in (result.error or ""):
                return result
            
            if "Missing required parameter" in (result.error or ""):
                return result
            
            # Wait before retry
            if attempt < self.max_retries - 1:
                await asyncio.sleep(self.retry_delay * (attempt + 1))
        
        return last_result

LLM Tool Integration

from dataclasses import dataclass
from typing import Any, Optional
import json

@dataclass
class ToolUseResponse:
    """Response from tool-using LLM."""
    
    content: str
    tool_calls: list[ToolCall]
    tool_results: list[ToolResult]
    finished: bool

class ToolUsingLLM:
    """LLM client with tool use capabilities."""
    
    def __init__(
        self,
        client: Any,
        registry: ToolRegistry,
        model: str = "gpt-4o-mini",
        max_tool_rounds: int = 5
    ):
        self.client = client
        self.registry = registry
        self.executor = ToolExecutor(registry)
        self.model = model
        self.max_tool_rounds = max_tool_rounds
    
    async def chat(
        self,
        messages: list[dict],
        system_prompt: str = None
    ) -> ToolUseResponse:
        """Chat with tool use."""
        
        all_messages = []
        if system_prompt:
            all_messages.append({"role": "system", "content": system_prompt})
        all_messages.extend(messages)
        
        all_tool_calls = []
        all_tool_results = []
        
        for round_num in range(self.max_tool_rounds):
            # Call LLM with tools
            response = await self.client.chat.completions.create(
                model=self.model,
                messages=all_messages,
                tools=self.registry.to_openai_tools(),
                tool_choice="auto"
            )
            
            message = response.choices[0].message
            
            # Check if model wants to use tools
            if not message.tool_calls:
                return ToolUseResponse(
                    content=message.content or "",
                    tool_calls=all_tool_calls,
                    tool_results=all_tool_results,
                    finished=True
                )
            
            # Add assistant message
            all_messages.append({
                "role": "assistant",
                "content": message.content,
                "tool_calls": [
                    {
                        "id": tc.id,
                        "type": "function",
                        "function": {
                            "name": tc.function.name,
                            "arguments": tc.function.arguments
                        }
                    }
                    for tc in message.tool_calls
                ]
            })
            
            # Execute tool calls
            for tc in message.tool_calls:
                call = ToolCall(
                    id=tc.id,
                    name=tc.function.name,
                    arguments=json.loads(tc.function.arguments)
                )
                all_tool_calls.append(call)
                
                result = await self.executor.execute(call)
                all_tool_results.append(result)
                
                # Add tool result to messages
                all_messages.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": json.dumps(result.result if result.success else {"error": result.error})
                })
        
        # Max rounds reached
        return ToolUseResponse(
            content="Maximum tool use rounds reached",
            tool_calls=all_tool_calls,
            tool_results=all_tool_results,
            finished=False
        )

class AnthropicToolUsingLLM:
    """Anthropic Claude with tool use."""
    
    def __init__(
        self,
        client: Any,
        registry: ToolRegistry,
        model: str = "claude-3-5-sonnet-20241022",
        max_tool_rounds: int = 5
    ):
        self.client = client
        self.registry = registry
        self.executor = ToolExecutor(registry)
        self.model = model
        self.max_tool_rounds = max_tool_rounds
    
    async def chat(
        self,
        messages: list[dict],
        system_prompt: str = None
    ) -> ToolUseResponse:
        """Chat with tool use."""
        
        all_tool_calls = []
        all_tool_results = []
        current_messages = list(messages)
        
        for round_num in range(self.max_tool_rounds):
            response = await self.client.messages.create(
                model=self.model,
                max_tokens=4096,
                system=system_prompt or "",
                messages=current_messages,
                tools=self.registry.to_anthropic_tools()
            )
            
            # Check stop reason
            if response.stop_reason == "end_turn":
                # Extract text content
                text_content = ""
                for block in response.content:
                    if block.type == "text":
                        text_content += block.text
                
                return ToolUseResponse(
                    content=text_content,
                    tool_calls=all_tool_calls,
                    tool_results=all_tool_results,
                    finished=True
                )
            
            # Process tool use blocks
            assistant_content = []
            tool_use_blocks = []
            
            for block in response.content:
                if block.type == "text":
                    assistant_content.append({"type": "text", "text": block.text})
                elif block.type == "tool_use":
                    assistant_content.append({
                        "type": "tool_use",
                        "id": block.id,
                        "name": block.name,
                        "input": block.input
                    })
                    tool_use_blocks.append(block)
            
            current_messages.append({"role": "assistant", "content": assistant_content})
            
            # Execute tools and add results
            tool_results_content = []
            
            for block in tool_use_blocks:
                call = ToolCall(
                    id=block.id,
                    name=block.name,
                    arguments=block.input
                )
                all_tool_calls.append(call)
                
                result = await self.executor.execute(call)
                all_tool_results.append(result)
                
                tool_results_content.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result.result if result.success else {"error": result.error})
                })
            
            current_messages.append({"role": "user", "content": tool_results_content})
        
        return ToolUseResponse(
            content="Maximum tool use rounds reached",
            tool_calls=all_tool_calls,
            tool_results=all_tool_results,
            finished=False
        )

Tool Selection and Routing

from dataclasses import dataclass
from typing import Any, Optional

@dataclass
class ToolSelectionResult:
    """Result of tool selection."""
    
    selected_tools: list[str]
    confidence: float
    reasoning: str

class ToolSelector:
    """Select appropriate tools for a query."""
    
    def __init__(self, client: Any, registry: ToolRegistry, model: str = "gpt-4o-mini"):
        self.client = client
        self.registry = registry
        self.model = model
    
    async def select_tools(
        self,
        query: str,
        max_tools: int = 3
    ) -> ToolSelectionResult:
        """Select tools needed for a query."""
        
        tools_description = "\n".join([
            f"- {tool.name}: {tool.description}"
            for tool in self.registry.list_tools()
        ])
        
        prompt = f"""Given this user query, select which tools (if any) would be helpful.

Available tools:
{tools_description}

User query: {query}

Respond in JSON format:
{{
    "selected_tools": ["tool1", "tool2"],
    "reasoning": "Brief explanation of why these tools are needed"
}}

If no tools are needed, return an empty list."""
        
        response = await self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"}
        )
        
        import json
        result = json.loads(response.choices[0].message.content)
        
        # Validate selected tools exist
        valid_tools = [
            t for t in result.get("selected_tools", [])
            if self.registry.get(t)
        ][:max_tools]
        
        return ToolSelectionResult(
            selected_tools=valid_tools,
            confidence=0.9 if valid_tools else 0.5,
            reasoning=result.get("reasoning", "")
        )

class FilteredToolRegistry:
    """Registry that filters tools based on selection."""
    
    def __init__(self, base_registry: ToolRegistry):
        self.base_registry = base_registry
        self.allowed_tools: set[str] = set()
    
    def set_allowed_tools(self, tool_names: list[str]) -> None:
        """Set which tools are allowed."""
        self.allowed_tools = set(tool_names)
    
    def to_openai_tools(self) -> list[dict]:
        """Get filtered tools in OpenAI format."""
        return [
            tool.to_openai_schema()
            for tool in self.base_registry.list_tools()
            if tool.name in self.allowed_tools
        ]
    
    def get(self, name: str) -> Optional[Tool]:
        """Get tool if allowed."""
        if name in self.allowed_tools:
            return self.base_registry.get(name)
        return None

class SmartToolRouter:
    """Route queries to appropriate tool configurations."""
    
    def __init__(
        self,
        client: Any,
        registry: ToolRegistry,
        selector: ToolSelector
    ):
        self.client = client
        self.registry = registry
        self.selector = selector
        self.executor = ToolExecutor(registry)
    
    async def process(
        self,
        query: str,
        system_prompt: str = None
    ) -> ToolUseResponse:
        """Process query with smart tool routing."""
        
        # First, select relevant tools
        selection = await self.selector.select_tools(query)
        
        if not selection.selected_tools:
            # No tools needed, direct response
            response = await self.client.chat.completions.create(
                model="gpt-4o-mini",
                messages=[
                    {"role": "system", "content": system_prompt or "You are a helpful assistant."},
                    {"role": "user", "content": query}
                ]
            )
            
            return ToolUseResponse(
                content=response.choices[0].message.content,
                tool_calls=[],
                tool_results=[],
                finished=True
            )
        
        # Create filtered registry
        filtered = FilteredToolRegistry(self.registry)
        filtered.set_allowed_tools(selection.selected_tools)
        
        # Use tool-enabled LLM with filtered tools
        tool_llm = ToolUsingLLM(
            self.client,
            filtered,
            max_tool_rounds=3
        )
        
        return await tool_llm.chat(
            [{"role": "user", "content": query}],
            system_prompt
        )

Production Tool Service

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

# Initialize components
registry = create_default_tools()
executor = ToolExecutor(registry)
tool_llm = None  # Initialize with client
tool_selector = None
smart_router = None

class ChatRequest(BaseModel):
    messages: list[dict]
    system_prompt: Optional[str] = None
    max_tool_rounds: int = 5

class ToolCallRequest(BaseModel):
    name: str
    arguments: dict

class SelectToolsRequest(BaseModel):
    query: str
    max_tools: int = 3

@app.post("/v1/chat")
async def chat_with_tools(request: ChatRequest):
    """Chat with tool use."""
    
    response = await tool_llm.chat(
        request.messages,
        request.system_prompt
    )
    
    return {
        "content": response.content,
        "tool_calls": [
            {
                "id": tc.id,
                "name": tc.name,
                "arguments": tc.arguments
            }
            for tc in response.tool_calls
        ],
        "tool_results": [
            {
                "call_id": tr.call_id,
                "tool_name": tr.tool_name,
                "success": tr.success,
                "result": tr.result,
                "error": tr.error
            }
            for tr in response.tool_results
        ],
        "finished": response.finished
    }

@app.post("/v1/chat/smart")
async def smart_chat(request: ChatRequest):
    """Chat with smart tool routing."""
    
    query = request.messages[-1]["content"] if request.messages else ""
    
    response = await smart_router.process(query, request.system_prompt)
    
    return {
        "content": response.content,
        "tools_used": [tc.name for tc in response.tool_calls],
        "finished": response.finished
    }

@app.post("/v1/tools/execute")
async def execute_tool(request: ToolCallRequest):
    """Execute a single tool."""
    
    call = ToolCall(
        id="direct_call",
        name=request.name,
        arguments=request.arguments
    )
    
    result = await executor.execute(call)
    
    return {
        "success": result.success,
        "result": result.result,
        "error": result.error,
        "execution_time_ms": result.execution_time_ms
    }

@app.post("/v1/tools/select")
async def select_tools(request: SelectToolsRequest):
    """Select appropriate tools for a query."""
    
    result = await tool_selector.select_tools(
        request.query,
        request.max_tools
    )
    
    return {
        "selected_tools": result.selected_tools,
        "confidence": result.confidence,
        "reasoning": result.reasoning
    }

@app.get("/v1/tools")
async def list_tools():
    """List available tools."""
    
    return {
        "tools": [
            {
                "name": tool.name,
                "description": tool.description,
                "parameters": [
                    {
                        "name": p.name,
                        "type": p.param_type.value,
                        "description": p.description,
                        "required": p.required
                    }
                    for p in tool.parameters
                ]
            }
            for tool in registry.list_tools()
        ]
    }

@app.get("/v1/tools/{tool_name}")
async def get_tool(tool_name: str):
    """Get tool details."""
    
    tool = registry.get(tool_name)
    if not tool:
        raise HTTPException(status_code=404, detail="Tool not found")
    
    return {
        "name": tool.name,
        "description": tool.description,
        "parameters": [
            {
                "name": p.name,
                "type": p.param_type.value,
                "description": p.description,
                "required": p.required,
                "enum_values": p.enum_values
            }
            for p in tool.parameters
        ],
        "timeout_seconds": tool.timeout_seconds,
        "requires_confirmation": tool.requires_confirmation
    }

@app.get("/health")
async def health():
    return {"status": "healthy"}

References

Conclusion

Tool use is what transforms LLMs from impressive text generators into practical agents that can take action in the world. Start with clear tool definitions—good descriptions and parameter schemas help the model understand when and how to use each tool. Implement robust execution with proper validation, timeouts, and error handling. Use retry logic for transient failures but fail fast on validation errors. Multi-round tool use enables complex workflows where the model can chain multiple tools together, using results from one tool to inform the next. Smart tool selection reduces latency and improves accuracy by only presenting relevant tools for each query. For production systems, log all tool calls for debugging and auditing, implement rate limiting to prevent runaway tool use, and consider requiring confirmation for destructive operations. The key insight is that tool use quality depends as much on your tool definitions and execution infrastructure as on the LLM itself—invest in both.