Hermes 源码视角:工具调用实现拆解¶
文档目的¶
这份文档从本地 Hermes 源码出发,拆解“工具调用是如何在框架内部被发现、暴露、验证、执行并回填给 LLM”的实现链路。
它是上一份概念文档《LLM 工具调用确认机制整理》的源码落地版,重点回答:
- Hermes 里工具 schema 从哪里来
- toolset 是如何解析的
run_conversation()主循环如何工作- 模型返回
tool_calls后,Hermes 如何验证并执行 - 工具结果如何重新放回消息历史
- 哪些地方做了健壮性保护(非法工具、非法 JSON、孤儿 tool result、并发执行、压缩等)
0. 本次检查基线¶
基于本地实际安装检查得到:
- Hermes 版本:
Hermes Agent v0.10.0 (2026.4.16) - 项目路径:
/root/.hermes/hermes-agent
本次重点阅读的文件:
/root/.hermes/hermes-agent/run_agent.py/root/.hermes/hermes-agent/model_tools.py/root/.hermes/hermes-agent/tools/registry.py/root/.hermes/hermes-agent/toolsets.py/root/.hermes/hermes-agent/tools/file_tools.py/root/.hermes/hermes-agent/tools/terminal_tool.py
说明:下面提到的行号基于当前本地版本,后续升级后可能漂移,但结构关系通常仍成立。
1. 先看整体结论¶
Hermes 的工具调用链路可以概括成 8 步:
- 工具模块通过
registry.register(...)在导入时自注册 model_tools.py启动时触发工具发现(builtin / MCP / plugin)AIAgent.__init__()调用get_tool_definitions(),按 toolset 解析出当前会话可见的工具 schemarun_conversation()进入主循环,构造messages + tools发给模型- 模型返回
assistant_message.tool_calls - Hermes 先做工具名与参数层面的校验,再标准化为内部 assistant message
_execute_tool_calls()执行工具,必要时并发执行,并把结果作为role=tool追加进消息历史- 主循环继续下一轮;如果没有 tool call,则走最终回复分支并返回
一句话版:
Hermes 把“工具注册与筛选”“LLM 工具决策”“工具调度执行”“结果重新注入上下文”拆成了清晰的多层结构,而不是把所有逻辑塞在一个函数里。
2. 源码结构里,哪几个文件最关键¶
2.1 run_agent.py¶
这是主战场。
它负责:
AIAgent初始化- 加载当前会话可用工具
- 构建系统提示与 API 消息
- 主循环
run_conversation() - 解析模型响应
- 校验和执行 tool calls
- 把 tool result 加回消息历史
- 最终返回结果
2.2 model_tools.py¶
这是“工具编排层”。
它不是每个工具的具体实现,而是:
- 触发工具发现
- 解析 toolset
- 返回 OpenAI 格式的工具 schema
- 做统一的函数调度入口
handle_function_call()
可以把它理解成:
run_agent 和底层工具实现之间的一层薄中间件。
2.3 tools/registry.py¶
这是工具注册中心。
它负责:
- 保存每个工具的 schema / handler / check_fn / toolset 等元信息
- 返回工具定义
- 按名字 dispatch 到具体 handler
这是 Hermes 的工具系统中枢。
2.4 toolsets.py¶
这是“工具集合定义层”。
它定义:
webterminalfilebrowserhermes-clihermes-discordhermes-gateway- 以及各类组合 toolset
也就是说,模型并不是默认看到“所有工具”,而是先经过 toolset 解析再暴露。
3. 工具是怎么“出现”在系统里的¶
Hermes 使用的是“自注册工具”模式,而不是在一个中央文件里手工维护超长工具清单。
3.1 discover_builtin_tools():扫描并导入工具模块¶
在 tools/registry.py:56-73:
discover_builtin_tools()会扫描tools/目录- 只挑出那些包含顶层
registry.register(...)调用的模块 - 然后
importlib.import_module(mod_name)导入它们
关键点:
导入工具模块本身,就是注册动作发生的时刻。
也就是:
- 不是“先 import,再手工 add 到列表”
- 而是“import 时模块自己调用 registry.register(...)”
3.2 例子:文件工具的注册¶
在 tools/file_tools.py:941-944,可以看到:
read_filewrite_filepatchsearch_files
都是通过 registry.register(...) 注册进去的。
3.3 例子:terminal 工具的注册¶
在 tools/terminal_tool.py:2068-2076:
- 工具名:
terminal - toolset:
terminal - schema:
TERMINAL_SCHEMA - handler:
_handle_terminal - 可用性检查:
check_terminal_requirements
这说明每个工具在注册时就已经把以下信息交齐了:
- 名字
- 属于哪个 toolset
- 对外暴露的 schema
- 实际执行函数
- 当前是否可用的检查逻辑
- 可选的 emoji、结果大小限制等元数据
4. 注册中心 ToolRegistry 到底存了什么¶
在 tools/registry.py 里,核心数据结构是 ToolEntry 和 ToolRegistry。
4.1 ToolEntry¶
tools/registry.py:76-98
一个工具注册后,会被封装成 ToolEntry,里面至少包含:
nametoolsetschemahandlercheck_fnrequires_envis_asyncdescriptionemojimax_result_size_chars
这说明 Hermes 对工具不是只存一个 callable,而是存“完整元信息对象”。
4.2 ToolRegistry.register()¶
tools/registry.py:176-227
这个函数负责把工具写进注册表。
几个重要点:
-
会做冲突控制
- 不允许普通工具轻易覆盖已有工具
- 只对某些 MCP-to-MCP 的刷新场景放宽 -
会记录 toolset 对应的
check_fn
- 这为后续 toolset 可用性判断提供基础 -
注册是线程安全的
- Registry 内部有RLock
这说明 Hermes 的工具系统是考虑过:
- 插件扩展
- MCP 动态刷新
- 多线程读取
- 名称冲突
而不是只服务于一个静态单进程 CLI。
5. toolset 是怎么解析成真实工具列表的¶
Hermes 不会简单把注册表里所有工具全发给模型,而是通过 toolsets.py 先做一轮筛选。
5.1 静态 toolset 定义¶
在 toolsets.py:68 开始,TOOLSETS = {...} 定义了很多 toolset。
例如:
webterminalfilebrowserskillsmemorydelegationhermes-clihermes-gateway
其中还有 _HERMES_CORE_TOOLS(toolsets.py:31-63),供多个平台 toolset 复用。
5.2 resolve_toolset():递归展开¶
在 toolsets.py:465-515:
resolve_toolset(name) 会:
- 找到该 toolset 的直接工具
- 递归处理它的
includes - 去重后返回最终工具名列表
这就支持了:
- 单层 toolset
- 组合 toolset
- 平台级 toolset
- “all / *” 这种全量快捷别名
5.3 validate_toolset():先验合法性检查¶
在 toolsets.py:611-628:
Hermes 会判断:
- 名字是否存在于静态 TOOLSETS
- 是否是插件注册的 toolset
- 是否是 registry alias
- 是否是
all/*
这就是为什么 enabled_toolsets 传进来以后,不是盲目相信,而是先验证。
6. 当前会话到底能看到哪些工具:get_tool_definitions()¶
这是工具从“注册状态”变成“实际暴露给模型的 schema”的关键一步。
位置:model_tools.py:202-346
6.1 解析 enabled / disabled toolsets¶
它会先根据:
enabled_toolsetsdisabled_toolsets
决定要包含哪些工具名。
逻辑大概是:
- 如果显式启用了某些 toolset,就只解析这些
- 如果是禁用模式,就从全部 toolset 里减掉禁用项
- 如果都没传,则按全部可见 toolset 处理
6.2 调用 registry 取 schema¶
随后在 model_tools.py:269-270:
- 调用
registry.get_definitions(tools_to_include, quiet=quiet_mode)
这一步已经不是静态工具名集合,而是真正生成 OpenAI 工具 schema 列表。
6.3 registry.get_definitions() 里的可用性过滤¶
在 tools/registry.py:258-286:
它会遍历请求中的工具名,并对每个工具做:
- 是否已注册
check_fn()是否通过
只有通过的工具才会进入最终结果。
也就是说:
toolset 解析解决“理论上应该有哪些工具”,
check_fn过滤解决“此刻实际上哪些工具可用”。
6.4 动态 schema 修正¶
model_tools.py:278-345 还做了几件很工程化的事:
-
动态重建
execute_codeschema
- 只把当前会话实际可用的 sandbox 工具写进去
- 避免模型看到并不存在的工具名 -
动态调整某些工具 schema
- 例如 Discord 工具可根据能力动态收缩 schema -
清理跨工具描述误导
- 例如 browser schema 里如果提到web_search,但当前没启用,就删掉那段描述
这说明 Hermes 不只是“筛选工具”,还在尽量避免 schema 层的幻觉诱导。
6.5 结果写回 AIAgent¶
在 run_agent.py:1302-1312:
AIAgent.__init__() 调用:
self.tools = get_tool_definitions(...)self.valid_tool_names = {...}
这两者分别承担:
self.tools:发给模型的正式 schema 列表self.valid_tool_names:后续对模型返回 tool call 做合法性验证
这是一个非常重要的分层。
7. run_conversation():主循环从哪里开始¶
主函数位置:run_agent.py:8649
它是 Hermes 的核心执行循环。
7.1 前置准备¶
在进入主循环前,run_conversation() 做了大量准备工作,包括:
- 安装安全 stdio wrapper
- 绑定 session context
- 清理上次 turn 的 fallback 状态
- 清洗用户输入中的 surrogate / memory-context 泄漏
- 生成
effective_task_id - 重置各类 retry counter
- 初始化
IterationBudget - 载入 conversation history
- 恢复 todo store
- 构建或复用 system prompt
- 预检查并可能触发 context compression
- 通过 plugin / memory manager 预取上下文
这说明 Hermes 的主循环并不是“用户消息 -> 直接调模型”,而是在进入推理前已经做了一整套上下文治理。
7.2 主循环条件¶
在 run_agent.py:9018:
while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) or self._budget_grace_call:
说明 Hermes 同时受两类约束:
api_call_countIterationBudget
并且留了一个 grace call 机制。
这意味着它不是无限循环执行工具,而是严格受预算控制。
8. 每轮调用模型前,Hermes 做了什么¶
在主循环内部(大约 run_agent.py:9129-9258),Hermes 会构建真正发给 API 的 api_messages。
8.1 从内部 messages 拷贝出 api_messages¶
Hermes 内部保存的是 richer message 结构,里面可能包含:
reasoningfinish_reason_thinking_prefill- Codex / provider-specific 字段
但发给 API 的时候,会做一轮清理和适配。
8.2 把 ephemeral context 注入当前 user message¶
包括:
- memory manager 预取内容
- plugin pre_llm_call 提供的上下文
这些内容会拼接进当前 user message,而不是改 system prompt。
这背后的设计意图是:
尽量保持 system prompt 稳定,以利于 prompt cache 命中。
8.3 拼接 system prompt¶
最后才把:
- cached system prompt
- ephemeral system prompt
拼成实际的 role=system 消息。
8.4 预发送校验:_sanitize_api_messages()¶
在 run_agent.py:4217-4293,每次 API 调用前都会跑这个安全网。
它做两件关键事:
-
删除 orphaned tool result
- 即有role=tool,但找不到对应 assistant tool call 的结果消息 -
给缺失结果的 tool call 自动补 stub
- 内容是:[Result unavailable — see context summary above]
这一步非常关键,因为如果消息序列损坏,很多模型 API 会直接拒绝请求。
所以 Hermes 在发送前会主动修补工具调用配对关系。
9. 模型返回后,assistant message 是怎么标准化的¶
Hermes 会把不同 provider / API 形态的返回统一归一化。
核心函数:run_agent.py:7151-7293 的 _build_assistant_message()
9.1 统一抽取 reasoning¶
它会:
- 读取结构化 reasoning 字段
- 如果没有,再尝试从
<think>...</think>中提取 - 清洗 surrogate
- 把 reasoning 单独放到
msg["reasoning"]
9.2 清理内容中的 think block¶
它还会把 assistant 的可见 content 里的 <think> 标签剥离掉,避免:
- reasoning 泄露给最终用户
- 下轮上下文被无谓膨胀
- session title 被污染
9.3 标准化 tool_calls¶
如果模型返回了 assistant_message.tool_calls:
- 会把每个 tool call 统一转成 dict
- 确保有稳定的
id/call_id - 保留
function.name - 保留
function.arguments - 保留某些 provider-specific 字段,例如
extra_content
这说明 Hermes 内部消息不是直接照抄 SDK 原始对象,而是做了自己的归一化层。
10. 模型返回 tool call 后,Hermes 先做哪些校验¶
这一段在 run_agent.py:11091-11321 附近,是最贴近“LLM 与工具调用确认”的核心实现。
10.1 先检查是否存在 tool calls¶
if assistant_message.tool_calls:
进入这个分支,就说明当前不是最终文本回复,而是工具调用轮次。
10.2 工具名校验与自动修复¶
逻辑:run_agent.py:11100-11149
Hermes 会:
- 检查 tool name 是否在
self.valid_tool_names - 如果不在,先尝试
_repair_tool_call()自动修复 - 仍无效则:
- 记录重试次数
- 把错误以 tool result 的形式回填给模型
- 让模型下一轮自我修正
这点很关键:
Hermes 对非法工具名不是立刻崩掉,而是尽量修复;修不了就把“未知工具”当成结构化错误反馈给模型。
10.3 参数 JSON 校验¶
逻辑:run_agent.py:11151-11240
Hermes 会检查:
arguments是否能被json.loads- 空字符串会被当作
{} - 非字符串、dict/list 等也会标准化
如果 JSON 非法:
- 会区分“普通格式错误”与“输出被截断导致的半截 JSON”
- 普通情况先重试 API
- 超过阈值后,构造 tool error result 回给模型恢复
- 截断情况则直接作为 partial 失败处理,避免执行半截参数
这一步体现出 Hermes 对“模型输出脏数据”的容错做得很细。
10.4 post-call guardrails¶
接着在 run_agent.py:11243-11249:
_cap_delegate_task_calls(...)_deduplicate_tool_calls(...)
说明 Hermes 在真正执行前,还会对某些高风险/高成本工具做额外收敛处理。
11. assistant message 何时被写回消息历史¶
在工具执行前,Hermes 会先:
assistant_msg = self._build_assistant_message(...)messages.append(assistant_msg)
见 run_agent.py:11251 和 11306
这一步很重要,因为它保证消息顺序是:
- assistant 发出 tool_calls
- tool 返回各自结果
而不是只有 tool result 没有对应 assistant call。
这和 OpenAI / Anthropic 这类工具调用协议的消息形状是对齐的。
12. 工具执行入口:_execute_tool_calls()¶
位置:run_agent.py:7652-7673
这个函数负责根据当前 tool call 批次决定:
- 走顺序执行
- 还是并发执行
逻辑是:
- 如果
_should_parallelize_tool_batch(tool_calls)返回 False,就走顺序 - 否则走并发
13. 并发执行判定是怎么做的¶
位置:run_agent.py:289-330
_should_parallelize_tool_batch(tool_calls) 不是简单地“多个工具就并发”,而是比较保守:
13.1 直接禁止并发的工具¶
_NEVER_PARALLEL_TOOLS 当前至少包含:
clarify
这类需要用户交互或有强状态依赖的工具,绝不并发。
13.2 明确允许并发的只读工具¶
_PARALLEL_SAFE_TOOLS 包括:
read_filesearch_filessession_searchskill_viewskills_listvision_analyzeweb_searchweb_extract- 以及若干只读工具
13.3 路径作用域工具要做路径冲突判断¶
_PATH_SCOPED_TOOLS 包括:
read_filewrite_filepatch
如果两个调用目标路径重叠,就不允许并发。
也就是说,Hermes 的并发策略是:
只有看起来彼此独立、不会互相污染状态的工具调用,才走并发。
这很实用,也很稳。
14. 真正执行单个工具时走哪条路径¶
核心函数:run_agent.py:7694-7771 的 _invoke_tool()
这里是非常清晰的分层点。
14.1 Agent-level tools:不走 registry.dispatch¶
这些工具需要依赖 agent 自身状态,所以直接在 _invoke_tool() 里处理:
todosession_searchmemoryclarifydelegate_task- 以及 memory manager 暴露的外部记忆工具
原因很合理:
这些工具依赖:
- todo store
- session db
- memory store
- clarify callback
- parent agent
不是一个纯函数 handler 能搞定的。
14.2 普通工具:走 handle_function_call()¶
如果不是 agent-level tool,就进入:
handle_function_call(...)
这又把控制权交回 model_tools.py。
也就是说:
- agent 有状态的工具,在 agent 层拦截
- 通用工具,走 registry-dispatch 体系
这个边界划分是合理的。
15. handle_function_call() 做了什么¶
位置:model_tools.py:452-583
15.1 参数类型矫正¶
在进入主逻辑前,coerce_tool_args() 会按 schema 尝试把:
"42" -> 42"true" -> True
这类 LLM 常见的字符串化参数矫正回来。
这一步很重要,因为模型即使 schema 看懂了,也常把数字和布尔输出成字符串。
15.2 Agent-loop tools 保护¶
如果有些工具本应在 agent loop 中处理,却意外落到这里,会返回:
{"error": "xxx must be handled by the agent loop"}
避免错误路径被静默执行。
15.3 plugin hook¶
执行前后会触发:
pre_tool_callpost_tool_calltransform_tool_result
这说明 Hermes 的工具执行链路为插件系统预留了扩展点。
15.4 最终交给 registry.dispatch()¶
execute_code会带上当前可用工具集合- 其他工具正常 dispatch
出错时统一返回 JSON error string。
16. registry.dispatch() 是最后一跳分发¶
位置:tools/registry.py:292-309
逻辑非常直接:
- 通过名字找到
ToolEntry - 如果不存在,返回
Unknown tool - 如果是 async handler,则通过
_run_async()桥接 - 否则直接调用
entry.handler(args, **kwargs) - 捕获异常并统一返回 JSON error
这就是最底层的工具执行门。
16.1 为什么 async bridging 要单独做¶
model_tools.py:81-131 里定义了 _run_async(),并处理:
- 主线程持久 event loop
- worker thread 独立 loop
- 当前线程已在 async 环境中的回退方式
目的是避免:
Event loop is closed- cached async client 绑定死 loop
- 多线程并发工具执行时的 loop 混乱
这部分很工程化,说明 Hermes 在 async tool 上踩过坑,也已经固化了解法。
17. 并发工具执行后,结果如何回填¶
看 run_agent.py:7798-8100 的 _execute_tool_calls_concurrent()。
17.1 worker 线程实际调用 _invoke_tool()¶
每个 worker 会:
- 记录线程 id
- 绑定 activity callback
- 执行
_invoke_tool() - 记录 duration
- 判断是否 error
- 把结果放回
results[index]
17.2 结果按原始顺序回填¶
虽然执行是并发的,但 Hermes 最后仍按原始 tool_call 顺序,把结果追加进 messages:
tool_msg = {
"role": "tool",
"content": function_result,
"tool_call_id": tc.id,
}
messages.append(tool_msg)
这点非常重要,因为很多 API 要求:
- tool results 的顺序和 assistant tool_calls 一一对应
- 至少要维持 call_id 配对关系
17.3 工具结果还有后处理¶
回填前后,Hermes 还会做:
maybe_persist_tool_result(...)- subdirectory hints 注入
- per-turn aggregate budget enforcement
- pending steer 注入
说明 tool result 不只是原样字符串回填,而是进入了框架自己的结果治理流程。
18. 顺序执行路径和并发路径的区别¶
顺序执行在 run_agent.py:8101+。
核心差异:
- 更适合交互式或状态敏感工具
- 每个工具执行前后可插入更多逐步控制逻辑
- 同样会把结果作为
role=tool消息写回messages
不管并发还是顺序,核心协议都不变:
- assistant message 先入历史
- 每个 tool result 都带
tool_call_id - 结果进入下一轮模型上下文
这保证了协议一致性。
19. 工具执行完成后,主循环如何继续¶
在 run_agent.py:11321-11385:
_execute_tool_calls(...)执行完后- 工具结果已经写回
messages - Hermes 会保存增量 session log
- 必要时触发 context compression
- 然后
continue
这意味着:
同一个
run_conversation()会继续下一轮 API 调用,让模型读取刚刚的 tool results,再决定后续动作。
这就是 Agent 闭环真正落地的地方。
20. 如果没有 tool calls,会发生什么¶
在 run_agent.py:11387 开始进入最终回复分支。
即:
final_response = assistant_message.content or ""
然后 Hermes 会处理很多边缘情况:
- 只有 think block 没有可见内容
- 部分流式结果恢复
- 上一轮内容 + housekeeping tool 的 fallback
- post-tool empty response nudge
- thinking-only continuation
- 空回复重试
- fallback provider 切换
这些都说明:
Hermes 不是“没 tool_calls 就简单 return”,而是会尽量把最终回答做稳。
不过从工具调用机制视角看,最核心的判断仍然是:
- 有 tool_calls -> 执行工具后继续循环
- 无 tool_calls -> 进入最终回答路径
21. 从源码角度看,Hermes 的“确认工具调用”发生在哪几层¶
把前面内容压缩一下,Hermes 里真正的“确认”大概分成 5 层:
第 1 层:注册确认¶
发生在:
discover_builtin_tools()registry.register()
确认内容:
- 这个工具是否真的存在于系统中
- 它的 schema / handler / check_fn 是否完整
第 2 层:可见性确认¶
发生在:
resolve_toolset()get_tool_definitions()registry.get_definitions()
确认内容:
- 当前会话到底允许模型看到哪些工具
- 哪些工具因
check_fn失败而被隐藏
第 3 层:模型输出合法性确认¶
发生在:
valid_tool_names校验- tool name auto repair
- JSON 参数校验
- delegate_task 限流 / 去重
确认内容:
- 模型返回的 tool_call 是否可执行
第 4 层:调度执行确认¶
发生在:
_invoke_tool()handle_function_call()registry.dispatch()
确认内容:
- 调哪个 handler
- 是否需要 async bridging
- 异常如何标准化
第 5 层:结果配对确认¶
发生在:
- tool result append
_sanitize_api_messages()
确认内容:
- 每个
tool_call_id是否有对应 result - 是否存在 orphaned tool result
- API 消息序列是否仍然合法
这 5 层一起构成了 Hermes 的工具调用闭环。
22. 一张 Hermes 源码视角的链路图¶
工具模块 (tools/*.py)
-> registry.register(...)
-> ToolRegistry
model_tools.py
-> discover_builtin_tools()
-> resolve_toolset()
-> registry.get_definitions()
-> get_tool_definitions()
AIAgent.__init__()
-> self.tools
-> self.valid_tool_names
run_conversation()
-> 构造 api_messages + tools
-> 调模型
-> assistant_message
if assistant_message.tool_calls:
-> 校验 tool name / JSON args
-> _build_assistant_message()
-> messages.append(assistant tool_calls)
-> _execute_tool_calls()
-> _invoke_tool()
-> agent-level tools / handle_function_call()
-> registry.dispatch()
-> messages.append(role=tool, tool_call_id=...)
-> continue 下一轮
else:
-> final_response
-> return
23. 一个很值得注意的设计取舍¶
23.1 Agent-level tools 与 registry tools 分层¶
Hermes 没有把所有工具都丢给统一 registry handler。
像:
todomemorysession_searchclarifydelegate_task
这些依赖 agent 实例状态的工具,被留在 _invoke_tool() 里处理。
这是一个很合理的边界划分。
23.2 schema 过滤不是一次性的¶
Hermes 不只是“启动时筛工具”,还会在 get_tool_definitions() 阶段对某些 schema 动态重写。
这说明它意识到:
错误的 schema 暴露本身就会诱导模型产生错误工具调用。
23.3 消息配对修复是必要的¶
_sanitize_api_messages() 这类函数看似不起眼,但实际上是多轮工具调用系统稳定运行的关键。
因为真实环境里会出现:
- session 历史加载不完整
- 压缩丢掉局部消息
- 插件插消息
- 中断导致 tool result 缺失
如果不修补,模型 API 很容易直接报错。
24. 如果你要读 Hermes 的工具调用源码,推荐阅读顺序¶
最省脑的顺序建议如下:
第一轮:抓主链¶
run_agent.py
-AIAgent.__init__()看self.tools/self.valid_tool_names
-run_conversation()看主循环
-_build_assistant_message()
-_execute_tool_calls()
-_invoke_tool()
第二轮:看工具编排层¶
model_tools.py
-get_tool_definitions()
-handle_function_call()
-_run_async()
第三轮:看注册中心¶
tools/registry.py
-discover_builtin_tools()
-register()
-get_definitions()
-dispatch()
第四轮:看 toolset¶
toolsets.py
-_HERMES_CORE_TOOLS
-TOOLSETS
-resolve_toolset()
-validate_toolset()
第五轮:挑具体工具看样板¶
- 工具实现文件
-tools/file_tools.py
-tools/terminal_tool.py
这样能最快建立 Hermes 的整体心智模型。
25. 最终总结¶
从源码来看,Hermes 的工具调用实现有几个鲜明特点:
-
自注册工具架构
- 工具模块导入即注册 -
toolset 驱动的工具暴露
- 不是所有工具都默认暴露给模型 -
registry 统一管理 schema / handler / check_fn
- 把工具元数据集中化 -
run_conversation()内部有清晰的工具循环
- 模型返回 tool_calls -> 执行 -> 回填 -> 继续 -
对“模型不可靠输出”做了很多守护
- 工具名自动修复
- JSON 参数校验
- orphaned tool result 修补
- post-tool 空回复恢复
- context compression 与 fallback
一句话总结:
Hermes 不是“让模型直接调工具”,而是通过注册中心、toolset 解析、主循环校验、调度执行和消息修补,把工具调用包装成一个强约束、可恢复、可持续多轮运行的协议闭环。
附:和上一份概念文档的对应关系¶
如果把两份文档连起来看:
- 《LLM 工具调用确认机制整理》讲的是抽象层:
- User / LLM / Agent / Tool 四层关系
- tool call / tool result 闭环
-
时序图和伪代码
-
本文讲的是 Hermes 具体实现层:
- 哪些文件负责哪一层
- 哪个函数在哪一步参与
- 本地版本中真实的源码落点
两者合起来,既有“原理图”,也有“实物拆机图”。
后续可继续扩展的方向¶
如果后续还想继续深入,可以再单独补这几篇:
run_conversation()全链路逐段精读版- Hermes 各类工具(terminal/file/browser)的 schema 设计模式
- Hermes 插件系统如何插入 pre_tool_call / post_tool_call
- Hermes 的 session persistence 与 message replay 机制
- Hermes 在不同 provider(OpenAI/Anthropic/OpenRouter/Codex)下的工具调用兼容层