1508 字
约 5 分钟
6
工具注册表
2026-10-13
无标签
工具注册表:一个接口 = 一个工具
系列:ToyAgent 智能体知识库 11/26
一句话核心:把工具从写死的 switch 里解放出来——统一协议 + 注册表,注册即生效、注销即消失,系统提示词里的工具清单永远与实际可调用工具一致。
为什么需要
朴素实现里加一个工具要改两处:系统提示词里的工具清单 + 执行器里的 switch 分支。工具一多,这两处必然失同步:
// 提示词里写了 get_weather,执行器却漏了分支——模型调用即报"未知工具"
// 或者反过来:执行器有 get_stock,提示词清单没写——模型永远不知道它能用
失同步的后果是隐性的:不报编译错,只在运行到某次对话时才炸。解法是让清单从代码里长出来,而不是人工维护——参考 harness 类实现的 ToolDefinition(一个 70 行左右的接口)+ ToolRegistry 组合:实现协议、注册进表,模型就能"看见";提示词清单由注册表自动渲染,永不脱节。
核心机制
协议四件套——一个接口定义一个工具的全部:
public interface ToolDefinition {
String name(); // 模型调用标识
String description(); // 用途:模型选工具的唯一依据
Map<String, String> parameters(); // 参数说明(生产中是 JSON Schema)
String execute(Map<String, Object> args); // 执行手脚
default boolean concurrencySafe() { return false; } // 可否并行(预留)
}
// 一个 record 就是一个工具——新增工具不再改任何现有代码
record WeatherTool() implements ToolDefinition {
public String name() { return "get_weather"; }
public String description() { return "查询指定城市的实时天气"; }
public Map<String, String> parameters() {
return Map.of("city", "城市名,如:杭州");
}
public String execute(Map<String, Object> args) {
return WeatherApi.query((String) args.get("city"));
}
}
注册表的三职责:
| 职责 | 方法 | 关键设计 |
|---|---|---|
| 注册 | register(tool) |
返回注销器 Runnable——持有它才能摘除工具,"注册-回收对称性" |
| 发现 | lookup(name) |
执行器只查表不认具体工具,新增工具零改动 |
| 清单生成 | promptCatalog() |
自动渲染成系统提示词工具清单,注册即生效、注销即消失 |
Runnable disposer = registry.register(new WeatherTool());
disposer.run(); // 注销:模型下一次对话就"看不见"这个工具了
注册、渲染与调用三者同源的关系:
graph LR
T["ToolDefinition 工具实现"] -->|register 返回注销器| R["ToolRegistry 注册表"]
R -->|promptCatalog 自动渲染| P["系统提示词工具清单"]
P --> M["模型选工具"]
M -->|工具名| X["执行器 lookup 查表"]
X --> R
R -->|disposer.run| OFF["注销:清单同步消失"]
图示:提示词清单与执行器都从同一个注册表读数据,注册即生效、注销即消失,两处天然不脱节。
举例
模拟对话轨迹——演示"注册即生效、注销即消失":
[初始状态] registry = {get_time, get_weather}
用户:查看工具清单
Agent:(promptCatalog() 渲染)
- get_time: 查询当前时间
- get_weather: 查询指定城市的实时天气
用户:卸载:get_time
Agent:Runnable disposer.run() → registry = {get_weather}
已卸载 get_time。
用户:现在几点了?
模型思考:想查时间 → 检索工具清单 → 查无 get_time
Agent:我当前没有可用的查询时间工具,无法告诉你准确时间。
(关键:模型**如实承认没有工具**,而不是瞎猜一个时间——
因为清单与执行器同源,"看不见"就真的调不了)
生产对照:插件化宿主(如 dsh-java 这类动态插件系统)里,插件停止/卸载时,宿主正是靠注销器回收该插件贡献的工具、系统提示词条目与 Hook 注册项——插件贡献的每一样东西都能被完整收回,不留半截尸体。而 MCP(见 09 篇)则是这套协议的跨进程标准化版本:tools/list 就是动态版的 promptCatalog()。
LangChain4j 视角:ToolProvider 接口就是注册表思想的框架化——每次请求回调 provideToolSpecifications(),返回什么模型就看见什么;你在 provider 里按会话权限过滤工具,等价于动态注册/注销。
面试考点
- 为什么注册器要返回注销器? 注册-回收对称性。只有注册没有注销,工具只能增不能减,动态会话(用户卸载插件、技能失效)就做不了;返回
Runnable把"撤销权"交给调用方。 - 如何保证提示词清单与执行器不脱节? 单一事实源:清单由注册表自动渲染,执行器只查注册表。两处读同一份数据,脱节在结构上不可能发生。
- description 字段为什么重要? 它是模型选工具的唯一依据。描述含糊会导致模型选错工具或该用不用——工具工程一半的质量在描述文案上。
生产化路径
- 参数升级为 JSON Schema:
Map<String, String>的简化参数说明换成标准 Schema,模型能生成更可靠的参数,框架也能做参数校验。 - 按会话/权限动态注册:不同用户、不同技能绑定看到不同工具子集——注册表按会话隔离,权限收敛在注册这一步完成。
- 并发标记落地:
concurrencySafe()从预留变成实际能力——标记安全的工具在多工具调用时并行执行,省延迟。 - 注销泄漏防护:插件/技能异常退出时注销器可能丢失,注册表要支持按 owner 批量回收,防止幽灵工具残留。
- 清单渲染预算:工具多了清单本身吃 token——按语义相关性裁剪注入清单的工具数(工具检索),而不是全量灌入。
评论
0 条
还没有评论,先写一条吧。