Why Tool Design Matters
An AI agent is only as good as its tools. Give it poorly designed tools, and it will fail—often in spectacular and unexpected ways.
After building hundreds of agent tools, here's what we've learned about designing tools that agents can actually use.
The Anatomy of a Good Tool
Clear, Unambiguous Names
Agents choose tools based on descriptions. Be explicit:
# Bad @tool def search(query: str): """Search for information.""" pass # Good @tool def search_company_knowledge_base(query: str): """Search the internal company knowledge base for policies, procedures, and documentation. Use this for questions about company-specific information, not general knowledge.""" pass
Typed Parameters with Descriptions
from pydantic import BaseModel, Field class CreateTicketParams(BaseModel): title: str = Field( description="Brief summary of the issue (max 100 chars)" ) description: str = Field( description="Detailed description of the issue" ) priority: Literal["low", "medium", "high", "critical"] = Field( description="Ticket priority. Use 'critical' only for production outages." ) assignee_email: Optional[str] = Field( default=None, description="Email of the person to assign. Leave empty for auto-assignment." )
Structured Return Values
class ToolResult(BaseModel): success: bool data: Optional[dict] error: Optional[str] suggestions: Optional[list[str]] = Field( description="Suggested next actions if the operation partially failed" ) @tool def create_ticket(params: CreateTicketParams) -> ToolResult: try: ticket = ticket_system.create(params) return ToolResult( success=True, data={"ticket_id": ticket.id, "url": ticket.url} ) except ValidationError as e: return ToolResult( success=False, error=str(e), suggestions=["Check the assignee email is valid"] )
Error Handling for Agents
Agents need actionable error messages:
class AgentFriendlyError(Exception): def __init__( self, message: str, recoverable: bool, suggestions: list[str] ): self.message = message self.recoverable = recoverable self.suggestions = suggestions @tool def send_email(to: str, subject: str, body: str) -> ToolResult: try: # Validation if not is_valid_email(to): raise AgentFriendlyError( message=f"Invalid email address: {to}", recoverable=True, suggestions=[ "Ask the user for the correct email address", "Search the contact database for the person's email" ] ) # Send email_service.send(to, subject, body) return ToolResult(success=True, data={"sent_to": to}) except AgentFriendlyError as e: return ToolResult( success=False, error=e.message, suggestions=e.suggestions )
Tool Composition
Build complex capabilities from simple tools:
# Simple tools @tool def get_customer(customer_id: str) -> Customer: """Retrieve customer information by ID.""" pass @tool def get_recent_orders(customer_id: str, limit: int = 5) -> list[Order]: """Get customer's recent orders.""" pass @tool def get_support_tickets(customer_id: str, status: str = "open") -> list[Ticket]: """Get customer's support tickets.""" pass # Composed tool for common pattern @tool def get_customer_360_view(customer_id: str) -> Customer360: """Get complete customer overview including profile, recent orders, and open support tickets. Use this when you need comprehensive customer context.""" customer = get_customer(customer_id) orders = get_recent_orders(customer_id) tickets = get_support_tickets(customer_id) return Customer360( customer=customer, recent_orders=orders, open_tickets=tickets, lifetime_value=calculate_ltv(orders), health_score=calculate_health(customer, orders, tickets) )
Testing Agent Tools
Unit Tests
def test_tool_returns_structured_result(): result = search_knowledge_base("vacation policy") assert isinstance(result, ToolResult) assert result.success or result.error is not None def test_tool_handles_invalid_input(): result = create_ticket(CreateTicketParams( title="x" * 200, # Too long description="test", priority="low" )) assert not result.success assert "title" in result.error.lower()
Integration Tests
async def test_agent_uses_tool_correctly(): agent = Agent(tools=[search_knowledge_base, create_ticket]) response = await agent.run( "What's our vacation policy? If it's unclear, create a ticket to HR." ) # Verify tool was called with reasonable parameters assert search_knowledge_base in agent.tool_calls assert "vacation" in agent.tool_calls[search_knowledge_base].query.lower()
Rate Limiting and Safety
class SafeTool: def __init__( self, tool_fn, rate_limit: int = 10, requires_confirmation: bool = False ): self.tool_fn = tool_fn self.rate_limiter = RateLimiter(rate_limit) self.requires_confirmation = requires_confirmation async def __call__(self, **kwargs): # Rate limiting if not self.rate_limiter.allow(): return ToolResult( success=False, error="Rate limit exceeded. Please wait before trying again." ) # Confirmation for dangerous operations if self.requires_confirmation: # Return a confirmation request instead of executing return ConfirmationRequest( action=self.tool_fn.__name__, params=kwargs, message="This action requires confirmation. Proceed?" ) return await self.tool_fn(**kwargs) # Usage delete_customer = SafeTool( _delete_customer, rate_limit=5, requires_confirmation=True )
Conclusion
Good tool design is the foundation of reliable AI agents. Invest time in clear interfaces, comprehensive error handling, and thorough testing. Your agents—and your users—will thank you.