news 2026/9/29 2:17:26

Hermes Agent 核心循环拆解:run_conversation() 的 7 个阶段与 15 种故障自愈

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hermes Agent 核心循环拆解:run_conversation() 的 7 个阶段与 15 种故障自愈

1. 为什么我要逐行拆 run_conversation()

Hermes Agent 的run_conversation()是我见过把「一条用户消息」拆得最细的 Agent Loop 实现。它藏在agent/conversation_loop.py里,整个文件接近 4800 行,核心函数把一次对话切成 7 个阶段,并在主循环里塞进了 15 种以上的故障自愈分支。如果你正在自己写 Agent、或者被「循环跑飞、工具报错、上下文溢出」折磨过,这套结构值得抄作业。

这篇不聊虚的架构图,直接给你能跑的东西:阶段断点日志怎么配、每个阶段打印什么、15 种自愈分支怎么用最小用例触发、报错时怎么判断卡在哪一阶段。适合两类人:一是想读懂 Hermes Agent 源码的开发者,二是自己写 Agent Loop 想加故障恢复的工程师。读完你能在本地复现整条 Agent Loop,并且知道异常到底出在入参校验、上下文装配、模型调用、工具分发还是结果回写。

我试过把这套阶段日志接到自己的小 Agent 上,定位一个「工具结果回填后模型不继续」的 bug,从原来靠猜变成看日志三分钟锁定。下面按阶段拆。

2. 前置:把 TaoToken 接进 Hermes 的模型调用层

Hermes 的 Phase 5 主循环最终要发一次模型请求,请求走的是 OpenAI 兼容协议。本地复现时,模型侧我用 TaoToken 的 API 来承接,原因是它兼容标准 Chat Completion 格式,base_url一改就能接上,不用动conversation_loop.py里的请求构造逻辑。

你需要先拿到一个 API Key。打开 https://taotoken.net/api-keys ,登录后在控制台创建密钥,复制出来。注意 Key 只在创建时完整显示一次,丢了就重建。

拿到 Key 后,在 Hermes 的配置里指向 TaoToken 的 API 地址。API 基础地址是https://taotoken.net/api,注意这个地址不带任何查询参数。配置方式有两种,环境变量最省事:

export OPENAI_API_KEY="sk-你的TaoToken密钥" export OPENAI_BASE_URL="https://taotoken.net/api"

如果你用的是 Hermes 的配置文件(通常是~/.hermes/config.toml或项目内的config.yaml),对应字段这样写:

[model] provider = "openai-compatible" base_url = "https://taotoken.net/api" api_key_env = "OPENAI_API_KEY" model = "claude-sonnet-4-20250514"

注意:base_url结尾不要多加/v1,Hermes 的请求构造里已经带了路径拼接,多写一层会 404。这个坑我在 Phase 5 的 API 调用日志里见过,报错是404 page not found,但日志显示请求发出去了,很容易误判成模型侧问题。

模型名按你实际要用的填。想先确认 Key 和模型通不通,不用急着跑整个 Agent,直接去 https://taotoken.net/api 的模型对话页发一条消息验证即可,比在代码里 debug 快得多。

3. 七个阶段的断点日志配置

Hermes 的run_conversation()内部有大量logger.debug调用,但默认日志级别是 INFO,阶段边界看不到。要复现整条 Loop,先把日志级别调到 DEBUG,并且给每个阶段加一个显式断点。

3.1 打开阶段级日志

在启动 Hermes 前设置:

export HERMES_LOG_LEVEL=DEBUG export HERMES_LOG_PHASE_MARKERS=1

如果源码里没有HERMES_LOG_PHASE_MARKERS这个开关(不同版本命名可能不同),直接在conversation_loop.py的每个阶段入口插一行。七个阶段的插入位置和打印内容如下:

# Phase 1: Turn 初始化 logger.debug("[PHASE-1] turn_init task_id=%s turn_id=%s budget=%s", task_id, turn_id, iteration_budget.remaining) # Phase 2: System Prompt 构建 logger.debug("[PHASE-2] system_prompt_built stable_len=%d context_len=%d volatile_len=%d", len(stable), len(context), len(volatile)) # Phase 3: Preflight 压缩 logger.debug("[PHASE-3] preflight_compress ratio=%.2f rounds=%d", context_ratio, compress_rounds) # Phase 4: 外部记忆注入 logger.debug("[PHASE-4] memory_inject prefetch_len=%d plugin_ctx_len=%d", len(prefetch), len(plugin_ctx)) # Phase 5: 主循环(每轮迭代) logger.debug("[PHASE-5] iteration=%d finish_reason=%s tool_calls=%d", iteration, finish_reason, len(tool_calls)) # Phase 6: Turn 结束 logger.debug("[PHASE-6] turn_end reason=%s budget_left=%d", end_reason, iteration_budget.remaining) # Phase 7: 后置处理 logger.debug("[PHASE-7] post_process memory_written=%s skill_updated=%s", memory_written, skill_updated)

跑起来后,日志里会按顺序出现[PHASE-1]到[PHASE-7]。哪一阶段没打印,问题就在那一阶段之前。这是定位异常阶段最直接的办法。

3.2 各阶段的关键参数

阶段关键参数异常信号
Phase 1budget默认 90budget 为 0 说明上一轮没重置
Phase 2stable_len应恒定每次 turn 都变说明缓存没命中
Phase 3ratio阈值 0.5ratio 持续 >0.5 但 rounds=0 说明压缩没触发
Phase 4prefetch_len每轮迭代都变说明 prefetch 被重复调用
Phase 5iteration/finish_reasoniteration 不增长说明卡在工具执行
Phase 6end_reason出现max_iterations说明预算耗尽
Phase 7memory_written恒为 False 说明后台线程没启动

Phase 2 的stable_len恒定是重点。Hermes 的 System Prompt 分三层:Stable 层整个 session 只构建一次,Context 层按优先级加载项目文件,Volatile 层每 turn 变。如果stable_len每次都不同,说明前缀缓存被破坏,Anthropic 的 prompt caching 会全部 miss,费用和时间都会涨。

4. 主循环里的工具分发与结果回写

Phase 5 是整个文件最长的部分,从入参校验到结果回写都在这里。它的迭代顺序是固定的:中断检查 → API 消息准备 → API 调用 → 响应验证 → finish_reason 分流 → 工具执行 → 空响应兜底。

4.1 finish_reason 分流

模型返回后,finish_reason决定下一步:

if finish_reason == "tool_calls": results = _execute_tool_calls(tool_calls) _backfill_tool_results(messages, results) continue # 进入下一轮迭代 elif finish_reason == "length": _handle_length_overflow() # 最多 3 次 continuation retry elif finish_reason == "stop": return _finalize_response(content)

工具执行前有一道 Guardrail 检查。如果某个工具被安全护栏拦截,Agent 直接生成终止响应,工具根本不会执行。这一点在调试时容易误判:你以为工具报错了,其实是 Guardrail 拦了,日志里搜guardrail_blocked能看到。

4.2 工具结果回写

_backfill_tool_results()把工具返回塞回消息历史,格式必须是role: tool且带tool_call_id。如果回写格式不对,下一轮 API 调用会报消息角色交替异常,触发第 15 种自愈分支。

def _backfill_tool_results(messages, results): for call, result in zip(tool_calls, results): messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False) })

注意:ensure_ascii=False别省。工具返回中文时如果转义成\uXXXX,模型读起来没问题,但你的日志会很难看,排查时容易看漏。

4.3 空响应兜底链

模型返回空内容时,Hermes 不是直接抛异常,而是走一条 7 级降级链:块检测 → Streaming 恢复 → 前序 turn fallback → Nudge → Prefill → 最多 3 次重试 → Fallback Provider →(empty)占位。每一步都检查上一步能不能恢复,全走完还不行才塞占位文本。

这条链的存在是因为早期版本空响应直接抛异常,整个 session 终止,用户丢上下文。现在最差情况也只是拿到一个(empty),循环不崩。

5. 验证请求与成功结果

配好日志和模型接入后,发一条会触发工具调用的消息来验证整条 Loop。用一个简单的文件读取任务:

hermes run "读取当前目录下的 README.md,总结它的前三个小节"

预期日志顺序:

[PHASE-1] turn_init task_id=t-8f3a turn_id=1 budget=90 [PHASE-2] system_prompt_built stable_len=12480 context_len=0 volatile_len=320 [PHASE-3] preflight_compress ratio=0.12 rounds=0 [PHASE-4] memory_inject prefetch_len=0 plugin_ctx_len=0 [PHASE-5] iteration=1 finish_reason=tool_calls tool_calls=1 [PHASE-5] iteration=2 finish_reason=stop tool_calls=0 [PHASE-6] turn_end reason=stop budget_left=88 [PHASE-7] post_process memory_written=True skill_updated=False

看到iteration=1触发tool_calls、iteration=2拿到stop,说明工具分发和结果回写都正常。budget_left=88说明两轮迭代消耗了 2 个预算,符合预期。memory_written=True说明 Phase 7 的后台线程跑起来了。

如果iteration=1之后没有iteration=2,卡在工具执行。检查_execute_tool_calls()的返回,大概率是工具本身抛异常但被吞了。

6. 十五种自愈分支的触发与排查

这 15 种恢复策略不是一次性全跑,而是分层部署,每层恢复不了才进下一层。下面挑几个最容易在本地复现的,给出触发条件和验证方法。

6.1 空响应降级链

触发条件:模型返回content为空且无tool_calls。验证方法:把模型名改成一个不存在的,或者临时在请求里把max_tokens设成 1,强制模型输出被截断。

# 临时在 API 消息准备阶段注入 payload["max_tokens"] = 1

日志里会看到empty_response_detected→streaming_recovery→nudge的序列。如果直接跳到(empty)sentinel,说明中间几级都没恢复成功。

6.2 Context 溢出压缩

触发条件:会话历史超过模型 context window 的 50%。验证方法:连续发 30 条长消息,把历史撑大。

for i in $(seq 1 30); do hermes run "这是第 $i 条测试消息,请回复收到" done

日志里[PHASE-3] preflight_compress ratio=0.6 rounds=1说明压缩触发了。压缩保留前 3 轮和后 20 轮,中间的有损摘要。如果rounds到了 3 还是 ratio >0.5,说明单轮压缩不够,需要检查context_compressor.py的摘要质量。

6.3 工具调用格式错误

触发条件:模型返回的tool_calls结构不符合预期。验证方法:用一个对工具调用支持不好的模型,或者手动构造一个缺function.name的响应。

Hermes 用_invalid_tool_retries计数,超限后强制模型用文本回复,不再尝试工具调用。日志里搜invalid_tool_retries能看到计数增长。

6.4 文件写入未落盘

触发条件:write_file或patch调用返回成功但文件实际没写。验证方法:把工作目录设成只读。

chmod -w /tmp/hermes_test hermes run "在 /tmp/hermes_test 下创建 test.txt"

Phase 6 的_verify_file_operations()会检查文件真实存在,不存在就补 warning。日志里出现file_verify_failed就是这个分支。

6.5 其余分支速查

分支触发条件恢复动作
Images 被拒后端不支持 visionstrip images,降级 text-only
Unicode 错误surrogate 字符ASCII codec 清理,最多 2 次
Rate Limit 429请求过频Fallback Provider + 凭证轮换
Auth 过期凭据失效主动刷新对应 provider 凭据
死 TCP 连接socket 僵尸调用前清理
Budget 耗尽迭代到 90去工具化最终调用
API 超时请求超时重试 + fallback 链
Thinking 不足reasoning 参数不够降低或关闭 thinking
权限不足Guardrail 拦截生成终止响应
Session DB 损坏SQLite 异常WAL fallback + 重试
角色交替异常消息顺序错插入/清理 orphan tool results
安全违规输出不合规过滤后截断或重生成

排查时先看日志里哪个分支的关键字出现,再对照上表定位。大部分分支在 DEBUG 级别都有独立日志行。

7. 接入与排障的下一步

如果你在本地复现时卡在模型调用阶段,先确认base_url和 Key 没问题,去 https://taotoken.net/api-keys 重新生成一个 Key 试试,再对照 https://taotoken.net/api 的接入文档检查请求格式。文档里有标准的 Chat Completion 请求示例,和你conversation_loop.py里构造的 payload 对一遍,字段名对不上就是这里的问题。

如果你是要长期跑 Agent、频繁触发工具调用和自愈分支,建议用 Coding Plan 来承接高频请求,比按次调用更稳。入口在 https://taotoken.net/coding-plan ,适合这种需要反复迭代、每轮都发模型请求的场景。

排障的核心思路就一条:先看[PHASE-N]日志断在哪,再进对应阶段的源码看自愈分支有没有触发。阶段日志配好之后,整条 Agent Loop 对你就是透明的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 2:16:32

使用 Python 将 HTML 表格写入数据库的方法

在数据采集和处理过程中,经常需要将 HTML 表格中的数据提取并存入数据库,以便进一步分析和处理。实现这一功能通常涉及解析 HTML 获取表格内容,并将提取的数据以结构化形式存入数据库。为了确保数据的完整性和安全性,需要考虑数据…

作者头像 李华
网站建设 2026/9/29 2:16:24

OpenCV与PIL图像基础:从读写到PyTorch边缘检测的完整实战

上次我们把 PyTorch 的环境跑通了,这次继续往前走,把图像处理的两大基础库 OpenCV 和 PIL 装好,再拿一个真正能跑的小实战练手。很多零基础的同学在这里会卡住:不是 PyTorch 不会用,而是读进来的图片到底是个 numpy 数…

作者头像 李华
网站建设 2026/9/29 2:16:22

解决 MoviePy 输出文件无法保存问题

在使用 moviepy 进行视频处理时&#xff0c;调用 write_videofile 方法保存视频文件时出现如下错误&#xff1a; Traceback (most recent call last):File "Video\VideoShuffleConcatenate\script.py", line 361, in runFile "<decorator-gen-55>", …

作者头像 李华
网站建设 2026/9/29 2:15:27

PositionSetpointTriplet 消息深度解析:PX4 航点任务的核心数据通道

嵌入式物联网机器人自动驾驶智能硬件 【免费下载链接】PX4-Autopilot PX4 Autopilot Software 项目地址&#xff1a; https://gitcode.com/gh_mirrors/px/PX4-Autopilot 点击查看 免费下载 导读 PositionSetpointTriplet&#xff08;全局位置设定点三元组&#xff09;是 PX4 …

作者头像 李华