Designing Tools for AI Agents: A Practical Guide

Amit Patel

Amit Patel

ML Platform Lead

December 15, 202410 min read
AI AgentsTool DesignAPI DesignBest Practices
Designing Tools for AI Agents: A Practical Guide

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.

Share this article:
Back to all posts

Ready to build production AI?

We help companies ship AI systems that actually work. Let's talk about your project.

Start a conversation