工具系统:从注册到调度

Hermes 如何让 50+ 个工具自动被发现、安全过滤、并行分发

通过 Hermes 探秘 Agent 工程 | 第 3 篇 · 查看全部

上一篇:System Prompt:身份、上下文与策略的三层架构


[系列文章导航]

#文章定位
1Agent Loop:Agent 的核心执行循环入口
2System Prompt:身份、上下文与策略的三层架构认知层
3工具系统:从注册到调度工具层
4工具调度系统:从注册到执行的完整生命周期调度层
5安全防护体系:当 Agent 拥有终端时如何防止「做出格」的事安全层
6沙箱与代码执行:让 Agent 安全跑代码的 RPC 架构执行层
7上下文压缩:让 Agent 在有限窗口里「记得住」记忆层
8记忆系统:跨会话持久化的工程实现记忆层
9技能系统:Agent 如何把经验变成可复用的程序化记忆记忆层
10Provider 抽象层:让 Hermes 同时驾驭 30+ 个 LLM 提供商模型层
11Gateway 网关:连接 20+ 平台的统一消息路由接入层
12多 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 定义了一组命名的工具集,比如 web(web_search, web_extract)、terminal(terminal, process)、file(read_file, write_file, patch, search_files)等。每个 toolset 可以引用其他 toolset(includes 字段),形成组合。

三层过滤

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

  1. 启用集(enabled_toolsets):用户配置启用了哪些 toolset,取并集
  2. 禁用集(disabled_toolsets):从并集中减去这些
  3. 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 个工具可用、如何在运行时动态加载/卸载工具集