工具系统:从注册到调度
Hermes 如何让 50+ 个工具自动被发现、安全过滤、并行分发
通过 Hermes 探秘 Agent 工程 | 第 3 篇 · 查看全部
[系列文章导航]
工具系统解决了什么问题?
一个 Agent 再聪明,如果只能"说话"不能"做事",就只是一个聊天机器人。工具系统就是 Agent 的手脚——让模型能读写文件、执行命令、搜索网络、操作浏览器、管理定时任务……
但工具越多,管理越复杂:
- 怎么让新增的工具被 Agent 感知又不改核心代码?
- 怎么让不同平台(CLI vs 网关 vs 子代理)看到不同的工具集?
- 怎么让工具调用安全可控?
Hermes 用三个机制解决这些问题:自注册、toolset 分层、capabilities 检查。
机制一:模块级自注册
最精巧的设计是——每个工具文件自己注册自己。
Hermes 规定:任何放在 tools/ 目录下的 Python 文件,只要在模块顶层调用 registry.register(),就会被系统自动发现。你不需要在一个"总清单"里添加新工具的引用。
registry.register(
name="read_file",
toolset="file",
schema=READ_FILE_SCHEMA,
handler=_handle_read_file,
check_fn=_check_file_reqs,
emoji="📖",
max_result_size_chars=100_000
)
| 参数 | 含义 |
|---|---|
name | 工具名,模型用这个名称调用工具 |
toolset | 工具归属的分组,用于批量启用/禁用 |
schema | OpenAI function calling 格式的参数描述 |
handler | 实际执行函数 |
check_fn | 可用性检查函数(比如检查 Docker 是否可用) |
emoji | 显示用图标 |
max_result_size_chars | 输出结果的最大字符数 |
发现过程:AST 扫描
你可能会问:系统怎么知道 tools/ 下哪些文件调用了 registry.register?
答案很巧妙——不用运行代码就能发现。Harmes 的 discover_builtin_tools() 函数在导入模块之前,先对文件做 AST(抽象语法树)扫描,寻找模块顶层的 registry.register() 调用。只有被 AST 判定为"会注册工具"的模块,才会被真正导入。
tools/*.py → AST 扫描 → 发现 registry.register() → 导入模块 → 执行注册
这个设计的好处是零配置扩展:丢一个新文件到 tools/,加一行 registry.register(),Agent 下次启动就能用。
机制二:Toolset 分层
光有注册还不够。50+ 个工具不能同时塞给所有场景——CLI 用户需要 terminal,Telegram bot 不需要;子代理可能只给 read_file 和 write_file;webhook 回调必须严格限制为只读工具。
Hermes 的办法是toolset(工具集)。
toolsets.py 定义了一组命名的工具集,比如 web(web_search, web_extract)、terminal(terminal, process)、file(read_file, write_file, patch, search_files)等。每个 toolset 可以引用其他 toolset(includes 字段),形成组合。
三层过滤
最终给模型的工具列表,经过三层过滤:
- 启用集(enabled_toolsets):用户配置启用了哪些 toolset,取并集
- 禁用集(disabled_toolsets):从并集中减去这些
- capabilities 检查:对每个工具调用
check_fn(),去掉当前环境不支持的
机制三:capabilities 检查与缓存
很多工具的可用性取决于外部环境。比如 terminal 工具在非 local 后端可能不可用,browser 需要 Playwright 安装。
TTL 缓存
check_fn 的结果缓存 30 秒,同一个会话里连续调用不重复检查。
瞬断抑制
更精妙的是"最近一次成功抑制“机制:如果某个 check_fn 最近成功过(证明功能确实可用),那么接下来 60 秒内的失败会被当作"瞬断"忽略掉——工具仍然可用,不报错。
分发:handle_function_call()
当模型返回工具调用时,Agent 通过 handle_function_call() 分发。这个函数是整个工具系统的"路由器”。
路由逻辑
模型输出 tool_calls
↓
handle_function_call(name, args, task_id, ...)
↓
参数类型强制转换(字符串 "42" → 整数 42)
↓
工具 Search bridge 特殊处理
↓
查找 ToolEntry → 调用 handler(args) → 返回 JSON 结果
参数类型强制转换
不同模型的 function calling 实现有差异——有些模型会把数字参数传成字符串。handle_function_call() 会自动根据 schema 做类型强制转换。
并行执行
如果模型一次返回多个独立的工具调用,Hermes 支持并行执行。并行执行的关键是每个工具 handler 要线程安全。
为什么这样设计?
| 原则 | 对应机制 | 为什么 |
|---|---|---|
| 扩展不改核心 | AST 自注册 | 新增工具加一行代码,零配置 |
| 平台差异化 | toolset 分层 | CLI 和 Telegram 看到不同工具 |
| 弹性可用 | 瞬断抑制 | 外部服务偶尔抖动不丢工具 |
| 按需加载 | Tool Search Bridge | 工具多时不爆上下文 |
| 类型安全 | 强制转换 | 不同模型输出的格式差异被抹平 |
总结
工具系统是 Agent 可靠性的基础。Hermes 用自注册让扩展零成本,用 toolset 让多平台共享代码,用 capabilities 检查让工具可用性可感知,用并行分发让执行不阻塞。
下一篇:工具调度系统:从注册到执行的完整生命周期 — Agent 如何在调用 LLM 之前发现哪 50 个工具可用、如何在运行时动态加载/卸载工具集