DeepAI Paper Hermes Agent 教程,Hermes 上下文、流式与输出 让 Hermes 别回复却收到 Codex incomplete?空响应被重试 3 次的原因

让 Hermes 别回复却收到 Codex incomplete?空响应被重试 3 次的原因

在群聊里,AI 助手有时最好的回复是“不回复”。比如你明确告诉机器人:这条消息不用回应,保持安静。

但 Hermes Agent 曾经在 openai-codex 路径上出现过一个反直觉问题:Codex 已经正常完成了一个空 final answer,Hermes 却把它归一化成 incomplete,连续重试 3 次,最后反而在群里发出错误:

⚠️ Codex response remained incomplete after 3 continuation attempts

这篇文章基于 NousResearch/hermes-agent issue #3956,解释为什么“空回复”不等于“未完成”,以及 OpenAI/Codex Responses API 的状态语义在 Agent 框架里该如何处理。


典型场景:用户说“不用回复”,Bot 偏偏报错

issue 里的复现很有代表性:

  • 场景:Telegram group;
  • 用户告诉 bot 不要回复;
  • Codex Responses API 返回的是一个有效的空 final answer;
  • Hermes 没有保持沉默,而是进入 continuation loop;
  • 重试 3 次后发送错误消息。

也就是说,原本的目标是:

用户:这条不用回复
Bot:<保持沉默>

实际变成:

Bot:⚠️ Codex response remained incomplete after 3 continuation attempts

在群聊里,这比普通报错更糟:用户已经明确要求不要打扰,结果系统为了“补完”一个本来已经完成的空回复,制造了额外噪音。


关键区别:empty final answer ≠ incomplete

issue 中记录的 provider 结果是:

response.status = completed
message.status = completed
message.phase = final_answer
final text = empty

这组状态语义非常重要。

它表示:

  • Codex 请求完成了;
  • final answer 阶段也完成了;
  • 只是最终文本为空。

空文本在很多 Agent 场景中是合法结果:

  • 用户要求不要回复;
  • 群聊中无需插话;
  • 审核/路由任务决定不输出;
  • 工具执行后没有需要展示的内容;
  • 系统策略要求静默。

所以判断是否 incomplete,不能只看 final_text 是否为空,还要看 provider 返回的完成状态。


Hermes 当时的误判逻辑

issue 指向 run_agent.py_normalize_codex_response() 的一个分支:

elif reasoning_items_raw and not final_text:
    finish_reason = "incomplete"

这个逻辑的问题在于:只要存在 reasoning items 且 final text 为空,就倾向于认为响应不完整。

但 Codex 可能已经明确告诉你:

status = completed
phase = final_answer

如果归一化层忽略这个状态,就会把“合法的空完成”误改成“未完成”。


误判之后发生了什么?

Hermes 看到:

finish_reason = incomplete

就会进入 continuation loop,试图继续向模型请求补全内容。

问题是,模型本来就完成了,它不是被截断,也不是还缺 continuation。于是系统重复尝试后,最终给用户一个错误:

Codex response remained incomplete after 3 continuation attempts

这类错误在日志里看起来像“模型没给够内容”,但本质是 归一化层把完成状态改坏了


为什么这不是普通的模型 API 故障?

如果是模型 API 故障,你通常会看到:

  • HTTP 401 / 403 / 429;
  • timeout;
  • upstream 5xx;
  • token limit 截断;
  • streaming 中断;
  • provider 返回明确 error object。

#3956 的关键点是:provider 返回本身是 completed。

故障发生在 Hermes 内部把 provider-specific response 转成统一 agent response 的过程中:

Codex Responses API completed
        ↓
Hermes _normalize_codex_response()
        ↓
finish_reason 被改成 incomplete
        ↓
continuation loop 误触发

所以这不是“Codex 不会回答”,而是“框架不承认 Codex 的空完成”。


群聊机器人为什么必须支持“沉默成功”?

很多 AI Bot 的早期设计默认:每次收到消息都应该回点什么。

但真实群聊不是这样。

一个好用的群聊 Agent 必须能判断:

  • 什么时候应该回答;
  • 什么时候应该只用 reaction;
  • 什么时候应该记录但不打扰;
  • 什么时候应该完全沉默。

如果框架不能表达“成功但无输出”,就会把很多正常行为当成异常:

无输出 → 不完整 → 重试 → 报错 → 打扰群聊

这会直接破坏群聊体验。


排查清单:如何确认你遇到的是空完成误判

如果你看到类似:

Codex response remained incomplete after 3 continuation attempts

可以按下面检查。

1. 看 provider 原始状态

重点看:

response.status
message.status
message.phase
final text

如果是:

completed / completed / final_answer / empty

那就不是未完成,而是空完成。

2. 看 Hermes 归一化后的 finish_reason

如果 _normalize_codex_response() 把它变成:

finish_reason = incomplete

就命中了问题核心。

3. 看是否进入 continuation loop

如果连续 3 次 continuation,然后报:

remained incomplete after 3 continuation attempts

说明系统把空完成当成了需要补全。

4. 检查版本或更新记录

评论里有人反馈更新后恢复,也有人在 latest version 仍复现。遇到这类问题,建议确认具体 commit / version,而不是只说“最新版”。


正确修复思路:保留 provider 的 completed 状态

合理的归一化逻辑应该优先尊重 provider 状态:

如果 response.status = completed
且 message.status = completed
且 message.phase = final_answer
那么 final_text 为空也可以是 completed

也就是说,finish_reason 不能只由 final_text 是否为空决定。

更稳的设计是区分三种情况:

情况含义应对
completed + empty final_answer合法静默完成不重试,不报错
incomplete + 有截断信号真实未完成continuation
error / failed / cancelledProvider 异常报错或重试

这样才能支持“沉默成功”。


和 DeepAI API 中转站的关系

DeepAI API 中转站提供 OpenAI-compatible API 入口,例如:

Base URL: https://api.deepai.wang/v1
API Key: 你的 DeepAI Key
Model: 以 DeepAI 控制台为准

如果 Hermes 走的是 DeepAI 或其他 OpenAI-compatible 上游,也可能遇到类似“响应语义映射”问题:上游返回格式是合理的,但 Agent 框架的归一化层没有正确理解。

不过,本文这个 issue 的核心在 Hermes 的 Codex response normalization。DeepAI API 中转站不能替 Hermes 修复 _normalize_codex_response() 里的 finish_reason 判断。

实际排查时建议:

  • 先保存 provider 原始响应;
  • 再看 Hermes 归一化后的结构;
  • 不要只看最终聊天报错;
  • 如果原始响应是 completed,优先查框架映射层。

FAQ

空回复为什么算成功?

因为用户可能明确要求 bot 不回复,或者 Agent 判断当前场景不应打扰。空 final answer 可以是合法完成。

incomplete 一定代表模型没输出完吗?

不一定。它也可能是框架归一化错误,把 completed empty final answer 误标成 incomplete。

为什么会重试 3 次?

Hermes 看到 finish_reason = incomplete 后会尝试 continuation,希望模型补完未完成内容。但这里本来就没有需要补的内容。

DeepAI API 中转站能解决吗?

不能直接解决 Hermes 内部的 Codex normalization bug。但使用任何 OpenAI-compatible 上游时,都建议保存原始响应,排查是否是映射层误判。

群聊里怎么避免这种噪音?

框架要明确支持“成功但无输出”的状态,不要把空文本自动当失败或 incomplete。


总结

Hermes 的 openai-codex 路径曾经把一个合法状态:

completed + final_answer + empty text

误归一化成:

finish_reason = incomplete

结果用户要求 bot 不回复,系统却重试 3 次并发送错误提示。

一句话:在 Agent 框架里,空输出不是天然错误;对群聊机器人来说,“沉默成功”必须是一等状态。

Related Post

Hermes 命令一跑就卡死:install.sh 重跑后为什么把 CLI 入口改成了自我调用Hermes 命令一跑就卡死:install.sh 重跑后为什么把 CLI 入口改成了自我调用

重跑 Hermes install.sh 后,hermes doctor 没报错也没输出,只是一直卡住?本文复盘 #21454:旧版 ~/.local/bin/hermes symlink 被新版 cat > wrapper 穿透覆盖,导致 venv/bin/hermes 变成自我 exec 的无限循环。

对象不是流:Copilot ACP 把 SimpleNamespace 塞进迭代器后的连锁崩溃对象不是流:Copilot ACP 把 SimpleNamespace 塞进迭代器后的连锁崩溃

Hermes copilot-acp 使用 claude-opus-4.7 时出现 SimpleNamespace object is not iterable,不是模型或 API Key 问题,而是 ACP adapter 把对象当成 stream iterable 处理。本文复盘 #16271,解释 acp://copilot 的 response 形态为什么不能靠猜。

Hermes terminal 误拦截 setsid:安全过滤为什么不能只靠全文正则Hermes terminal 误拦截 setsid:安全过滤为什么不能只靠全文正则

Hermes terminal tool 为了拦截 nohup、disown、setsid 这类 shell-level background wrapper 使用了全文关键词正则,结果连引号、commit message、PR body、echo 和 Python 字符串里的 setsid 也被误拦。本文客观复盘 #20064:安全过滤应识别 shell 命令位置,而不是只匹配任意文本。