""" Step 01: 理解 Tool - 工具系统基础 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🎓 本节内容: 1. 什么是 Tool? 2. 如何定义一个 Tool? 3. 如何设计可扩展的工具系统? ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ """ from abc import ABC, abstractmethod from typing import Any, Dict, Optional from dataclasses import dataclass # ═══════════════════════════════════════════════════════════════════════════════ # 第一部分:理解 Tool 的基本结构 # ═══════════════════════════════════════════════════════════════════════════════ """ 在开始写代码之前,我们先理解 Tool 的本质: Tool = 名称 + 描述 + 执行逻辑 为什么这样设计?因为 LLM 选择工具时只看: 1. 工具名称(叫什么) 2. 工具描述(什么时候用) 然后 LLM 会决定:是否调用这个工具?调用时传什么参数? 所以一个好的 Tool 设计,必须: - 名称清晰、一目了然 - 描述准确、告诉 LLM 何时使用 - 执行逻辑独立、不影响其他工具 """ # ═══════════════════════════════════════════════════════════════════════════════ # 第二部分:定义 Tool 的数据结构 # ═══════════════════════════════════════════════════════════════════════════════ @dataclass # Python 数据类,自动生成 __init__ 等方法 class ToolResult: """ Tool 执行结果的数据结构 为什么需要单独定义这个? 因为 Tool 执行可能成功,也可能失败,我们需要统一格式来传递结果。 属性说明: success: 是否执行成功 result: 执行结果(如果成功) error: 错误信息(如果失败) metadata: 额外元数据(如执行时间、使用的参数等) """ success: bool result: Any = None error: Optional[str] = None metadata: Dict[str, Any] = None def __post_init__(self): """初始化后确保 metadata 不为 None""" if self.metadata is None: self.metadata = {} # ═══════════════════════════════════════════════════════════════════════════════ # 第三部分:定义抽象 Tool 基类(重要概念) # ═══════════════════════════════════════════════════════════════════════════════ class BaseTool(ABC): """ 所有工具的抽象基类 为什么需要抽象基类? 因为我们希望所有工具都有统一的接口,这样: 1. 任意 Tool 都可以被统一管理 2. 新增 Tool 时不需要修改已有代码(开闭原则) 3. 可以批量操作所有 Tool 抽象基类 = 定义"接口规范",子类负责"具体实现" """ # ============================================================ # 属性:每个 Tool 必须有的特性 # ============================================================ @property @abstractmethod def name(self) -> str: """ 工具名称 为什么用 @property? 因为 name 通常是只读的,我们不希望运行时被随意修改 为什么用 @abstractmethod? 因为这是一个"抽象方法",所有子类必须实现 如果不实现,Python 会报错,强制你提供具体实现 """ pass @property @abstractmethod def description(self) -> str: """ 工具描述 这是 LLM 理解"何时使用这个工具"的关键! 描述越准确,LLM 越能正确选择工具。 好的描述应该包含: 1. 工具能做什么 2. 输入参数是什么 3. 输出结果是什么 4. 适用的场景 """ pass # ============================================================ # 方法:Tool 的核心执行逻辑 # ============================================================ @abstractmethod def execute(self, **kwargs) -> ToolResult: """ 执行工具 参数使用 **kwargs 的原因: 不同的 Tool 可能需要不同的参数 用 **kwargs 可以接受任意参数,具体由子类决定 返回 ToolResult 而不是直接返回值的原因: 1. 统一成功/失败的判断 2. 可以附带错误信息 3. 可以附带元数据(执行时间、使用的参数等) 为什么是抽象方法? 因为"如何执行"是每个工具自己的事,基类不知道具体逻辑 """ pass # ============================================================ # 辅助方法:让 Tool 更容易使用 # ============================================================ def to_dict(self) -> dict: """ 将 Tool 转换为字典格式 这个方法用于: 1. 给 LLM 展示可用的工具列表 2. 序列化/反序列化工具配置 返回格式: { "name": "工具名称", "description": "工具描述", "parameters": {...} # 可选,参数schema } """ return { "name": self.name, "description": self.description, } def __repr__(self) -> str: """调试时显示工具信息""" return f"" # ═══════════════════════════════════════════════════════════════════════════════ # 第四部分:实现具体的 Tool # ═══════════════════════════════════════════════════════════════════════════════ class CalculatorTool(BaseTool): """ 计算器工具示例 这个工具演示: 1. 如何定义一个简单的 Tool 2. 如何处理输入参数 3. 如何返回结果 4. 如何处理错误 """ @property def name(self) -> str: return "calculator" @property def description(self) -> str: return """ 计算器工具,执行数学运算。 输入: expression: str - 数学表达式,如 "2 + 3" 或 "10 * 5" 输出: 计算结果(数字) 示例: expression="2 + 3" -> 5 expression="10 / 2" -> 5.0 expression="2 ** 3" -> 8 """ def execute(self, **kwargs) -> ToolResult: """ 执行计算 为什么用 try-except? 因为 eval() 可能执行恶意代码或语法错误 我们需要捕获异常,返回友好的错误信息 """ expression = kwargs.get("expression") # 参数验证 if expression is None: return ToolResult( success=False, error="缺少必需参数 'expression'" ) if not isinstance(expression, str): return ToolResult( success=False, error=f"参数 'expression' 应该是字符串,实际是 {type(expression)}" ) try: # 使用 eval 计算表达式 # 注意:生产环境中应该用更安全的计算方式 # 这里用 eval 是为了简化,实际推荐用 ast.literal_eval 或专用计算库 result = eval(expression, {"__builtins__": {}}, {}) return ToolResult( success=True, result=result, metadata={"expression": expression} ) except SyntaxError as e: return ToolResult( success=False, error=f"表达式语法错误: {e}" ) except ZeroDivisionError: return ToolResult( success=False, error="除数不能为零" ) except Exception as e: return ToolResult( success=False, error=f"计算错误: {e}" ) class SearchTool(BaseTool): """ 搜索工具示例 这个工具演示: 1. 如何模拟一个搜索功能 2. 如何返回结构化数据 3. 如何设计工具的"真实感" 实际应用中,这里会调用真实的搜索 API """ @property def name(self) -> str: return "web_search" @property def description(self) -> str: return """ 网络搜索工具,在互联网上搜索信息。 输入: query: str - 搜索关键词 limit: int - 返回结果数量(默认5条) 输出: 搜索结果列表,每条包含标题、链接、摘要 适用场景: - 查找最新资讯 - 搜索技术文档 - 查找某个问题的答案 """ def execute(self, **kwargs) -> ToolResult: """ 执行搜索 注意:这里用模拟数据演示 真实场景中,你需要调用 Bing/Google API """ query = kwargs.get("query", "") limit = kwargs.get("limit", 5) if not query: return ToolResult( success=False, error="搜索关键词不能为空" ) # 模拟搜索结果 # 真实场景:这里调用 search_api(query) mock_results = [ { "title": f"关于 '{query}' 的官方文档", "url": "https://example.com/doc", "snippet": f"这是关于 {query} 的详细官方文档..." }, { "title": f"{query} 入门教程", "url": "https://example.com/tutorial", "snippet": f"学习 {query} 的快速入门指南..." }, ] return ToolResult( success=True, result=mock_results[:limit], metadata={"query": query, "count": len(mock_results[:limit])} ) class JaspersoftCodeGeneratorTool(BaseTool): """ Jaspersoft JRXML 代码生成工具(对应你的实际项目) 这个工具演示: 1. 如何设计业务相关的 Tool 2. 如何处理复杂输入 3. 如何返回有意义的结果 这个工具的作用: 用户说"生成一个销售报表" -> Tool 调用 LLM 生成 JRXML 代码 -> 返回生成的代码 """ @property def name(self) -> str: return "jrxml_generator" @property def description(self) -> str: return """ JasperReports JRXML 报表生成工具。 根据用户的自然语言需求,生成 JRXML 报表模板代码。 输入: requirement: str - 用户的需求描述 context: str - 额外的上下文信息(如参考模板、数据源信息等) 输出: 生成的 JRXML 代码(字符串) 适用场景: - 从零生成新报表 - 基于现有模板修改 - 生成特定格式的报表 注意: 生成的代码需要通过 jrxml_validator 验证后才能使用 """ def execute(self, **kwargs) -> ToolResult: """ 执行 JRXML 生成 参数处理说明: - 使用 kwargs.get() 安全获取参数,避免 KeyError - 提供默认值作为后备 - 类型检查确保参数正确 """ requirement = kwargs.get("requirement", "") context = kwargs.get("context", "") if not requirement: return ToolResult( success=False, error="需求描述不能为空" ) # 实际场景中,这里会: # 1. 构建 Prompt # 2. 调用 LLM # 3. 提取生成的 JRXML 代码 # # 示例代码: # prompt = build_generation_prompt(requirement, context) # response = llm.invoke(prompt) # jrxml = extract_jrxml(response) # 模拟生成结果 mock_jrxml = f''' '2024-01-01']]> Product $F{{product_name}} ''' return ToolResult( success=True, result=mock_jrxml, metadata={ "requirement": requirement, "context_provided": bool(context), "note": "这是模拟结果,实际需要调用 LLM" } ) # ═══════════════════════════════════════════════════════════════════════════════ # 第五部分:工具管理系统 # ═══════════════════════════════════════════════════════════════════════════════ class ToolRegistry: """ 工具注册表 这个类负责: 1. 注册所有可用的 Tool 2. 根据名称查找 Tool 3. 列出所有可用 Tool(供 LLM 了解能力) 为什么需要这个? 因为一个 Agent 可能有很多 Tool 需要一个统一的地方来管理它们 设计模式:注册表模式(Registry Pattern) 核心思想:用一个中心来管理所有实例 """ def __init__(self): """初始化空的工具注册表""" self._tools: Dict[str, BaseTool] = {} def register(self, tool: BaseTool) -> None: """ 注册一个工具 为什么用 tool.name 作为 key? 因为 name 是工具的唯一标识符 同一个 name 的工具只能注册一次 实际应用: registry = ToolRegistry() registry.register(CalculatorTool()) registry.register(SearchTool()) """ if tool.name in self._tools: raise ValueError(f"工具 '{tool.name}' 已经注册过了") self._tools[tool.name] = tool def get(self, name: str) -> Optional[BaseTool]: """ 根据名称获取工具 返回 Optional[BaseTool]: 如果找到,返回工具实例 如果没找到,返回 None """ return self._tools.get(name) def list_tools(self) -> list[dict]: """ 列出所有已注册的工具 返回格式: [ {"name": "calculator", "description": "..."}, {"name": "web_search", "description": "..."}, ] 这个格式是为了方便给 LLM 展示可用工具列表 """ return [tool.to_dict() for tool in self._tools.values()] def execute(self, tool_name: str, **kwargs) -> ToolResult: """ 执行指定名称的工具 这是对用户暴露的统一接口 用户不需要知道具体哪个工具,只要说"执行这个" """ tool = self.get(tool_name) if tool is None: return ToolResult( success=False, error=f"工具 '{tool_name}' 不存在" ) return tool.execute(**kwargs) # ═══════════════════════════════════════════════════════════════════════════════ # 第六部分:演示代码 # ═══════════════════════════════════════════════════════════════════════════════ def demo(): """ 演示如何使用工具系统 这个函数展示: 1. 创建工具注册表 2. 注册工具 3. 执行工具 4. 查看可用工具 """ print("=" * 60) print("Step 01: 理解 Tool - 工具系统演示") print("=" * 60) # 1. 创建工具注册表 registry = ToolRegistry() # 2. 注册工具 registry.register(CalculatorTool()) registry.register(SearchTool()) registry.register(JaspersoftCodeGeneratorTool()) print("\n📋 已注册的工具列表:") for tool_info in registry.list_tools(): print(f" - {tool_info['name']}: {tool_info['description'][:50]}...") # 3. 执行计算器工具 print("\n🔧 执行计算器工具:") result = registry.execute("calculator", expression="2 + 3 * 4") if result.success: print(f" 结果: {result.result}") else: print(f" 错误: {result.error}") # 4. 执行搜索工具 print("\n🔍 执行搜索工具:") result = registry.execute("web_search", query="JasperReports 教程", limit=2) if result.success: for item in result.result: print(f" - {item['title']}") else: print(f" 错误: {result.error}") # 5. 执行 JRXML 生成工具 print("\n📄 执行 JRXML 生成工具:") result = registry.execute( "jrxml_generator", requirement="生成一个销售报表,显示月度汇总" ) if result.success: print(f" 生成成功! (长度: {len(result.result)} 字符)") print(f" 前100字符: {result.result[:100]}...") else: print(f" 错误: {result.error}") # 6. 演示错误处理 print("\n⚠️ 演示错误处理:") result = registry.execute("calculator", expression="1 / 0") if not result.success: print(f" 预期错误: {result.error}") if __name__ == "__main__": demo()