1508 字
约 5 分钟
6
工具注册表
无标签

工具注册表:一个接口 = 一个工具

系列: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 字段为什么重要? 它是模型选工具的唯一依据。描述含糊会导致模型选错工具或该用不用——工具工程一半的质量在描述文案上。

生产化路径

  1. 参数升级为 JSON Schema:Map<String, String> 的简化参数说明换成标准 Schema,模型能生成更可靠的参数,框架也能做参数校验。
  2. 按会话/权限动态注册:不同用户、不同技能绑定看到不同工具子集——注册表按会话隔离,权限收敛在注册这一步完成。
  3. 并发标记落地:concurrencySafe() 从预留变成实际能力——标记安全的工具在多工具调用时并行执行,省延迟。
  4. 注销泄漏防护:插件/技能异常退出时注销器可能丢失,注册表要支持按 owner 批量回收,防止幽灵工具残留。
  5. 清单渲染预算:工具多了清单本身吃 token——按语义相关性裁剪注入清单的工具数(工具检索),而不是全量灌入。
工具注册表
https://clxhxhhr.top/posts/4711/
作者
clxstart
发布于
2026-10-13
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。