
结论先说:Hermes 的 tools 架构是"注册表 + toolset 暴露 + agent 执行器"的三层结构,不是简单把 tools/ 目录里所有函数都塞给模型。
核心链路
-
工具发现
tools/registry.py 会扫描tools/*.py,只导入存在顶层registry.register(...)的模块。导入本身触发注册。 -
工具注册表
tools/registry.py 的ToolRegistry保存:
name、toolset、OpenAI tool schema、handler、check_fn、env 需求、是否 async、emoji、结果大小限制等。 -
schema 生成
model_tools.py 的get_tool_definitions()根据启用/禁用的 toolsets 生成最终给模型看的 tools。真正计算在 model_tools.py。
-
toolset 决定"模型看不看得到"
toolsets.py 的_HERMES_CORE_TOOLS是默认核心工具集合。
toolsets.py 的hermes-cli等平台 toolset 再组合这些工具。关键点:工具文件自注册只表示 registry 知道它;必须在 toolset 中解析出来,模型才看得到。
-
Agent 初始化加载 tools
agent/agent_init.py 调get_tool_definitions(),然后把工具名放进agent.valid_tool_names。 -
模型发 tool call 后执行
并发/顺序执行入口在 agent/tool_executor.py 和 agent/tool_executor.py。
单个工具调用会走 agent/agent_runtime_helpers.py。
普通 registry 工具最终走 model_tools.py 的
handle_function_call()和 tools/registry.py 的dispatch()。
当前实际注册内容
我本地导入后看到 71 个工具,按 toolset 大致是:
web:web_search,web_extractterminal:terminal,processfile:read_file,write_file,patch,search_filesbrowser: navigate/click/type/scroll/snapshot/console/vision/images 等browser-cdp:browser_cdp,browser_dialogvision:vision_analyzeimage_gen:image_generatevideo:video_analyzevideo_gen:video_generatetts:text_to_speechcode_execution:execute_codedelegation:delegate_tasktodo,memory,session_search,clarifycronjobmessaging:send_messagehomeassistantdiscord,discord_adminfeishu_doc,feishu_driveyuanbaokanbancomputer_usemoa:mixture_of_agentsskills:skills_list,skill_view,skill_managex_search- 插件带来的
spotify_*
注意:这个数量包含 repo 内插件发现后注册的工具,不等于纯 tools/ 目录文件数。
代表性写法
文件工具是最标准的例子:tools/file_tools.py。每个工具通常有:
python
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,
)
Web 工具展示了 async handler:tools/web_tools.py。
Terminal 工具展示了复杂 schema、后台进程、超时、PTY、通知等:tools/terminal_tool.py。
handler 必须返回 JSON 字符串。辅助函数在 tools/registry.py 和 tools/registry.py,也就是 tool_error() / tool_result()。
插件与 MCP
自定义或本地工具,按项目规则不应该优先改核心 tools/,而应该走插件。插件通过 hermes_cli/plugins.py 的 ctx.register_tool() 注册,底层仍然进入同一个 registry。
MCP 工具也是动态注册进 registry:tools/mcp_tool.py。MCP 会用 mcp-<server> 作为 toolset,并注册别名:tools/mcp_tool.py。
一个重要变化:MCP discovery 不再是 model_tools.py 的导入副作用,注释写在 model_tools.py。各入口要自己显式发现,避免 gateway 事件循环被慢 MCP 卡住。
工具启用方式
平台级启用配置在 hermes_cli/tools_config.py。
CLI 在 cli.py 读取当前平台启用 toolsets。
Gateway 在 gateway/run.py 按平台加载。
TUI 在 tui_gateway/server.py 也有自己的加载函数。
实际用户侧就是 hermes tools / setup 保存到 platform_toolsets,再由 _get_platform_tools() 转成 enabled toolsets。
几个关键坑
todo、memory、session_search、delegate_task虽然注册在 registry,但属于 agent-level tools,会被 agent loop 特殊处理;直接走普通handle_function_call()会被拦:model_tools.py。check_fn有 30 秒 TTL 缓存:tools/registry.py。改 env/config 后不是每次立即生效。discover_builtin_tools()只识别模块顶层registry.register(...)。如果把注册藏在函数里,自动发现不会导入它。- 注册同名工具默认会被拒绝,插件要有意覆盖必须
override=True:tools/registry.py。 - 大输出不是简单截断。执行器会把超大结果持久化到 sandbox 文件,再给模型预览:tools/tool_result_storage.py;整轮预算控制在 tools/tool_result_storage.py。
TOOL_TO_TOOLSET_MAP在 model_tools.py 是 discovery 后构建的兼容常量;动态 MCP/插件刷新后,优先信 registry 实时查询,不要过度依赖旧常量。
我对架构的评价
整体设计是对的:registry 解耦工具实现,toolset 控制暴露面,agent executor 负责权限、并发、结果预算、插件 hook。这比"每个入口手写工具列表"稳很多。
但也有明显技术债:browser_tool.py、terminal_tool.py、code_execution_tool.py 都偏巨型模块;toolset 静态配置也比较长,平台差异容易漂移。后续如果要继续扩展,最好坚持插件路线,并把核心大工具按 backend/schema/handler/guardrail 拆小,而不是继续往单文件里堆。
换一种方式讲:Hermes 的 tools 系统,本质上不是"工具目录",而是一个 给 LLM 建立可控行动空间的运行时协议层。
也就是说,它解决的不是"Python 怎么调用函数",而是这几个更深的问题:
- 这个 agent 此刻允许做什么?
- 模型看到的动作空间如何被压缩、过滤、动态修正?
- 模型发来的 tool call 如何变成可信、可审计、可中断、可并发、可限流的真实操作?
- 外部插件、MCP、平台专属能力如何接进来,但不污染核心?
- 工具结果如何返回给模型而不把上下文撑爆?
所以它的内核可以抽象成这条链:
text
工具实现
↓ 注册
Registry:系统知道有哪些能力
↓ 选择
Toolset:本轮/本平台允许暴露哪些能力
↓ Schema
model_tools:把能力翻译成模型 API 能理解的 JSON Schema
↓ 模型决策
LLM 输出 tool_call
↓ 执行器
Agent runtime / tool_executor:校验、审批、并发、拦截、调用
↓ 返回
结果预算、持久化、插件 hook、写回 conversation
真正的核心不是 tools/,而是 "能力从存在到可见,再到可执行"的分层控制。
第一层:工具存在,不等于模型能用
比如 tools/file_tools.py 注册了 read_file/write_file/patch/search_files。这一步只是告诉 registry:
系统里有这么几个函数,它们叫什么、schema 是什么、handler 是什么、属于哪个 toolset。
但是模型并不会自动看到它们。
模型是否看到,要看 toolsets.py 里的工具集合,以及平台配置最终启用了哪些 toolsets。
这就是 Hermes 的第一个重要设计:能力注册和能力暴露分离。
为什么要分离?
因为 Hermes 不是只有 CLI。它还有 gateway、TUI、ACP、cron、API server、Discord、Feishu、Home Assistant 等入口。不同入口的风险边界不同:
- CLI 可以用 terminal
- ACP 要更偏代码编辑
- API server 不适合 interactive clarify
- Discord 可以额外暴露 Discord 工具
- 某些工具只有 env/key 存在时才应该出现
所以 registry 回答的是:
系统会什么?
toolset 回答的是:
这个场景允许模型做什么?
这两个问题不能混在一起。
第二层:schema 不是文档,是模型的操作界面
model_tools.py 的 get_tool_definitions() 很关键。它不是简单返回 registry 里的 schema,而是在做"模型可见世界"的最终裁剪。
比如:
- enabled toolsets 解析成具体工具名
- disabled toolsets 再做减法
check_fn失败的工具被隐藏execute_code的 schema 会根据当前实际可用工具重建- browser schema 会在 web 工具不可用时移除误导描述
- Discord schema 会根据 bot 权限动态变化
- 最后还要做 schema sanitizer,兼容不同模型后端
这说明一件事:tool schema 是行为约束,不只是说明书。
对 LLM 来说,它看到的 schema 就是它能采取行动的边界。如果 schema 里说"你可以调用 web_search",但实际 web_search 不可用,模型就会被误导。因此 Hermes 做了很多动态修正,目的就是让模型的"认知地图"和真实运行时尽量一致。
这是深层内核之一:
工具系统不是给人看的 API,而是给模型看的可行动作空间。
第三层:handler 不是直接执行,而是进入管控管线
模型发出 tool call 后,不是直接 handler(args)。
先进入 agent/tool_executor.py 或顺序路径,再到 agent/agent_runtime_helpers.py。
这里开始有真正的 agent runtime 语义:
- 解析模型参数
- 修复错误工具名
- 检查 interruption
- 触发插件
pre_tool_call - 做 guardrail
- 文件修改前 checkpoint
- destructive terminal command 前 checkpoint
- 多个 tool call 可以并发执行
- 工具执行中可以回调 UI 显示状态
- 执行结果会被预算控制
- 最后 append 成 tool message 回 conversation
也就是说,Hermes 的 tool call 不是普通函数调用,而是一个 事务化的 agent action。
这个 action 需要考虑:
- 能不能执行
- 怎么展示给用户
- 怎么被中断
- 怎么被审计
- 失败怎么反馈
- 输出太大怎么办
- 多工具并发时顺序怎么保持
- 插件能不能阻止或改写结果
这就是为什么代码看起来厚。它厚不是因为"调用函数复杂",而是因为它在把 LLM 的不稳定输出包进一个相对可靠的执行环境。
第四层:有些工具是假 registry 工具,真 agent 工具
这个点很容易误解。
model_tools.py 里有:
python
_AGENT_LOOP_TOOLS = {"todo", "memory", "session_search", "delegate_task"}
这些工具虽然也注册 schema,让模型能看见,但执行时不能普通 dispatch。因为它们需要 agent 内部状态:
todo需要当前 agent 的 todo storememory需要 memory store / memory managersession_search需要当前 session DBdelegate_task需要当前 agent 的 delegation runtime
所以它们是"对模型暴露为工具",但真实执行属于 agent loop。
这揭示了另一个内核:
工具系统不是纯函数系统,它承载 agent 状态。
普通工具像 web_search,输入 query 输出 JSON。
agent-level 工具像 delegate_task,会影响运行时结构,甚至生成子 agent。
所以 Hermes 的 tools 里混合了两类能力:
text
纯外部能力:web/file/browser/terminal/tts
运行时能力:memory/todo/delegation/session_search
前者是"做事"。
后者是"改变 agent 自己如何做事"。
第五层:结果不是直接塞回上下文
LLM 工具系统最容易炸的地方,是工具返回太多内容。
Hermes 在 tools/tool_result_storage.py 做了一个很重要的设计:大结果持久化。
逻辑是:
text
工具返回内容
↓
如果小,直接进上下文
↓
如果大,写入 sandbox 文件
↓
上下文里只放 preview + 文件路径
↓
模型需要细节时再 read_file 分页读取
这其实是给 LLM 做"外部记忆分页"。否则一次 search_files、terminal、web_extract 就可能把上下文塞爆。
所以 tool result storage 不是优化项,而是 Hermes 能持续长任务的基础设施。
第六层:插件/MCP 是同一套能力模型的外延
插件的 ctx.register_tool() 最终还是进 registry:hermes_cli/plugins.py。
MCP 工具也是转成 registry entry:tools/mcp_tool.py。
这说明 Hermes 没有给插件/MCP 另起一套调用机制,而是把它们全部归一到:
text
name + toolset + schema + handler + check_fn
这很好,因为 agent runtime 不需要关心工具来自哪里。
坏处是:registry 成了一个非常关键的全局能力总线。它必须处理:
- 名字冲突
- toolset alias
- MCP 动态刷新
- 插件覆盖内置工具
- check_fn 缓存
- schema 兼容性
所以 tools/registry.py 的 ToolRegistry 不是普通 map,而是 Hermes 的"能力注册中心"。
如果用一句话概括
Hermes tools 的深层内核是:
把 LLM 的文本决策,约束成一个动态、可配置、可审计、可中断、可扩展的行动系统。
不是"模型调用 Python 函数"。
而是"模型在一个被 Hermes 精心裁剪过的行动空间里选择动作,然后 Hermes runtime 负责把动作安全地落地"。
你读代码时可以这样分层看
不要从 browser_tool.py 这种大文件开始读,会被淹没。建议按这个顺序:
-
看注册协议:
tools/registry.py -
看暴露策略:
toolsets.py -
看 schema 生成:
model_tools.py -
看 tool call 执行:
agent/agent_runtime_helpers.py -
看并发和结果处理:
agent/tool_executor.py -
最后才看具体工具,比如:
tools/file_tools.py
tools/terminal_tool.py
你可以把它想成一个操作系统的小内核:
text
registry = 设备表
toolsets = 权限/能力配置
schema = 系统调用 ABI
tool call = 用户态请求
executor = 内核调度器
handler = 驱动实现
result store = I/O 缓冲和分页
plugins/MCP = 外接设备
这个类比比"工具函数列表"更接近 Hermes tools 的真实结构。