主题
Day 5 -- Tools 设计文档
设计目标:构建可扩展的工具系统,支持通过基类继承和装饰器两种方式定义工具,自动生成 JSON Schema 参数描述,并通过注册中心统一管理。
一、核心接口设计
1.1 Tool(抽象基类)
| 属性/方法 | 类型 | 说明 |
|---|---|---|
| name | str (abstract property) | 工具唯一标识 |
| description | str (abstract property) | 功能描述(给 LLM 看的) |
| parameters | dict (abstract property) | JSON Schema 格式的参数定义 |
| execute | async method | 实际执行逻辑 |
1.2 ToolResult
| 字段 | 类型 | 说明 |
|---|---|---|
| output | str | 成功结果 |
| error | Optional[str] | 错误信息 |
| is_error | bool | 是否出错 |
为什么不直接返回 str?
- 需要区分"成功但结果为空"和"执行出错"
- 上层可以根据 is_error 做不同处理(如日志、重试)
- 结构化返回值便于后续扩展(如添加 metadata)
1.3 @tool 装饰器
| 参数 | 说明 |
|---|---|
| name | 工具名称 |
| description | 工具描述 |
自动推导的内容:
- 从函数签名获取参数名和类型标注
- Python 类型到 JSON Schema 类型的映射
- 有默认值的参数标记为非必填
- 从 docstring 提取参数描述
1.4 ToolRegistry
| 方法 | 签名 | 说明 |
|---|---|---|
| register | register(tool: Tool) | 注册工具(name 重复则覆盖) |
| get | get(name: str) -> Optional[Tool] | 按名字获取工具 |
| list_tools | list_tools() -> List[Tool] | 获取所有已注册工具 |
| get_tool_definitions | get_tool_definitions() -> List[dict] | 生成 OpenAI API 兼容的工具定义 |
二、关键流程图
@tool 装饰器的工作原理
工具调用流程(与 Agent 协作)
三、设计决策与权衡
决策 1:两种工具定义方式(基类继承 vs 装饰器)
| 方案 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| 基类继承 | 复杂工具(需要初始化状态) | 灵活,可以有构造函数和内部状态 | 样板代码多 |
| @tool 装饰器 | 简单工具(纯函数) | 极简,一个函数搞定 | 不适合有状态的工具 |
选择理由:同时提供两种方式,让开发者根据需求选择。简单工具用装饰器(快),复杂工具用基类(灵活)。
决策 2:JSON Schema 自动生成
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自动从类型标注推导(选择) | 零手工编写,减少出错 | 复杂类型(如嵌套对象)支持有限 |
| 手动编写 JSON Schema | 完全控制 | 繁琐,容易和代码不同步 |
| Pydantic 模型定义参数 | 强大的类型系统 | 工具定义变重 |
类型映射表:
| Python 类型 | JSON Schema 类型 |
|---|---|
| str | string |
| int | integer |
| float | number |
| bool | boolean |
| list | array |
| dict | object |
| 无标注 | string(兜底) |
决策 3:工具执行异常处理
python
try:
result = await tool.execute(**args)
except Exception as e:
result = ToolResult(error=str(e), is_error=True)关键:工具内部异常不向上传播,而是包装成 ToolResult。这保证了 Agent 的推理循环不会因为一个工具报错就中断。
决策 4:Registry 用 dict 而非 list
| 方案 | 按名查找 | 重复检测 |
|---|---|---|
| Dict[name, tool](选择) | O(1) | 自动覆盖(或可改为报错) |
| List[tool] | O(n) 遍历 | 需要手动检查 |
四、与前序章节的集成点
- 被 Day 4 依赖:AgentRuntime 通过 ToolRegistry 获取工具定义和执行工具
- 被 Day 6 依赖:Skill 的 required_tools 引用 ToolRegistry 中的工具名
- 被 Day 8 依赖:MCPServer 把 ToolRegistry 的工具暴露为 MCP 服务;MCPClient 的 RemoteTool 也注册到 ToolRegistry
五、与真实生产系统的对比
| 维度 | miniOpenClaw | 生产级实现 |
|---|---|---|
| 参数校验 | JSON Schema(LLM 端校验) | JSON Schema + 服务端 Pydantic 二次校验 |
| 工具发现 | 硬编码注册 | 动态加载(插件系统、MCP 发现) |
| 执行安全 | 无沙箱 | Docker 容器 / 沙箱环境执行 |
| 权限控制 | 无 | 按用户/角色控制工具访问 |
| 并行执行 | 顺序执行 | asyncio.gather 并行 + 依赖图调度 |
| 超时 | 依赖 Agent 全局超时 | 每个工具独立超时设置 |