Skip to content

AI Agents: Mastering the Tool Use Design Pattern


Link: https://techcommunity.microsoft.com/t5/educator-developer-blog/ai-agents-mastering-the-tool-use-design-pattern/ba-p/4189012
Verified Views: 1,200+
Technology Area: Tool Integration, Function Calling, External APIs
Publication Date: March 24, 2025


Article Overview

External Integration

This article provides advanced patterns for tool integration, enabling agents to interact with external systems, APIs, and services effectively. As Part 4 of the AI Agents series, it demonstrates how to implement the Tool Use pattern that expands agent capabilities.

The Tool Use Design Pattern

The Tool Use pattern is a fundamental design approach that enables AI agents to interact with external systems, APIs, and services. By incorporating tools, agents gain the ability to:

  1. Access External Data: Query databases, search the web, or retrieve information from APIs
  2. Perform Actions: Execute operations like sending emails, creating calendar events, or modifying data
  3. Process Specialized Tasks: Leverage domain-specific functionalities like mathematical calculations, code execution, or data analysis

Core Components

Text Only
1
2
3
4
5
6
graph LR
    A[Agent] --> B[Tool Manager]
    B --> C1[Tool 1]
    B --> C2[Tool 2]
    B --> C3[Tool 3]
    A --> D[Response Generator]
  1. Agent Core: The central LLM-based reasoning component
  2. Tool Manager: Middleware that handles tool registration, selection, and invocation
  3. Tools: Individual function implementations with clear interfaces
  4. Response Generator: Component that combines tool outputs with agent reasoning

Implementing Tool Integration

Tool Definition Pattern

Tools should be defined with clear interfaces, including:

  • Name: Unique identifier for the tool
  • Description: Clear explanation of functionality for the agent
  • Parameters: Well-defined input schema
  • Return Type: Structured output format
  • Error Handling: Consistent approach to failures

Example Implementation:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pydantic import BaseModel
from typing import List, Optional

class SearchToolParams(BaseModel):
    query: str
    num_results: Optional[int] = 5

class SearchResult(BaseModel):
    title: str
    url: str
    snippet: str

class SearchTool:
    name = "web_search"
    description = "Search the web for information on a specific topic."

    async def execute(self, params: SearchToolParams) -> List[SearchResult]:
        # Implementation details to search the web
        try:
            results = await self._perform_search(params.query, params.num_results)
            return [SearchResult(**r) for r in results]
        except Exception as e:
            # Standardized error handling
            raise ToolExecutionError(f"Search failed: {str(e)}")

Tool Registration and Discovery

Tools should be dynamically discoverable by the agent, with a consistent registration system:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ToolRegistry:
    def __init__(self):
        self.tools = {}

    def register(self, tool_instance):
        """Register a tool with the registry."""
        self.tools[tool_instance.name] = tool_instance

    def get_tool(self, tool_name: str):
        """Get a tool by name."""
        return self.tools.get(tool_name)

    def get_available_tools(self):
        """Get descriptions of all available tools."""
        return [
            {
                "name": tool.name,
                "description": tool.description,
                "parameters": tool.__class__.__annotations__
            }
            for tool in self.tools.values()
        ]

# Usage
registry = ToolRegistry()
registry.register(SearchTool())
registry.register(CalculatorTool())

Function Calling Implementation

Modern LLMs support structured function calling. Here's how to implement this pattern:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
async def process_with_tools(user_input: str, registry: ToolRegistry, llm_client):
    # 1. Present available tools to the LLM
    available_tools = registry.get_available_tools()

    # 2. Request function calls from the LLM
    response = await llm_client.chat.completions.create(
        model="gpt-4",
        messages=[
            {"role": "system", "content": "You are an assistant with access to tools."},
            {"role": "user", "content": user_input}
        ],
        tools=[
            {
                "type": "function",
                "function": {
                    "name": tool["name"],
                    "description": tool["description"],
                    "parameters": tool["parameters_schema"]
                }
            } 
            for tool in available_tools
        ],
        tool_choice="auto"
    )

    # 3. Check if the LLM wants to use a tool
    message = response.choices[0].message

    if message.tool_calls:
        # 4. Execute the requested tool call
        tool_call = message.tool_calls[0]
        tool_name = tool_call.function.name
        tool_args = json.loads(tool_call.function.arguments)

        tool = registry.get_tool(tool_name)
        if tool:
            try:
                tool_result = await tool.execute(**tool_args)

                # 5. Send the tool result back to the LLM for final response
                final_response = await llm_client.chat.completions.create(
                    model="gpt-4",
                    messages=[
                        {"role": "system", "content": "You are an assistant with access to tools."},
                        {"role": "user", "content": user_input},
                        message,
                        {
                            "role": "tool",
                            "tool_call_id": tool_call.id,
                            "name": tool_name,
                            "content": json.dumps(tool_result)
                        }
                    ]
                )
                return final_response.choices[0].message.content
            except Exception as e:
                # Handle tool execution errors
                return f"I encountered an error while using the {tool_name} tool: {str(e)}"
        else:
            return f"I don't have access to the {tool_name} tool."
    else:
        # LLM provided a direct response without tools
        return message.content

Common Tool Categories

Information Retrieval Tools

Tools that access external data sources and bring information to the agent:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class WebSearchTool:
    name = "web_search"
    description = "Search the web for current information"

    async def execute(self, query: str, num_results: int = 5):
        # Implementation using a search API
        pass

class DatabaseQueryTool:
    name = "database_query"
    description = "Retrieve information from the company database"

    async def execute(self, query: str, database: str, parameters: dict = {}):
        # Secure database access implementation
        pass

Action Execution Tools

Tools that perform actions in external systems:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class EmailSenderTool:
    name = "send_email"
    description = "Send an email to specified recipients"

    async def execute(self, to: List[str], subject: str, body: str):
        # Email sending implementation
        pass

class CalendarEventTool:
    name = "create_calendar_event"
    description = "Create a calendar event"

    async def execute(self, title: str, start_time: str, end_time: str, 
                     attendees: List[str] = [], description: str = ""):
        # Calendar API implementation
        pass

Specialized Processing Tools

Tools that perform complex or domain-specific processing:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class MathTool:
    name = "calculate"
    description = "Perform complex mathematical calculations"

    async def execute(self, expression: str):
        # Safe expression evaluation
        pass

class CodeExecutionTool:
    name = "execute_code"
    description = "Execute code in a sandboxed environment"

    async def execute(self, code: str, language: str):
        # Secure code execution implementation
        pass

Best Practices

Security Considerations

Tool integration introduces potential security risks that must be addressed:

  1. Input Validation: Always validate and sanitize inputs before tool execution
  2. Permission Management: Implement tool-specific permission controls
  3. Rate Limiting: Prevent abuse through appropriate rate limiting
  4. Sandboxing: Isolate tool execution environments where appropriate
  5. Audit Logging: Maintain detailed logs of all tool invocations

Error Handling

Implement robust error handling to ensure graceful degradation:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class ToolExecutionError(Exception):
    """Base exception for tool execution failures."""
    pass

class PermissionDeniedError(ToolExecutionError):
    """Raised when permissions are insufficient."""
    pass

async def execute_tool_with_error_handling(tool, params):
    try:
        return await tool.execute(**params)
    except PermissionDeniedError as e:
        return {"error": "permission_denied", "message": str(e)}
    except ToolExecutionError as e:
        return {"error": "tool_error", "message": str(e)}
    except Exception as e:
        # Unexpected errors
        logging.error(f"Unexpected error in {tool.name}: {str(e)}")
        return {"error": "unknown_error", "message": "An unexpected error occurred"}

Tool Response Formatting

Format tool responses for optimal LLM consumption:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def format_tool_response_for_llm(tool_name: str, response: Any) -> str:
    """Format tool responses for LLM consumption."""
    if isinstance(response, dict) and "error" in response:
        return f"Error using {tool_name}: {response['message']}"

    if isinstance(response, (list, dict)):
        # For complex data, provide a summarized version
        summary = summarize_data(response)
        return f"Results from {tool_name}:\n{summary}\n\nFull data: {json.dumps(response, indent=2)}"

    # Simple response types
    return f"Results from {tool_name}: {response}"

Advanced Implementation Patterns

Tool Composition Pattern

Enable tools to work together by supporting composition:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
async def compose_tools(tools_pipeline: List[dict], initial_input):
    """Execute a pipeline of tools, passing outputs as inputs."""
    current_input = initial_input

    for step in tools_pipeline:
        tool = registry.get_tool(step["tool"])
        if not tool:
            raise ValueError(f"Unknown tool: {step['tool']}")

        # Map current input to tool parameters
        params = {
            param: evaluate_parameter(current_input, mapping)
            for param, mapping in step["parameter_mapping"].items()
        }

        # Execute the tool
        result = await tool.execute(**params)

        # Set as input for next tool
        current_input = result

    return current_input

Dynamic Tool Selection

Implement intelligent tool selection based on task needs:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
async def select_appropriate_tools(query: str, available_tools: List[dict], llm_client):
    """Use the LLM to select appropriate tools for a given query."""
    response = await llm_client.chat.completions.create(
        model="gpt-4",
        messages=[
            {
                "role": "system", 
                "content": "You are a tool selection assistant. Your job is to analyze a user query and determine which tools would be helpful."
            },
            {
                "role": "user", 
                "content": f"Query: {query}\n\nAvailable tools:\n{json.dumps(available_tools, indent=2)}\n\nSelect the most appropriate tools for this query."
            }
        ],
        response_format={"type": "json_object"}
    )

    selected_tools = json.loads(response.choices[0].message.content)
    return selected_tools["tools"]

Tool Result Caching

Implement caching for tool results to improve performance and reduce API calls:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ToolResultCache:
    def __init__(self, ttl_seconds=300):
        self.cache = {}
        self.ttl = ttl_seconds

    async def execute_with_cache(self, tool, params):
        """Execute a tool with caching."""
        cache_key = self._generate_cache_key(tool.name, params)

        # Check cache
        if cache_key in self.cache:
            entry = self.cache[cache_key]
            if time.time() < entry["expiry"]:
                return entry["result"]

        # Cache miss or expired, execute tool
        result = await tool.execute(**params)

        # Update cache
        self.cache[cache_key] = {
            "result": result,
            "expiry": time.time() + self.ttl
        }

        return result

    def _generate_cache_key(self, tool_name, params):
        """Generate a unique cache key for the tool and params."""
        params_str = json.dumps(params, sort_keys=True)
        return f"{tool_name}:{params_str}"

Real-World Example: Travel Assistant Agent

A travel assistant agent demonstrates the effective use of multiple tools:

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Tool definitions
flight_search_tool = FlightSearchTool()
hotel_booking_tool = HotelBookingTool() 
weather_tool = WeatherForecastTool()
currency_converter_tool = CurrencyConverterTool()

# Registry setup
travel_tools = ToolRegistry()
travel_tools.register(flight_search_tool)
travel_tools.register(hotel_booking_tool)
travel_tools.register(weather_tool)
travel_tools.register(currency_converter_tool)

# Agent implementation
class TravelAssistantAgent:
    def __init__(self, llm_client, tool_registry):
        self.llm = llm_client
        self.tools = tool_registry

    async def process_request(self, user_query: str):
        # Determine intent
        intent = await self._determine_intent(user_query)

        if intent == "flight_search":
            # Extract flight search parameters
            params = await self._extract_flight_parameters(user_query)

            # Use flight search tool
            flight_tool = self.tools.get_tool("flight_search")
            flights = await flight_tool.execute(**params)

            # If destination found, also get weather
            if flights and len(flights) > 0:
                destination = flights[0]["destination"]["city"]
                arrival_date = flights[0]["arrival_date"]

                # Get weather for destination
                weather_tool = self.tools.get_tool("weather_forecast")
                weather = await weather_tool.execute(
                    location=destination,
                    date=arrival_date
                )

                # Generate comprehensive response
                return await self._generate_response(user_query, {
                    "flights": flights,
                    "weather": weather
                })

        # Similar patterns for hotel booking, etc.

        # Default handling for other queries
        return await self._generate_direct_response(user_query)

Conclusion

The Tool Use Design Pattern is essential for building capable and practical AI agents. By following these implementation patterns and best practices, developers can create agents that effectively integrate with external systems and provide meaningful value to users.

In the next article in this series, we'll explore the Agentic RAG pattern, showing how to effectively combine retrieval-augmented generation with agent capabilities.


Series Navigation