593 lines
20 KiB
Python
593 lines
20 KiB
Python
"""
|
||
Step 01: 理解 Tool - 工具系统基础
|
||
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
|
||
🎓 本节内容:
|
||
1. 什么是 Tool?
|
||
2. 如何定义一个 Tool?
|
||
3. 如何设计可扩展的工具系统?
|
||
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
"""
|
||
|
||
from abc import ABC, abstractmethod
|
||
from typing import Any, Dict, Optional
|
||
from dataclasses import dataclass
|
||
import json
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 第一部分:理解 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: {self.name}>"
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 第四部分:实现具体的 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'''<?xml version="1.0" encoding="UTF-8"?>
|
||
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports"
|
||
name="GeneratedReport" pageWidth="595" pageHeight="842">
|
||
<queryString>
|
||
<![CDATA[SELECT * FROM sales WHERE date > '2024-01-01']]>
|
||
</queryString>
|
||
<field name="product_name" class="java.lang.String"/>
|
||
<field name="quantity" class="java.lang.Integer"/>
|
||
<field name="price" class="java.math.BigDecimal"/>
|
||
<band height="50">
|
||
<staticText>
|
||
<reportElement x="0" y="0" width="100" height="20"/>
|
||
<text>Product</text>
|
||
</staticText>
|
||
<textField>
|
||
<reportElement x="0" y="20" width="100" height="20"/>
|
||
<textFieldExpression>$F{{product_name}}</textFieldExpression>
|
||
</textField>
|
||
</band>
|
||
</jasperReport>'''
|
||
|
||
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()
|