引言:
如果说 LLM 为 AI Agent 提供了 “大脑” 来进行推理和生成,RAG 技术为其提供了 “记忆” 来获取和利用知识,那么工具集成 (Tool Integration) 则是赋予 Agent “双手” 去执行实际操作、与现实世界进行交互。如果缺少工具集成,Agent 只能停留在 “纸上谈兵” 的阶段,无法真正地解决实际问题。工具是 Agent 连接虚拟和现实的桥梁,是 Agent 行动能力的体现。 本章将详细介绍如何为 AI Agent 集成工具支持,使其能够调用外部工具和 API 来执行各种操作,从而扩展 Agent 的能力边界,让 Agent 真正地 “行动” 起来。
+-----------+ +-----------------+ +-----------------+ | LLM | | Reasoning | | Tools | | (大脑) | | Engine | | (双手) | +-----------+ +--------+--------+ +--------+--------+ ^ | | | v | | +------------------------------------------+ | | | AI Agent | | | +------------------------------------------+ | | ^ | | | RAG | | | (Memory & Knowledge) | v v v +-----------+ +-----------------+ +-----------------+ | Prompt | | Context | | External World | | (指令/任务)| | (知识库) | | (API, Databases, etc.)| +-----------+ +-----------------+ +-----------------+ (图 37. 工具集成在 AI Agent 中的作用)
6.1 工具集成的重要性 (The Significance of Tool Integration)
6.1.1 超越语言的限制 (Beyond Language Limitations):
大型语言模型 (LLMs) 的主要能力在于处理和生成文本,它们本身并不具备与现实世界进行物理交互的能力。例如,一个 LLM 可以生成一篇关于如何预订酒店的文章,但它自身无法真正地完成酒店预订操作。工具的引入打破了这种局限,使得 Agent 能够通过调用外部工具来执行各种操作,例如:
- 查询数据库: 获取特定信息,例如产品库存、用户信息等。
- 发送邮件或短信: 例如,发送订单确认信息、会议提醒等。
- 调用 API: 例如,获取天气预报、股票行情、地图导航等。
- 控制硬件设备: 例如,操作机器人、智能家居设备等。
- 执行代码: 例如,运行 Python 脚本来进行数据分析或图像处理。
通过工具集成,Agent 的能力不再局限于文本的生成和处理,而是可以扩展到现实世界的各种应用场景中。
6.1.2 增强 Agent 的能力 (Enhancing Agent Capabilities):
工具为 Agent 提供了各种专门的能力,使其能够处理更加多样化和复杂的任务。例如:
- 获取实时信息: 通过集成
WebSearchTool
,Agent 可以获取最新的新闻、天气、股票等信息,从而提供更及时、更准确的服务。
- 执行特定领域的任务: 通过集成特定领域的工具,例如
MedicalDiagnosisTool
、FinancialAnalysisTool
等,Agent 可以成为特定领域的专家助手。
- 完成复杂的工作流程: 通过组合多个工具,Agent 可以完成复杂的工作流程,例如,一个旅行助手 Agent 可以调用航班查询工具、酒店预订工具、天气查询工具等,来帮助用户制定旅行计划。
- 验证信息: 使用工具来检索信息, 验证 LLM 生成内容的可靠性。
6.1.3 实现真正的智能化 (Enabling True Intelligence):
工具的使用使得 Agent 能够与外部世界进行交互, 从而实现真正的智能化。Agent 可以根据自身的推理和规划, 选择合适的工具来执行任务, 并根据工具的执行结果来调整自身的行为, 实现 “感知 - 思考 - 行动” 的闭环。这种与外部世界的交互能力, 是 Agent 智能化的重要体现, 也是 Agent 区别于传统 LLM 应用的关键所在。
总结: 工具集成是构建实用型 AI Agent 的关键步骤, 它使 Agent 能够超越语言的限制, 执行实际操作, 与外部世界进行交互, 从而实现真正的智能化。
6.2 工具架构 (Tool Architecture)
为了方便地管理和使用工具, 我们需要设计一个合理的工具架构。一个典型的工具架构包括以下几个组件:
Tool
基类: 定义了工具的通用接口。
- 具体的工具类: 继承
Tool
基类, 实现特定工具的功能。
ToolRegistry
类: 负责注册、管理和查找工具。
6.2.1
Tool
基类 (The Tool
Base Class)Tool
基类定义了所有工具都必须遵循的通用接口。它是一个抽象基类 (Abstract Base Class, ABC), 规定了工具的基本属性和方法。from abc import ABC, abstractmethod from typing import Any, Dict, Optional from dataclasses import dataclass @dataclass class ToolResult: """Represents the result of a tool execution.""" success: bool data: Any error: Optional[str] = None class Tool(ABC): """Base class for all tools.""" @property @abstractmethod def name(self) -> str: """The name of the tool.""" pass @property @abstractmethod def description(self) -> str: """Description of what the tool does.""" pass @property @abstractmethod def parameters(self) -> Dict[str, str]: """Dictionary of parameter names and their descriptions.""" pass @abstractmethod def execute(self, **kwargs) -> ToolResult: """Execute the tool with the given parameters.""" pass def to_prompt_format(self) -> str: """Convert tool information to a format suitable for prompts.""" params_str = "\\n".join(f" - {name}: {desc}" for name, desc in self.parameters.items()) return f"""Tool: {self.name} Description: {self.description} Parameters: {params_str}"""
代码解释:
ToolResult
类: 这是一个数据类 (dataclass), 用于表示工具执行的结果。success
: 一个布尔值, 表示工具是否执行成功。data
: 工具执行成功后返回的数据, 可以是任意类型。error
: 工具执行失败时的错误信息, 可选。
Tool
类: 这是一个抽象基类, 定义了所有工具的通用接口。@abstractmethod
装饰器: 将方法标记为抽象方法, 需要在子类中具体实现。name
属性: 工具的名称, 例如 "web_search", "wikipedia_search" 等。description
属性: 工具的功能描述, 例如 "Search the web for information about a topic"。parameters
属性: 一个字典, 描述了工具的参数, 键 (key) 是参数名称, 值 (value) 是参数的描述。execute(**kwargs) -> ToolResult
方法: 执行工具的方法, 接收关键字参数*kwargs
, 返回ToolResult
对象。to_prompt_format(self) -> str
: 将工具信息转换为 Prompt 可以使用的格式。
Tool
基类的作用:- 定义了工具的通用接口: 所有的工具类都必须继承
Tool
基类, 并实现其抽象方法, 从而保证了所有工具都具有相同的接口。
- 规范了工具的开发: 开发者在开发新的工具时, 只需要遵循
Tool
基类的接口规范即可, 无需考虑与其他组件的集成问题。
- 方便了 Agent 对工具的使用: Agent 可以通过
Tool
基类的接口来调用任何工具, 而无需关心具体工具的实现细节。
6.2.2 具体工具的实现 (Implementing Specific Tools)
在定义了
Tool
基类之后, 我们可以创建具体的工具类, 来实现各种各样的功能。每个工具类都需要继承 Tool
基类, 并实现其抽象方法。示例 1:
WikipediaTool
import wikipedia from typing import Dict, Any class WikipediaTool(Tool): """Tool for searching Wikipedia""" @property def name(self) -> str: return "wikipedia_search" @property def description(self) -> str: return "Search Wikipedia for information about a topic" @property def parameters(self) -> Dict[str, Dict[str, Any]]: return { "query": { "type": "string", "description": "The Wikipedia search query" } } def execute(self, **kwargs) -> ToolResult: try: query = kwargs.get("query") print(f"Searching Wikipedia for: {query}") search_results = wikipedia.search(query) if not search_results: return ToolResult( success=True, data="No Wikipedia articles found for the query." ) page = wikipedia.page(search_results[0]) summary = page.summary[:500] + "..." return ToolResult( success=True, data=f"Title: {page.title}\\nSummary: {summary}" ) except Exception as e: return ToolResult( success=False, data="", error=f"Wikipedia search failed: {str(e)}" )
代码解释:
WikipediaTool
类: 继承自Tool
基类, 实现了name
、description
、parameters
和execute
方法。
name
属性: 返回工具的名称 "wikipedia_search"。
description
属性: 返回工具的描述 "Search Wikipedia for information about a topic"。
parameters
属性: 返回工具的参数列表, 这里只有一个参数query
, 类型为字符串, 描述为 "The Wikipedia search query"。
execute
方法: 执行维基百科搜索操作。- 获取
query
参数。 - 使用
wikipedia.search
函数搜索query
。 - 如果找到结果, 则获取第一个搜索结果的页面内容, 并截取前 500 个字符作为摘要。
- 返回
ToolResult
对象, 其中success
为True
,data
为页面的标题和摘要。 - 如果搜索失败, 则返回
ToolResult
对象, 其中success
为False
,error
为错误信息。
示例 2:
WebSearchTool
import os from typing import Dict, Any from tavily import TavilyClient class WebSearchTool(Tool): """Tool for performing web searches using Tavily API""" def __init__(self): """Initialize the web search tool with API key.""" self.api_key = os.getenv('TAVILY_API_KEY', '') if not self.api_key: raise ValueError("TAVILY_API_KEY environment variable not set") @property def name(self) -> str: return "web_search" @property def description(self) -> str: return "Search the web for information about a topic" @property def parameters(self) -> Dict[str, Dict[str, Any]]: return { "query": { "type": "string", "description": "The search query to look up" } } def execute(self, **kwargs) -> ToolResult: try: query = kwargs.get("query") if not query: return ToolResult( success=False, data="", error="No query provided" ) client = TavilyClient(api_key=self.api_key) search_response = client.search(query=query) # Take the top 3 results results = search_response['results'][:3] # Format results formatted_results = [] for result in results: formatted_results.append({ "title": result.get('title', 'No title'), "content": result.get('content', 'No content'), "url": result.get('url', 'No URL') }) formatted_output = self._format_search_results(formatted_results) return ToolResult( success=True, data=formatted_output ) except Exception as e: return ToolResult( success=False, data="", error=f"Web search failed: {str(e)}" ) def _format_search_results(self, results: List[Dict[str, Any]]) -> str: """Formats the search results for display.""" formatted_output = "" for i, result in enumerate(results, 1): formatted_output += f"Result {i}:\\n" formatted_output += f" Title: {result.get('title', 'No title')}\\n" formatted_output += f" Content: {result.get('content', 'No content')}\\n" formatted_output += f" URL: {result.get('url', 'No URL')}\\n\\n" return formatted_output
代码解释:
WebSearchTool
类: 继承自Tool
基类, 实现了name
、description
、parameters
和execute
方法。
__init__
方法: 初始化WebSearchTool
, 从环境变量中读取TAVILY_API_KEY
。
name
属性: 返回工具的名称 "web_search"。
description
属性: 返回工具的描述 "Search the web for information about a topic"。
parameters
属性: 返回工具的参数列表, 这里只有一个参数query
, 类型为字符串, 描述为 "The search query to look up"。
execute
方法: 执行网络搜索操作。- 获取
query
参数。 - 使用
TavilyClient
和 API 密钥进行搜索。 - 获取前三个搜索结果, 并将其格式化成一个字符串。
- 返回
ToolResult
对象, 其中success
为True
,data
为格式化后的搜索结果。 - 如果搜索失败, 则返回
ToolResult
对象, 其中success
为False
,error
为错误信息。
_format_search_results
方法: 一个辅助方法,用于将搜索结果格式化成一个易于阅读的字符串。
其他工具示例:
除了
WikipediaTool
和 WebSearchTool
之外, 我们还可以根据需要创建各种各样的工具, 例如:CalculatorTool
: 执行数学计算。
CalendarTool
: 查询和管理日历。
EmailTool
: 发送和接收电子邮件。
DatabaseTool
: 连接数据库并执行 SQL 查询。
ImageGenerationTool
: 调用图像生成 API (例如 DALL-E), 根据文本描述生成图像。
CodeExecutionTool
: 执行代码片段, 例如 Python 代码。
这些工具可以根据具体的应用场景进行定制, 并可以组合使用, 以实现更复杂的功能。
6.2.3
ToolRegistry
类:ToolRegistry
类负责管理 Agent 可以使用的工具, 提供注册、获取和列出工具的方法。class ToolRegistry: """Registry for managing available tools.""" def __init__(self): self._tools: Dict[str, Tool] = {} def register(self, tool: Tool) -> None: """Register a new tool.""" if not isinstance(tool, Tool): raise TypeError("Tool must be an instance of Tool class") self._tools[tool.name] = tool def get_tool(self, name: str) -> Optional[Tool]: """Get a tool by name.""" return self._tools.get(name) def list_tools(self) -> List[str]: """List all registered tool names.""" return list(self._tools.keys()) def get_tools_prompt(self) -> str: """Get a formatted string of all tools for use in prompts.""" if not self._tools: return "No tools available." tools_str = "\\n\\n".join(tool.to_prompt_format() for tool in self._tools.values()) return f"""Available Tools: {tools_str} To use a tool, specify it in your response as: Tool: [tool_name] Parameters: - param1: value1 - param2: value2 """
代码解释:
__init__(self)
: 初始化方法, 创建一个空的字典_tools
来存储工具。
register(self, tool: Tool)
: 注册一个工具, 将工具的名称作为键, 工具对象作为值, 存储到_tools
字典中。
get_tool(self, name: str)
: 根据工具名称获取工具对象, 如果工具不存在, 则返回None
。
list_tools(self)
: 返回所有已注册工具的名称列表。
get_tools_prompt(self)
: 返回一个格式化的字符串, 列出所有可用的工具及其描述和参数, 用于添加到 Agent 的 Prompt 中。- 遍历所有已注册的工具, 调用每个工具的
to_prompt_format
方法, 将工具信息转换为 Prompt 格式的字符串。 - 将所有工具的信息字符串连接起来, 并在开头添加 "Available Tools:" 以及工具使用说明。
ToolRegistry
的作用:- 集中管理: 将所有可用的工具集中到一个地方进行管理, 方便 Agent 查找和使用。
- 解耦: 将 Agent 与具体的工具实现解耦, Agent 不需要知道每个工具的具体实现, 只需要知道工具的名称和参数即可。
- 易于扩展: 可以通过向
ToolRegistry
注册新的工具来扩展 Agent 的能力, 而无需修改 Agent 的核心代码。
6.3 工具的集成 (Integrating Tools with Agents)
现在我们已经定义了工具的架构, 接下来我们需要将工具集成到 Agent 中, 使 Agent 能够调用这些工具来执行任务。
6.3.1 在 Agent 中添加
ToolRegistry
:首先, 我们需要在
Agent
类的 __init__
方法中添加一个 tool_registry
参数, 用于传入 ToolRegistry
实例, 并将其保存在 self.tool_registry
属性中。class Agent: def __init__(self, name: str, context: Optional[ContextManager] = None, persistence: Optional[AgentPersistence] = None, tool_registry: Optional[ToolRegistry] = None): # ... 其他属性初始化 ... self.tool_registry = tool_registry or ToolRegistry() # 如果没有传入, 则创建一个新的 ToolRegistry # ... 其他代码 ...
6.3.2 在
_build_messages
中添加工具信息:为了让 LLM 知道 Agent 可以使用哪些工具, 我们需要将工具的信息添加到 Prompt 中。在
Agent
类的 _build_messages
方法中, 我们可以调用 self.tool_registry.get_tools_prompt()
方法来获取格式化的工具信息, 并将其添加到 messages
列表中。class Agent: # ... def _build_messages(self) -> List[Dict[str, str]]: """Build the messages list for the API call including system, instruction, history, context, and tools.""" messages = [{"role": "system", "content": self.persona}] if self.instruction: messages.append({"role": "user", "content": f"Global Instruction: {self.instruction}"}) # Add conversation history messages.extend(self.history) # Add context if available and relevant if self._context and self._context._current_context: messages.append({"role": "user", "content": f"Context: {self._context._current_context}"}) # Add tools information if self.tool_registry: messages.append({"role": "user", "content": self.tool_registry.get_tools_prompt()}) # Add current task if exists if self.task: messages.append({"role": "user", "content": f"Current Task: {self.task}"}) return messages # ...
代码解释:
if self.tool_registry:
: 检查 Agent 是否配置了ToolRegistry
。
messages.append({"role": "user", "content": self.tool_registry.get_tools_prompt()})
: 如果配置了ToolRegistry
, 则调用get_tools_prompt
方法获取格式化的工具信息, 并将其作为一条user
消息添加到messages
列表中。
6.3.3 解析工具调用指令 (parse_tool_usage
):
Agent 需要能够解析 LLM 生成的响应, 从中提取出工具调用指令。这通常需要根据预定义的格式进行文本解析。
def parse_tool_usage(response: str) -> Optional[Dict[str, Any]]: """Parse a response string to extract tool usage information.""" try: if "Tool:" not in response: return None lines = response.split('\\n') tool_info = {} # Find tool name for i, line in enumerate(lines): if line.startswith("Tool:"): tool_info["name"] = line.replace("Tool:", "").strip() break # Find parameters params = {} for line in lines: if ":" in line and "-" in line: param_line = line.split(":", 1) param_name = param_line[0].replace("-", "").strip() param_value = param_line[1].strip() params[param_name] = param_value tool_info["parameters"] = params return tool_info if tool_info.get("name") else None except Exception as e: print(f"Error parsing tool usage: {e}") return None
代码解释:
parse_tool_usage
函数接收一个字符串response
作为输入, 该字符串是 LLM 生成的响应。
- 函数首先检查
response
中是否包含 "Tool:" 字符串, 如果不包含, 则说明没有使用工具, 返回None
。
- 如果包含 "Tool:" 字符串, 则按行分割响应, 并查找 "Tool:" 开头的行, 提取出工具名称。
- 然后, 函数查找包含 ":" 和 "-" 的行, 提取出参数名称和参数值。
- 最后, 函数将工具名称和参数组装成一个字典, 并返回该字典。
- 如果解析过程中出现异常, 则打印错误信息, 并返回
None
。
这个函数假设 LLM 生成的工具调用指令遵循以下格式:
Tool: [tool_name] Parameters: - param1: value1 - param2: value2 ...
6.3.4 执行工具调用 (execute_tool
):
Agent 需要根据解析出的工具名称和参数, 执行相应的工具, 并获取工具的执行结果。
class Agent: # ... def execute_tool(self, tool_name: str, **kwargs) -> ToolResult: """ Executes a tool and returns the result. """ tool = self.tool_registry.get_tool(tool_name) if tool: return tool.execute(**kwargs) else: return ToolResult(success=False, data="", error=f"Unknown tool: {tool_name}") # ...
代码解释:
execute_tool
方法接收工具名称和参数作为输入。
- 它首先根据工具名称, 调用
self.tool_registry.get_tool
方法来获取对应的Tool
对象。
- 如果找到了对应的工具, 则调用工具的
execute
方法, 并将参数传递给它, 然后返回ToolResult
对象。
- 如果没有找到对应的工具, 则返回一个
ToolResult
对象, 表示工具执行失败, 并包含错误信息。
6.3.5 在 execute
方法中集成工具调用逻辑:
最后, 我们需要修改
Agent
类的 execute
方法, 将工具调用的逻辑集成进来。class Agent: # ... (其他方法) ... def execute(self, task: Optional[str] = None) -> str: """ Execute the agent with the given task or use existing task if none provided. Updates conversation history with the interaction. """ client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) if task is not None: self._task = task # 设置上下文查询, 触发 RAG 检索 (如果 context 被初始化) if self._context: self.set_context_query(self._task) # Use strategy if set if self._strategy: self._task = self._strategy.build_prompt(self._task, self._instruction) messages = self._build_messages() try: response = client.chat.completions.create( model="gpt-4-turbo", # or model of your choice messages=messages ) response_content = response.choices[0].message.content # Parse for tool usage tool_usage = parse_tool_usage(response_content) if tool_usage: tool_name = tool_usage["name"] tool_params = tool_usage["parameters"] tool_result = self.execute_tool(tool_name, **tool_params) if tool_result.success: response_content += f"\\nTool Result:\\n{tool_result.data}" else: response_content += f"\\nTool Error:\\n{tool_result.error}" else: # Process response through strategy if set if self._strategy: response_content = self._strategy.process_response(response_content) # Update history with the interaction if self.task: self._history.append({"role": "user", "content": self.task}) self._history.append({"role": "assistant", "content": response_content}) # Clear current task after execution self._task = "" return response_content except Exception as e: return f"An error occurred: {str(e)}" # ... (其他方法) ...
代码解释:
- 设置上下文查询: 在执行任务之前, 先设置上下文查询, 触发 RAG 检索 (如果
_context
被初始化的话)。
- 构建 Prompt: 调用
_build_messages
方法, 构建 Prompt, 其中包括工具信息。
- 调用 LLM: 调用 OpenAI 的 API, 获取 LLM 的响应。
- 解析工具调用指令: 调用
parse_tool_usage
函数, 解析 LLM 的响应, 判断是否需要调用工具。
- 执行工具调用: 如果需要调用工具, 则调用
execute_tool
方法, 执行工具调用, 并将工具的执行结果 (或错误信息) 添加到response_content
中。
- 处理 LLM 响应: 如果没有调用工具, 则根据当前的策略 (如果已设置) 处理 LLM 的响应。
- 更新历史记录: 将当前的任务和 LLM 的响应添加到 Agent 的历史记录中。
- 清除当前任务: 将
self._task
设置为空字符串, 表示当前任务已执行完毕。
- 返回响应: 返回 LLM 的响应 (可能包含工具执行的结果)。
通过以上步骤, 我们就将工具集成到了 Agent 的执行流程中, 使 Agent 能够根据 LLM 的输出来调用工具, 并将工具的执行结果整合到最终的响应中。
6.4 工具使用的流程和示例 (Tool Usage Process and Examples)
6.4.1 工具使用的流程:
- 接收任务: Agent 接收到一个任务 (Task), 例如用户输入的一个问题或指令。
- 推理和规划: Agent 根据任务描述, 利用 LLM 和推理策略 (例如 ReAct、CoT 等) 进行推理和规划, 决定是否需要调用工具来完成任务, 以及调用哪个工具, 需要哪些参数。
- 生成工具调用指令: 如果 Agent 决定调用工具, 则会生成一个特定格式的响应, 该响应中包含了工具调用的指令, 例如:
Thought: 我需要查询今天的天气信息。 Action: weather_tool Parameters: - city: 北京
- 解析工具调用指令: Agent 的
execute
方法会调用parse_tool_usage
函数 (或类似的方法) 来解析 LLM 的响应, 提取出工具名称 (例如weather_tool
) 和参数 (例如{"city": "北京"}
)。
- 执行工具调用: Agent 的
execute
方法根据工具名称, 使用ToolRegistry
获取对应的Tool
对象, 并调用Tool
对象的execute
方法, 传入解析得到的参数, 执行工具调用。
- 获取工具执行结果:
Tool
对象的execute
方法返回一个ToolResult
对象, 其中包含了工具执行的结果 (成功或失败, 以及返回的数据或错误信息)。
- 将结果整合到 LLM 的响应中: Agent 将工具执行的结果格式化成文本, 并添加到 LLM 的响应中, 作为下一步推理的依据, 或者作为最终答案的一部分。例如:
Observation: 今天北京的天气是晴天, 温度 25 摄氏度。
- 继续执行任务: Agent 根据工具执行的结果, 继续进行推理, 并决定下一步的行动, 或者生成最终的答案。
+-----------+ +-----------------+ +------------+ | User |------>| Agent |------>| LLM | | (Task) | | (Reasoning & | | (Generate | +-----------+ | Prompt | | Tool | ^ | Engineering) | | Usage | | +--------+--------+ | Instruction)| | | +------------+ | v | | +-----------------+ | Parse | | parse_tool_ | | | | usage() |<------------+ | +--------+--------+ | Execute | | | | v | | +-----------------+ | | | execute_tool() | | | +--------+--------+ | | | v | | +-----------------+ | +----------->| Tool | | | (e.g., WebSearch)| | Tool Result +-----------------+ +----------------------------------<| | v +-----------------+ | Final Output | +-----------------+ (图 35. Agent 工具调用流程)
6.4.2 示例:使用
WikipediaTool
和 WebSearchTool
假设我们已经创建了
WikipediaTool
和 WebSearchTool
两个工具, 并将它们注册到了 ToolRegistry
中, 并且 Agent 的 persona
, instruction
都已经配置完成。示例 1: 使用
WikipediaTool
查询信息Task: "什么是大型语言模型 (LLM)?"
Agent 的执行过程:
- Thought: 我需要查找大型语言模型 (LLM) 的定义,
WikipediaTool
可以用来搜索维基百科, 或许是一个合适的工具。
- Action:
wikipedia_search
- Parameters:
query
: "大型语言模型"
- Observation: (假设
WikipediaTool
返回了 LLM 的维基百科页面的摘要)Title: 大型语言模型\\\\nSummary: 大型语言模型 (LLM) 是一种... (省略)
- Thought: 我已经从维基百科获取了 LLM 的定义, 现在可以根据这些信息回答用户的问题了。
- Final Answer: 大型语言模型 (LLM) 是一种... (根据 Observation 中的信息总结答案)
示例 2: 使用
WebSearchTool
查询最新信息Task: "2023 年关于大型语言模型安全性的论文有哪些?"
Agent 的执行过程:
- Thought: 我需要查找 2023 年关于大型语言模型安全性的论文,
WebSearchTool
可以用来搜索网络, 或许是一个合适的工具。
- Action:
web_search
- Parameters:
query
: "2023 年 大型语言模型 安全性 论文"
- Observation: (假设
WebSearchTool
返回了相关的搜索结果, 例如论文标题、链接等)Result 1: Title: Enhancing LLM Security ... URL: ... Result 2: Title: A Survey on Security ... URL: ...
- Thought: 我已经从网络上搜索到了 2023 年关于大型语言模型安全性的论文, 现在可以列出这些论文的标题和链接了。
- Final Answer: 以下是一些 2023 年关于大型语言模型安全性的论文: (列出论文标题和链接)
示例 3: 结合使用
WikipediaTool
和 WebSearchTool
Task: "请介绍一下 ReAct 策略, 并列举一些 2023 年之后关于 ReAct 的最新研究论文。"
Agent 的执行过程:
- Thought: 我需要先介绍 ReAct 策略,
WikipediaTool
可以用来搜索维基百科, 可以先从维基百科获取 ReAct 的定义和介绍。
- Action:
wikipedia_search
- Parameters:
query
: "ReAct"
- Observation: (假设
WikipediaTool
返回了 ReAct 的维基百科页面的摘要) `Title: ReAct\\nSummary:Title: ReAct\\\\nSummary: ReAct 是一种将推理和行动与语言模型结合的通用范式... (省略)
- Thought: 我已经从维基百科获取了 ReAct 的基本信息, 接下来我需要查找 2023 年之后关于 ReAct 的最新研究论文,
WebSearchTool
可以用来搜索网络。
- Action:
web_search
- Parameters:
*
query
: "ReAct 2023 之后 研究论文"
- Observation: (假设
WebSearchTool
返回了相关的搜索结果)Result 1: Title: ReAct: 新的进展 ... URL: ... Result 2: Title: 使用 ReAct 进行复杂任务规划 ... URL: ...
- Thought: 我已经获取了 ReAct 的基本信息和最新的研究论文, 现在可以总结并回答用户的问题了。
- Final Answer: ReAct 是一种将推理和行动与语言模型结合的通用范式...(根据 Observation 1 中的信息总结 ReAct 的定义和介绍)。以下是一些 2023 年之后关于 ReAct 的最新研究论文: (列出 Observation 2 中搜索到的论文标题和链接)。
通过这些示例, 我们可以看到 Agent 如何利用不同的工具来完成各种任务, 以及 ReAct 模式如何帮助 Agent 更好地利用工具。
6.6 工具实现的最佳实践 (Best Practices for Tool Implementation)
在实现工具时, 我们需要遵循一些最佳实践, 以确保工具的质量、可靠性和可维护性。
- 6.6.1 错误处理 (Error Handling):
- 在
execute
方法中使用try...except
块来捕获和处理异常, 例如, 网络连接错误、API 调用失败、参数错误等。 - 返回
ToolResult
对象, 其中包含错误信息: 当工具执行失败时, 应该返回一个ToolResult
对象, 并将success
属性设置为False
, 在error
属性中包含错误信息。 - 提供有意义的错误信息: 错误信息应该尽可能地具体和有帮助, 以便 Agent 或开发者能够理解错误的原因, 并进行相应的处理。例如, 可以提供错误码、错误描述、堆栈跟踪等信息。
def execute(self, **kwargs) -> ToolResult: try: # ... 执行工具的具体逻辑 ... return ToolResult(success=True, data=result) except Exception as e: return ToolResult(success=False, data=None, error=f"Tool execution failed: {str(e)}")
- 6.6.2 清晰的文档 (Clear Documentation):
- 为每个工具类、每个方法和每个参数编写清晰的文档字符串 (docstrings), 解释其作用、参数类型、返回值类型、可能的错误等。
- 提供使用示例: 在文档字符串中提供工具的使用示例, 以便开发者能够快速了解如何使用该工具。
- 遵循 PEP 8, PEP 257 等代码规范: 使代码风格一致, 易于阅读和维护。
class MyTool(Tool): """ This is a description of what MyTool does. :param param1: Description of param1. :type param1: str :param param2: Description of param2. :type param2: int :raises MyException: If something goes wrong. :returns: A ToolResult object. :rtype: ToolResult """ @property def name(self) -> str: return "my_tool" @property def description(self) -> str: return "This is a description of what MyTool does." @property def parameters(self) -> Dict[str, str]: return { "param1": "Description of param1", "param2": "Description of param2" } def execute(self, **kwargs) -> ToolResult: """ Execute the tool with the given parameters. :param param1: The first parameter. :param param2: The second parameter. :raises MyException: If something goes wrong. :return: The result of the tool execution. :Example: >>> tool = MyTool() >>> result = tool.execute(param1="hello", param2=123) >>> result.success True """ try: # ... 执行工具的具体逻辑 ... return ToolResult(success=True, data=result) except Exception as e: return ToolResult(success=False, data=None, error=f"Tool execution failed: {str(e)}")
- 6.6.3 一致的接口 (Consistent Interface):
- 遵循
Tool
基类定义的接口: 所有的工具类都应该继承Tool
基类, 并实现其抽象方法。 - 使用
ToolResult
对象返回结果: 所有的工具都应该使用ToolResult
对象来返回执行结果, 以便 Agent 能够统一处理工具的输出。 - 参数命名和类型保持一致: 不同的工具可以使用相同的参数名和类型来表示相同的概念, 例如, 使用
query
表示查询字符串, 使用limit
表示返回结果的数量等。这样可以降低 Agent 的使用难度, 提高代码的可读性。
- 6.6.4 结果格式化 (Result Formatting):
- 将工具的执行结果格式化成易于 LLM 理解的文本格式, 例如, JSON、Markdown、YAML 等, 或者遵循 Agent 的 Prompt Template 中预定义的格式。
- 提供必要的信息: 确保工具的输出结果中包含了所有必要的信息, 以便 Agent 能够正确地理解和使用这些信息。例如,
WebSearchTool
的输出结果中应该包含搜索结果的标题、URL 和内容摘要。 - 控制输出长度: 对于可能产生大量输出的工具, 需要限制输出的长度, 避免超出 LLM 的输入长度限制, 或者对输出进行分页处理。例如, 可以限制
WebSearchTool
的输出结果只包含前 N 个搜索结果。
- 6.6.5 资源管理 (Resource Management):
- 安全地处理 API 密钥和其他凭证: 不要将 API 密钥直接硬编码在代码中, 而是应该将其存储在环境变量或配置文件中, 并在运行时读取。例如,
WebSearchTool
的__init__
方法中, 从环境变量中读取TAVILY_API_KEY
。 - 及时释放资源: 在使用完外部资源 (例如, 文件句柄、网络连接、数据库连接等) 后, 应该及时释放这些资源, 避免资源泄漏。可以使用
with
语句来管理资源的生命周期, 或者在finally
块中显式地释放资源。 - 处理 API 的速率限制: 许多 API 都有速率限制, 例如, 每秒钟最多调用 10 次。在调用 API 时, 需要考虑速率限制, 并在达到速率限制时进行等待或重试。可以使用一些现有的库来处理速率限制, 例如
ratelimit
库。
- 6.6.6 模块化 (Modularity):
- 保持工具实现的独立性和单一职责: 每个工具应该只负责一项具体的任务, 例如,
WikipediaTool
只负责搜索维基百科,WebSearchTool
只负责进行网络搜索。这样可以提高代码的可读性、可维护性和可重用性。 - 避免工具之间的耦合: 不同的工具之间应该尽量解耦, 避免一个工具的修改影响到其他工具。例如, 可以通过定义清晰的接口和数据格式来实现工具之间的解耦。
- 使用依赖注入: 可以通过依赖注入的方式, 将工具的依赖项 (例如, API 客户端、数据库连接等) 传递给工具, 从而提高工具的可测试性和可移植性。
- 6.6.7 可测试性 (Testability):
- 编写单元测试: 为每个工具类编写单元测试, 以确保工具的功能正确性。可以使用 Python 的
unittest
模块或pytest
框架来编写单元测试。 - 模拟外部依赖: 在测试工具时, 可以使用 Mock 对象来模拟外部依赖, 例如, 模拟 API 的响应, 从而避免对外部服务的依赖, 并提高测试的速度和可靠性。
- 测试不同的场景: 测试工具在不同场景下的行为, 例如, 正常情况、边界情况、异常情况等, 以确保工具的鲁棒性。
- 6.6.8 安全性 (Security):
- 输入验证: 对工具的输入参数进行验证, 例如, 检查参数的类型、长度、格式等, 防止非法输入导致的安全漏洞。
- 输出转义: 对工具的输出进行转义, 例如, 对 HTML 标签进行转义, 以防止 XSS 攻击。
- 权限控制: 对工具的使用权限进行控制, 例如, 只允许授权的 Agent 或用户使用某些工具, 防止未经授权的访问和操作。
- 避免代码注入: 如果工具需要执行用户提供的代码, 需要进行严格的安全检查, 防止代码注入攻击。例如, 可以使用沙箱技术来隔离执行环境, 限制代码的执行权限。
- 6.6.9 效率 (Efficiency):
- 优化工具的执行时间: 例如, 可以使用缓存来避免重复的 API 调用, 或者使用异步编程来提高并发性。
- 选择合适的数据结构和算法: 例如, 可以使用哈希表来加速查找操作, 或者使用排序算法来加速排序操作。
- 避免不必要的计算: 例如, 可以只在需要时才计算某个值, 或者使用延迟加载的方式来加载数据。
- 6.6.10 可扩展性 (Scalability):
- 设计无状态的工具: 尽可能将工具设计成无状态的, 以便能够水平扩展, 例如, 可以通过增加工具的实例数量来提高系统的吞吐量。
- 使用分布式架构: 对于需要处理大量数据的工具, 可以考虑使用分布式架构, 例如, 使用分布式数据库、分布式文件系统等。
- 支持异步操作: 对于耗时较长的操作, 可以使用异步操作, 避免阻塞 Agent 的主线程, 例如, 可以使用异步 I/O 或消息队列来实现异步操作。
6.7 工具使用的局限性和未来发展方向 (Limitations and Future Directions of Tool Use)
尽管工具的引入极大地增强了 AI Agent 的能力, 但目前仍然存在一些局限性, 需要在未来的研究和开发中不断改进。
- 6.7.1 工具调用的可靠性: 目前的工具调用主要依赖于 LLM 对 Prompt 的理解和解析, 容易受到 Prompt 设计、LLM 能力和输出格式的限制, 导致工具调用失败或参数错误。
- 后续研究方向:
- 更鲁棒的工具调用解析方法: 例如, 使用更强大的解析器, 或者结合多种解析方法, 提高工具调用指令提取的准确性。
- 工具调用的验证和纠错: 例如, 在执行工具调用之前, 对工具名称和参数进行验证, 并在出现错误时, 进行自动纠错或提示用户进行修正。
- 工具调用的形式化验证: 例如, 使用形式化方法来验证工具调用的正确性和安全性。
- 6.7.2 工具的发现和选择: 目前的 Agent 主要依赖于预先注册的工具列表, Agent 需要根据任务描述和自身能力, 从中选择合适的工具。当工具数量较多或任务比较复杂时, Agent 可能难以做出最佳选择, 或者无法找到合适的工具。
- 后续研究方向:
- 工具的自动发现: 例如, Agent 可以根据任务的描述, 自动从外部的工具库 (例如, 一个工具市场) 中搜索和选择合适的工具, 或者根据工具的描述信息, 自动判断哪些工具可以用于当前的任务。
- 工具的组合和编排: 例如, Agent 可以将多个工具组合起来使用, 以完成更复杂的任务, 或者根据任务的需求, 动态地编排工具的执行顺序。
- 基于强化学习的工具选择: 例如, 使用强化学习来训练 Agent, 使其能够根据任务的上下文和目标, 自动选择合适的工具, 并通过不断的尝试和反馈, 学习到最优的工具选择策略。
- 6.7.3 工具的表示和描述: 目前的工具描述主要依赖于自然语言, 这可能会导致歧义或信息缺失, 影响 Agent 对工具的理解和使用。
- 后续研究方向:
- 结构化的工具描述: 例如, 使用本体论或语义网技术来描述工具的功能和参数, 使 Agent 能够更准确地理解工具的作用, 并进行自动化的工具选择和调用。
- 工具的自动描述生成: 例如, 根据工具的代码或 API 文档, 自动生成工具的描述, 从而减少人工编写工具描述的工作量。
- 6.7.4 工具的执行效率: 调用外部工具通常需要通过网络进行通信, 可能会导致延迟和性能瓶颈, 特别是当 Agent 需要频繁调用工具时, 或者当工具的执行时间较长时。
- 后续研究方向:
- 工具的本地化部署: 例如, 将常用的工具部署在 Agent 的本地环境中, 以减少网络通信的开销。
- 工具调用的异步执行: 例如, 使用异步编程或消息队列来实现工具调用的异步执行, 避免阻塞 Agent 的主线程, 提高 Agent 的响应速度和并发能力。
- 工具调用的缓存机制: 例如, 将工具调用的结果缓存起来, 避免重复调用相同的工具和参数, 从而提高效率。
- 6.7.5 工具的安全性: Agent 调用外部工具可能会带来安全风险, 例如, 工具可能存在漏洞, 或者 Agent 可能会被恶意诱导调用不安全的工具。
- 后续研究方向:
- 工具的安全审查: 在使用工具之前, 需要对其进行安全审查, 确保工具的可靠性和安全性, 例如, 可以对工具的代码进行静态分析, 或者对工具的行为进行动态监控。
- 工具调用的权限控制: 限制 Agent 可以调用的工具的范围, 例如, 只允许 Agent 调用经过授权的工具, 或者根据 Agent 的角色和权限, 限制其可以调用的工具。
- 沙箱机制: 在沙箱环境中执行工具, 以隔离工具的执行环境, 防止工具对 Agent 或外部系统造成破坏。例如, 可以使用 Docker 容器来运行工具, 并限制容器的网络访问权限和资源使用量。
总结:
本章详细介绍了如何为 AI Agent 集成工具支持, 使其能够执行实际操作, 与外部世界进行交互。我们首先讨论了工具集成的重要性, 然后介绍了工具架构的设计, 包括
Tool
基类、具体的工具实现, 以及 ToolRegistry
类。接着, 我们展示了如何将工具集成到 Agent 架构中, 并解释了工具使用的流程和示例。最后, 我们总结了工具实现的最佳实践, 并探讨了工具使用的局限性和未来的发展方向。工具的集成, 使得 Agent 不再局限于文本的生成和处理, 而是能够真正地与现实世界进行交互, 执行各种各样的任务, 成为真正意义上的 “智能代理”。 通过本章的学习, 开发者可以掌握如何为 AI Agent 设计和实现工具, 并将其集成到 Agent 的工作流程中, 从而构建出更强大、更实用的 AI Agent。