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 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:
- Access External Data: Query databases, search the web, or retrieve information from APIs
- Perform Actions: Execute operations like sending emails, creating calendar events, or modifying data
- Process Specialized Tasks: Leverage domain-specific functionalities like mathematical calculations, code execution, or data analysis
Core Components
Text Only |
---|
| graph LR
A[Agent] --> B[Tool Manager]
B --> C1[Tool 1]
B --> C2[Tool 2]
B --> C3[Tool 3]
A --> D[Response Generator]
|
- Agent Core: The central LLM-based reasoning component
- Tool Manager: Middleware that handles tool registration, selection, and invocation
- Tools: Individual function implementations with clear interfaces
- Response Generator: Component that combines tool outputs with agent reasoning
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)}")
|
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
|
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
|
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
|
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:
- Input Validation: Always validate and sanitize inputs before tool execution
- Permission Management: Implement tool-specific permission controls
- Rate Limiting: Prevent abuse through appropriate rate limiting
- Sandboxing: Isolate tool execution environments where appropriate
- 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"}
|
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
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
|
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"]
|
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