工具系统:从注册到调度

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工具归属的分组,用于批量启用/禁用
schemaOpenAI 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

三层过滤

最终给模型的工具列表,经过三层过滤:

  1. 启用集(enabled_toolsets):用户配置启用了哪些 toolset,取并集
  2. 禁用集(disabled_toolsets):从并集中减去这些
  3. 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工具多时不爆上下文
类型安全强制转换不同模型输出的格式差异被抹平

工程启示

工具系统的好坏不取决于工具数量,而在于管理质量

一些值得借鉴的设计决策:

  1. **让工具自己说"我需要什么"**——通过 check_fn` 声明依赖,而不是让外部代码猜测工具是否可用

  2. 缓存有退路——30 秒 TTL + 60 秒瞬断抑制,既避免重复检查,又不会把抖动当死机

  3. 渐进式暴露——不是所有工具平铺给模型,而是通过 toolset + bridge 按需暴露,控制上下文开销

总结

工具系统是 Agent 可靠性的基础。Hermes 用自注册让扩展零成本,用 toolset 让多平台共享代码,用 capabilities 检查让工具可用性可感知,用并行分发让执行不阻塞。

下一篇,我们将深入 System Prompt 的组装——Hermes 是如何把工具定义、记忆快照、用户规则拼成一个既紧凑又完整的系统提示的。