DeepAI Paper Hermes Agent 教程,Hermes 上下文、流式与输出,Hermes 模型与 Provider API 只有思考没有正文,Hermes 流式输出也会崩:Gemini reasoning-only delta 的 .content 空洞

只有思考没有正文,Hermes 流式输出也会崩:Gemini reasoning-only delta 的 .content 空洞

有些流式错误看起来像模型断流,实际上是客户端把“没有正文”的片段当成“没有字段”。

NousResearch/hermes-agent issue #24974 讲的就是这个细节。

用户通过 google-gemini-cli provider 使用 Gemini 2.5 Flash。模型先输出 thinking / reasoning block,还没有 visible text。Hermes 的 Gemini Code Assist streaming adapter 把这个 reasoning-only chunk 转成 OpenAI 风格的 stream delta。

但旧代码只在有正文时才写入:

content

于是 downstream consumer 访问:

delta.content

直接炸:

AttributeError: 'types.SimpleNamespace' object has no attribute 'content'

这不是 Gemini 没回复,也不是 API key、网络、配额第一时间出问题。

更准确地说:

流式 chunk 的 schema 没补齐。

现场表现:thinking 已经来了,正文还没来,流先挂了

issue 里的 live log 很典型:

run_agent - INFO - Gemini Cloud Code Assist client created
[thinking] **Acknowledging your input**
I've processed your acknowledgment and am ready for your next directive...
run_agent - INFO - Streaming failed before delivery: 'types.SimpleNamespace' object has no attribute 'content'
run_agent - WARNING - API call failed (attempt 2/3) error_type=AttributeError

关键点在这里:

[thinking] 已经出现了

说明模型并非完全没有响应。

真正失败点是 Hermes 收到 reasoning-only delta 后,后续消费代码假设每个 delta 都有 .content 属性。

但这个属性压根没被创建。


最小复现:不需要真实 API quota

这个 bug 甚至不需要真的调用 Gemini。

issue 给出的最小复现是:

from agent.gemini_cloudcode_adapter import _make_stream_chunk

d = _make_stream_chunk(model="m", reasoning="think").choices[0].delta
assert d.content is None

旧版本会在这里报:

AttributeError

因为 SimpleNamespace 不是 dict。

你没传进去的 key,不会变成 None

它就是不存在。


根因:SimpleNamespace 不会自动补字段

问题出在:

agent/gemini_cloudcode_adapter.py::_make_stream_chunk()

旧逻辑大概是:

delta_kwargs = {"role": "assistant"}

if content:
    delta_kwargs["content"] = content

if reasoning:
    delta_kwargs["reasoning"] = reasoning
    delta_kwargs["reasoning_content"] = reasoning

delta = SimpleNamespace(**delta_kwargs)

如果当前 chunk 是 reasoning-only:

content = ""
reasoning = "think"

那么 if content: 不成立。

最终生成的是:

SimpleNamespace(
    role="assistant",
    reasoning="think",
    reasoning_content="think"
)

里面没有:

content

于是访问:

delta.content

必然 AttributeError。


为什么这类 bug 在 Gemini 上特别容易出现?

因为 Gemini / reasoning 模型的流式输出不一定先给你可见文本。

它可能先吐:

  • thinking block;
  • reasoning delta;
  • tool call delta;
  • metadata;
  • 然后才是 visible content。

如果客户端 adapter 模拟 OpenAI ChatCompletionChunk,就必须保证 delta schema 稳定。

即使内容为空,也应该有:

content = None

而不是直接缺字段。

对下游代码来说,这两者差别巨大:

hasattr(delta, "content") == True

和:

AttributeError

不是一个级别的问题。


正确修法:给 delta 补完整 schema

issue 提出的最小修复是一行:

- delta_kwargs = {"role": "assistant"}
- if content:
-     delta_kwargs["content"] = content
+ delta_kwargs = {"role": "assistant", "content": content or None}

为什么是 None 而不是空字符串?

因为 OpenAI 风格的 ChatCompletionChunk 里,stream delta 的:

delta.content

通常是:

Optional[str]

也就是可以为 None

当这个片段只有 reasoning,没有 visible text 时,content=Nonecontent="" 更贴近语义。

后续维护者评论也确认类似方向:

#21856: always populate full delta schema in stream chunks

也就是给 stream chunk 的 delta key 预先填成 None


相关路径:Gemini CLI ACP 也踩过类似坑

评论还提到相关 issue / 修复:

#21609 fixes the content field omission in the Gemini CLI ACP path

这说明问题不是一个孤立 typo,而是 adapter 设计里常见的协议形态陷阱。

当一个系统把 Gemini、OpenAI、ACP、Cloud Code Assist 等不同流式协议统一成一个内部 chunk 对象时,字段缺省策略必须统一。

否则下游处理逻辑会变成:

有些 provider 是 content=None
有些 provider 是没有 content 属性
有些 provider 是 content=""

这种不一致会让 streaming consumer 很脆弱。


为什么错误会被重试,反而更难定位?

issue 里还有一个很实用的细节:

这个 AttributeError 被 Hermes 分类成:

reason=unknown
retryable=True

于是用户看到的是:

API call failed (attempt 2/3)

这会制造误导。

因为看起来像:

  • Gemini API 不稳定;
  • 网络断了;
  • 模型超时;
  • 配额不足;
  • provider 要 fallback。

如果后续 retry 又撞上 429 quota,用户甚至可能以为根因是 quota。

但第一次真正的根因是本地 adapter schema 缺字段。


排查时怎么一眼识别?

如果你看到这些组合,就要怀疑 reasoning-only delta 问题:

provider: google-gemini-cli
model: gemini-2.5-flash 或其他 Gemini thinking 模型
streaming failed before delivery
SimpleNamespace object has no attribute 'content'
[thinking] 已经打印过

尤其是日志里先出现:

[thinking]

再出现:

AttributeError: ... no attribute 'content'

基本就不是 API 服务端没返回,而是客户端处理 reasoning chunk 出错。


对 DeepAI API 中转站用户有什么启发?

这篇问题本身发生在 Gemini Code Assist / Cloud Code adapter,不是 DeepAI API 中转站的问题。

但它对 OpenAI-compatible 工具链有一个重要提醒:

统一 API 入口只能统一请求入口,不能替每个客户端修正内部 streaming schema。

如果你在 Cherry Studio、Cline、Dify、Open WebUI 或 Hermes 里使用 DeepAI API 中转站,重点要区分两层问题:

  • API 层:Base URL、API Key、模型 ID、请求参数、计费、限流;
  • 客户端适配层:stream chunk schema、tool call delta、reasoning metadata、history replay。

DeepAI 可以作为稳定的 OpenAI-compatible API 入口,方便统一模型调用和 key 管理。

但如果某个客户端内部把 content=None 写成了“没有 content 字段”,那仍然需要客户端升级或修补。


临时规避方案

如果你还在旧版本 Hermes,可以考虑:

1. 暂时关闭 streaming

如果配置允许,关闭流式输出,让客户端走 non-stream response path。

因为 issue 指向的是:

_make_stream_chunk()

非流式路径未必触发同样问题。

2. 换不先输出 reasoning-only delta 的模型

如果某些 Gemini 模型不会先吐 thinking block,可以暂时避开触发条件。

但这不是根治。

3. 升级到包含相关修复的 Hermes

重点关注:

  • #21856
  • #21609
  • commit 26933c2f592bda25df735c555620a2a978cfefb6

4. 本地打补丁

最小补丁思路:

delta_kwargs = {"role": "assistant", "content": content or None}

更完整的方式是把 delta schema 里的常见字段都初始化为 None


FAQ

看到 .content AttributeError 是 Gemini API 挂了吗?

不一定。这个 issue 中 Gemini 已经返回了 reasoning chunk,失败发生在 Hermes 本地 streaming adapter。

为什么 reasoning-only chunk 会没有 content?

因为旧代码只有 if content: 为真时才把 content 放进 SimpleNamespace。reasoning-only chunk 的 content 为空,所以字段缺失。

content=None 和没有 content 字段有什么区别?

区别很大。content=None 表示这个 delta 没有 visible text;没有字段会让 delta.content 访问直接抛 AttributeError。

关闭 streaming 有用吗?

可能有用。该问题指向 _make_stream_chunk(),关闭 streaming 可能避开这个具体路径,但最好还是升级或修补客户端。

DeepAI API 中转站能修复这个问题吗?

不能。这是 Hermes Gemini adapter 的本地 schema bug。DeepAI 可作为 OpenAI-compatible API 入口,但不能修复客户端内部 chunk 字段缺失。


总结

#24974 的核心不是“Gemini 没有 content”,而是:

reasoning-only delta 也必须有 content 字段,只是值可以是 None。

当 Agent 框架把不同 provider 的流式协议统一成 OpenAI-like chunk 时,schema 稳定性比字段值本身更重要。

少一个 .content,就足以让整条 streaming 链路从“模型正在思考”变成“客户端直接崩溃”。

Related Post

Openclaw deepai stream true sse wrapper no display.png

OpenClaw 接入 DeepAI API 中转站:stream=true 返回单个 JSON 导致界面不显示怎么修OpenClaw 接入 DeepAI API 中转站:stream=true 返回单个 JSON 导致界面不显示怎么修

OpenClaw openai-completions Provider 默认发送 stream:true,如果自定义 OpenAI-compatible model-router 只返回单个 chat.completion JSON,界面可能沉默不显示。本文结合 OpenClaw Issue #14262,说明为什么 stream:true 必须返回 text/event-stream SSE chunk,以及 DeepAI API 中转站/自建代理如何同时兼容流式和非流式响应。

/reload-mcp 一按就卡死:Hermes CLI 为什么会在确认框里等不到回车?/reload-mcp 一按就卡死:Hermes CLI 为什么会在确认框里等不到回车?

Hermes CLI 执行 /reload-mcp 后确认框显示出来,却无法输入 1/2/3,SSH 会话像被冻住。本文客观复盘 #23853:prompt_toolkit raw mode、daemon thread 中的 input() fallback、 / 行结束符错位、TUI slash worker pipe 死锁,以及 prompt_toolkit-native modal 的修复方向。

少了一个 api_mode,模型目录就串台:Hermes 自定义 Provider 为什么把 Anthropic 当 OpenAI 校验少了一个 api_mode,模型目录就串台:Hermes 自定义 Provider 为什么把 Anthropic 当 OpenAI 校验

Hermes 自定义 provider 明明配置了 api_mode: anthropic_messages,/model 校验却丢掉 api_mode,导致按 OpenAI catalog 探测并提示 gpt-5-pro 等相似模型。本文复盘 #9146:为什么协议模式必须贯穿 validate_requested_model、probe_api_models 与 fetch_api_models。