Files
jaspersoft-agent-learn/step_01_tools/concept.py
T

592 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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: {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()