文件:hermes_about/hermes-tool-calling-source-walkthrough.md 大小:27.2 KB 编码:UTF-8

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 步:

  1. 工具模块通过 registry.register(...) 在导入时自注册
  2. model_tools.py 启动时触发工具发现(builtin / MCP / plugin)
  3. AIAgent.__init__() 调用 get_tool_definitions(),按 toolset 解析出当前会话可见的工具 schema
  4. run_conversation() 进入主循环,构造 messages + tools 发给模型
  5. 模型返回 assistant_message.tool_calls
  6. Hermes 先做工具名与参数层面的校验,再标准化为内部 assistant message
  7. _execute_tool_calls() 执行工具,必要时并发执行,并把结果作为 role=tool 追加进消息历史
  8. 主循环继续下一轮;如果没有 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

这是“工具集合定义层”。

它定义:

  • web
  • terminal
  • file
  • browser
  • hermes-cli
  • hermes-discord
  • hermes-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_file
  • write_file
  • patch
  • search_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 里,核心数据结构是 ToolEntryToolRegistry

4.1 ToolEntry

tools/registry.py:76-98

一个工具注册后,会被封装成 ToolEntry,里面至少包含:

  • name
  • toolset
  • schema
  • handler
  • check_fn
  • requires_env
  • is_async
  • description
  • emoji
  • max_result_size_chars

这说明 Hermes 对工具不是只存一个 callable,而是存“完整元信息对象”。

4.2 ToolRegistry.register()

tools/registry.py:176-227

这个函数负责把工具写进注册表。

几个重要点:

  1. 会做冲突控制
    - 不允许普通工具轻易覆盖已有工具
    - 只对某些 MCP-to-MCP 的刷新场景放宽

  2. 会记录 toolset 对应的 check_fn
    - 这为后续 toolset 可用性判断提供基础

  3. 注册是线程安全的
    - Registry 内部有 RLock

这说明 Hermes 的工具系统是考虑过:

  • 插件扩展
  • MCP 动态刷新
  • 多线程读取
  • 名称冲突

而不是只服务于一个静态单进程 CLI。


5. toolset 是怎么解析成真实工具列表的

Hermes 不会简单把注册表里所有工具全发给模型,而是通过 toolsets.py 先做一轮筛选。

5.1 静态 toolset 定义

toolsets.py:68 开始,TOOLSETS = {...} 定义了很多 toolset。

例如:

  • web
  • terminal
  • file
  • browser
  • skills
  • memory
  • delegation
  • hermes-cli
  • hermes-gateway

其中还有 _HERMES_CORE_TOOLStoolsets.py:31-63),供多个平台 toolset 复用。

5.2 resolve_toolset():递归展开

toolsets.py:465-515

resolve_toolset(name) 会:

  1. 找到该 toolset 的直接工具
  2. 递归处理它的 includes
  3. 去重后返回最终工具名列表

这就支持了:

  • 单层 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_toolsets
  • disabled_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 还做了几件很工程化的事:

  1. 动态重建 execute_code schema
    - 只把当前会话实际可用的 sandbox 工具写进去
    - 避免模型看到并不存在的工具名

  2. 动态调整某些工具 schema
    - 例如 Discord 工具可根据能力动态收缩 schema

  3. 清理跨工具描述误导
    - 例如 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_count
  • IterationBudget

并且留了一个 grace call 机制。

这意味着它不是无限循环执行工具,而是严格受预算控制。


8. 每轮调用模型前,Hermes 做了什么

在主循环内部(大约 run_agent.py:9129-9258),Hermes 会构建真正发给 API 的 api_messages

8.1 从内部 messages 拷贝出 api_messages

Hermes 内部保存的是 richer message 结构,里面可能包含:

  • reasoning
  • finish_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 调用前都会跑这个安全网。

它做两件关键事:

  1. 删除 orphaned tool result
    - 即有 role=tool,但找不到对应 assistant tool call 的结果消息

  2. 给缺失结果的 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 会:

  1. 检查 tool name 是否在 self.valid_tool_names
  2. 如果不在,先尝试 _repair_tool_call() 自动修复
  3. 仍无效则:
    - 记录重试次数
    - 把错误以 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:1125111306

这一步很重要,因为它保证消息顺序是:

  1. assistant 发出 tool_calls
  2. 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_file
  • search_files
  • session_search
  • skill_view
  • skills_list
  • vision_analyze
  • web_search
  • web_extract
  • 以及若干只读工具

13.3 路径作用域工具要做路径冲突判断

_PATH_SCOPED_TOOLS 包括:

  • read_file
  • write_file
  • patch

如果两个调用目标路径重叠,就不允许并发。

也就是说,Hermes 的并发策略是:

只有看起来彼此独立、不会互相污染状态的工具调用,才走并发。

这很实用,也很稳。


14. 真正执行单个工具时走哪条路径

核心函数:run_agent.py:7694-7771_invoke_tool()

这里是非常清晰的分层点。

14.1 Agent-level tools:不走 registry.dispatch

这些工具需要依赖 agent 自身状态,所以直接在 _invoke_tool() 里处理:

  • todo
  • session_search
  • memory
  • clarify
  • delegate_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_call
  • post_tool_call
  • transform_tool_result

这说明 Hermes 的工具执行链路为插件系统预留了扩展点。

15.4 最终交给 registry.dispatch()

  • execute_code 会带上当前可用工具集合
  • 其他工具正常 dispatch

出错时统一返回 JSON error string。


16. registry.dispatch() 是最后一跳分发

位置:tools/registry.py:292-309

逻辑非常直接:

  1. 通过名字找到 ToolEntry
  2. 如果不存在,返回 Unknown tool
  3. 如果是 async handler,则通过 _run_async() 桥接
  4. 否则直接调用 entry.handler(args, **kwargs)
  5. 捕获异常并统一返回 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

不管并发还是顺序,核心协议都不变:

  1. assistant message 先入历史
  2. 每个 tool result 都带 tool_call_id
  3. 结果进入下一轮模型上下文

这保证了协议一致性。


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。

像:

  • todo
  • memory
  • session_search
  • clarify
  • delegate_task

这些依赖 agent 实例状态的工具,被留在 _invoke_tool() 里处理。

这是一个很合理的边界划分。

23.2 schema 过滤不是一次性的

Hermes 不只是“启动时筛工具”,还会在 get_tool_definitions() 阶段对某些 schema 动态重写。

这说明它意识到:

错误的 schema 暴露本身就会诱导模型产生错误工具调用。

23.3 消息配对修复是必要的

_sanitize_api_messages() 这类函数看似不起眼,但实际上是多轮工具调用系统稳定运行的关键。

因为真实环境里会出现:

  • session 历史加载不完整
  • 压缩丢掉局部消息
  • 插件插消息
  • 中断导致 tool result 缺失

如果不修补,模型 API 很容易直接报错。


24. 如果你要读 Hermes 的工具调用源码,推荐阅读顺序

最省脑的顺序建议如下:

第一轮:抓主链

  1. run_agent.py
    - AIAgent.__init__()self.tools / self.valid_tool_names
    - run_conversation() 看主循环
    - _build_assistant_message()
    - _execute_tool_calls()
    - _invoke_tool()

第二轮:看工具编排层

  1. model_tools.py
    - get_tool_definitions()
    - handle_function_call()
    - _run_async()

第三轮:看注册中心

  1. tools/registry.py
    - discover_builtin_tools()
    - register()
    - get_definitions()
    - dispatch()

第四轮:看 toolset

  1. toolsets.py
    - _HERMES_CORE_TOOLS
    - TOOLSETS
    - resolve_toolset()
    - validate_toolset()

第五轮:挑具体工具看样板

  1. 工具实现文件
    - tools/file_tools.py
    - tools/terminal_tool.py

这样能最快建立 Hermes 的整体心智模型。


25. 最终总结

从源码来看,Hermes 的工具调用实现有几个鲜明特点:

  1. 自注册工具架构
    - 工具模块导入即注册

  2. toolset 驱动的工具暴露
    - 不是所有工具都默认暴露给模型

  3. registry 统一管理 schema / handler / check_fn
    - 把工具元数据集中化

  4. run_conversation() 内部有清晰的工具循环
    - 模型返回 tool_calls -> 执行 -> 回填 -> 继续

  5. 对“模型不可靠输出”做了很多守护
    - 工具名自动修复
    - JSON 参数校验
    - orphaned tool result 修补
    - post-tool 空回复恢复
    - context compression 与 fallback

一句话总结:

Hermes 不是“让模型直接调工具”,而是通过注册中心、toolset 解析、主循环校验、调度执行和消息修补,把工具调用包装成一个强约束、可恢复、可持续多轮运行的协议闭环。


附:和上一份概念文档的对应关系

如果把两份文档连起来看:

  • 《LLM 工具调用确认机制整理》讲的是抽象层:
  • User / LLM / Agent / Tool 四层关系
  • tool call / tool result 闭环
  • 时序图和伪代码

  • 本文讲的是 Hermes 具体实现层:

  • 哪些文件负责哪一层
  • 哪个函数在哪一步参与
  • 本地版本中真实的源码落点

两者合起来,既有“原理图”,也有“实物拆机图”。


后续可继续扩展的方向

如果后续还想继续深入,可以再单独补这几篇:

  1. run_conversation() 全链路逐段精读版
  2. Hermes 各类工具(terminal/file/browser)的 schema 设计模式
  3. Hermes 插件系统如何插入 pre_tool_call / post_tool_call
  4. Hermes 的 session persistence 与 message replay 机制
  5. Hermes 在不同 provider(OpenAI/Anthropic/OpenRouter/Codex)下的工具调用兼容层