工具系统:从注册到调度
Hermes 如何让 50+ 个工具自动被发现、安全过滤、并行分发
系列:通过 Hermes 探秘 Agent 工程 | 第 2 篇 上一篇:Agent Loop:Agent 的核心执行循环
工具系统解决了什么问题?
一个 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 定义了一组命名的工具集:
TOOLSETS = {
"web": {
"description": "Web research and content extraction tools",
"tools": ["web_search", "web_extract"],
"includes": []
},
"terminal": {
"description": "Terminal/command execution and process management tools",
"tools": ["terminal", "process"],
"includes": []
},
"file": {
"tools": ["read_file", "write_file", "patch", "search_files"],
},
"browser": {
"tools": ["browser_navigate", "browser_snapshot", ...],
},
...
}
每个 toolset 可以引用其他 toolset(includes 字段),形成组合。比如 CLI 默认可能启用 terminal + file + web + browser,而子代理可能只给 file + web。
三层过滤
最终给模型的工具列表,经过三层过滤:
- 启用集(enabled_toolsets):用户配置启用了哪些 toolset,取并集
- 禁用集(disabled_toolsets):从并集中减去这些
- capabilities 检查:对每个工具调用
check_fn(),去掉当前环境不支持的
机制三:capabilities 检查与缓存
很多工具的可用性取决于外部环境。比如 terminal 工具在非 local 后端(比如 Docker 远程)可能不可用,browser 需要 Playwright 安装,ha_list_entities 需要 HASS_TOKEN 环境变量。
每个工具可以定义自己的 check_fn。但这个检查有个问题:外部环境(比如 Docker 守护进程的 socket 连接)可能瞬断,每次调用 LLM 前都检查一遍会增加延迟。
Hermes 用了两层优化:
TTL 缓存
check_fn 的结果缓存 30 秒,同一个会话里连续调用不重复检查。
瞬断抑制
更精妙的是"最近一次成功抑制“机制:如果某个 check_fn 最近成功过(证明功能确实可用),那么接下来 60 秒内的失败会被当作"瞬断"忽略掉——工具仍然可用,不报错。
为什么需要这个?想象一下:Docker daemon 的 Unix socket 偶尔超时(容器负载高时很常见),如果一刀切地"失败就禁用”,会导致 CLI 工具在你的会话中被随机移除。有了瞬断抑制,单次超时不影响使用,只有持续失败才会真正禁用。
分发: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 实现有差异——有些模型会把数字参数传成字符串("42" 而不是 42)。handle_function_call() 会自动根据 schema 做类型强制转换,确保 handler 收到正确类型的参数。
Tool Search Bridge
这是个有意思的工具:tool_search / tool_describe / tool_call,合称"工具搜索桥"。它让模型可以搜索当前会话可用的工具目录,而不需要在 system prompt 里塞进所有工具的定义。
启用场景:当工具数量太大(50+)会消耗大量上下文 token 时,模型可以先用 tool_search 找到需要的工具,再用 tool_describe 查看参数细节,最后用 tool_call 调用。相当于给工具系统加了一层"按需索引"。
并行执行
如果模型一次返回多个独立的工具调用,Hermes 支持并行执行。
模型返回:[tool_A(args_A), tool_B(args_B), tool_C(args_C)]
↓
并行分发到 ThreadPoolExecutor / asyncio
↓
等待所有结果
↓
合并到历史对话
并行执行的关键是每个工具 handler 要线程安全。大部分工具本质是无状态的(接收参数 → 执行 → 返回结果),天然支持并行;但有副作用的工具(比如写文件同一路径)需要开发者自己保证安全。
为什么这样设计?
回顾这三个机制,你会发现 Hermes 工具系统的设计哲学:
| 原则 | 对应机制 | 为什么 |
|---|---|---|
| 扩展不改核心 | AST 自注册 | 新增工具加一行代码,零配置 |
| 平台差异化 | toolset 分层 | CLI 和 Telegram 看到不同工具 |
| 弹性可用 | 瞬断抑制 | 外部服务偶尔抖动不丢工具 |
| 按需加载 | Tool Search Bridge | 工具多时不爆上下文 |
| 类型安全 | 强制转换 | 不同模型输出的格式差异被抹平 |
工程启示
工具系统的好坏不取决于工具数量,而在于管理质量。
一些值得借鉴的设计决策:
**让工具自己说"我需要什么"
**——通过check_fn` 声明依赖,而不是让外部代码猜测工具是否可用缓存有退路——30 秒 TTL + 60 秒瞬断抑制,既避免重复检查,又不会把抖动当死机
渐进式暴露——不是所有工具平铺给模型,而是通过 toolset + bridge 按需暴露,控制上下文开销
总结
工具系统是 Agent 可靠性的基础。Hermes 用自注册让扩展零成本,用 toolset 让多平台共享代码,用 capabilities 检查让工具可用性可感知,用并行分发让执行不阻塞。
下一篇,我们将深入 System Prompt 的组装——Hermes 是如何把工具定义、记忆快照、用户规则拼成一个既紧凑又完整的系统提示的。