Tool Design Principles

A good Agent tool should follow these principles:

  • Single Responsibility: Each tool does one thing and does it well
  • Clear Interface: Inputs and outputs are explicit, easy to understand and use
  • Robust Error Handling: Gracefully handle various exceptional conditions
  • Observability: Provide logs and metrics for debugging and monitoring
  • Security: Permission control and auditing for sensitive operations

Tool Interface Design

from pydantic import BaseModel, Field
from typing import Optional, List, Any
from enum import Enum

class ToolCategory(str, Enum):
    SEARCH = "search"
    DATA = "data"
    ACTION = "action"
    UTILITY = "utility"

class ToolParameter(BaseModel):
    name: str
    type: str
    description: str
    required: bool = True
    enum: Optional[List[str]] = None

class ToolDefinition(BaseModel):
    name: str = Field(description="Tool name")
    description: str = Field(description="Detailed description to help Agent understand when to use")
    category: ToolCategory
    parameters: List[ToolParameter]
    risk_level: str = Field(default="low", pattern="^(low|medium|high|critical)$")

    def to_openai_function(self) -> dict:
        """Convert to OpenAI Function Calling format"""
        properties = {}
        required = []

        for param in self.parameters:
            prop = {"type": param.type, "description": param.description}
            if param.enum:
                prop["enum"] = param.enum
            properties[param.name] = prop
            if param.required:
                required.append(param.name)

        return {
            "name": self.name,
            "description": self.description,
            "parameters": {
                "type": "object",
                "properties": properties,
                "required": required
            }
        }

Implementing a Complete Tool

import logging
import time
from functools import wraps

logger = logging.getLogger(__name__)

def tool_logger(func):
    """Tool decorator: automatically log call and duration"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        try:
            result = func(*args, **kwargs)
            elapsed = time.time() - start
            logger.info(f"Tool {func.__name__} succeeded in {elapsed:.2f}s")
            return result
        except Exception as e:
            elapsed = time.time() - start
            logger.error(f"Tool {func.__name__} failed in {elapsed:.2f}s: {e}")
            raise
    return wrapper

class DatabaseQueryTool:
    """Database query tool"""

    definition = ToolDefinition(
        name="query_database",
        description="Query database, supports SQL queries. Used to fetch user data, order information, statistics reports, etc.",
        category=ToolCategory.DATA,
        parameters=[
            ToolParameter(
                name="query_type",
                type="string",
                description="Query type",
                enum=["user", "order", "stats"]
            ),
            ToolParameter(
                name="filters",
                type="object",
                description="Query filter conditions",
                required=False
            )
        ],
        risk_level="medium"
    )

    def __init__(self, db_connection):
        self.db = db_connection
        self.allowed_tables = ["users", "orders", "products"]

    @tool_logger
    def execute(self, query_type: str, filters: dict = None):
        # Security validation
        if query_type not in ["user", "order", "stats"]:
            return {"error": f"Unsupported query type: {query_type}"}

        # Build safe query
        query = self._build_safe_query(query_type, filters or {})

        # Execute query
        result = self.db.execute(query)

        return {
            "success": True,
            "data": result,
            "count": len(result)
        }

    def _build_safe_query(self, query_type: str, filters: dict):
        # Use parameterized queries to prevent SQL injection
        # Only allow querying predefined tables
        pass

Tool Registration and Discovery

class ToolRegistry:
    """Tool registry"""

    def __init__(self):
        self._tools: dict[str, Any] = {}

    def register(self, tool):
        """Register a tool"""
        name = tool.definition.name
        if name in self._tools:
            raise ValueError(f"Tool {name} already registered")
        self._tools[name] = tool
        logger.info(f"Registered tool: {name}")

    def get_all_definitions(self):
        """Get all tool definitions (for Function Calling)"""
        return [
            tool.definition.to_openai_function()
            for tool in self._tools.values()
        ]

    def execute(self, name: str, params: dict):
        """Execute a tool"""
        if name not in self._tools:
            return {"error": f"Tool not found: {name}"}

        tool = self._tools[name]

        # High-risk operations require confirmation
        if tool.definition.risk_level in ["high", "critical"]:
            if not self._confirm_execution(name, params):
                return {"error": "Operation cancelled by user"}

        return tool.execute(**params)

Tool Testing

import pytest

class TestDatabaseQueryTool:
    def setup_method(self):
        self.tool = DatabaseQueryTool(MockDB())

    def test_valid_query(self):
        result = self.tool.execute("user", {"name": "Zhang San"})
        assert result["success"] is True
        assert "data" in result

    def test_invalid_query_type(self):
        result = self.tool.execute("delete_all")
        assert "error" in result

    def test_sql_injection_prevention(self):
        result = self.tool.execute("user", {"name": "'; DROP TABLE users; --"})
        assert result["success"] is True  # Should not execute malicious SQL

Tool Performance Optimization

  • Result Caching: Cache results for queries with identical parameters
  • Timeout Control: Set execution timeout for each tool
  • Parallel Execution: Independent tool calls can be executed in parallel
  • Result Trimming: Results returned to the LLM should be concise, containing only necessary information

Summary

Tools are an extension of Agent capabilities. Good tool design empowers the Agent, while poor tool design hinders it. Investing time in proper tool design and testing is key to building high-quality Agent systems.