DeepAI Paper Hermes Agent 教程 Gemini 刚开始 thinking,Hermes streaming 就炸了:.content 字段缺席引发的 AttributeError

Gemini 刚开始 thinking,Hermes streaming 就炸了:.content 字段缺席引发的 AttributeError

流式输出里最怕的不是模型慢,而是第一块 chunk 还没吐出正文,适配层就先崩了。

NousResearch/hermes-agent issue #24974 记录了一个很典型的 provider 兼容问题:Gemini Code Assist streaming 在发送 reasoning-only delta 时,Hermes 的 downstream consumer 访问 delta.content,结果直接抛出:

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

场景发生在 Gemini 2.5 Flash,通过 --provider google-gemini-clihermes chat -v 时复现。表面看是 streaming failed,底层其实是一个很小的 schema shape 问题:delta 可以没有可见正文,但不能没有 content 这个字段。


现场:thinking 已经出来,正文还没开始,streaming 先失败

issue 里的 live log 大概是这样:

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

这段日志很关键。

它说明 Gemini 的 thinking / reasoning 块已经进入了流式链路,但在还没有真正可见正文 content 的时候,Hermes 下游代码尝试读取:

delta.content

如果 delta 是一个普通 dict,可能还能用 .get("content") 兜底;但这里是:

SimpleNamespace

SimpleNamespace 只会暴露创建时传进去的属性。没传 content,访问 delta.content 就不是 None,而是直接 AttributeError


最小复现:不需要 API quota

这个 issue 的好处是,复现不依赖真实 API 调用。

只需要直接调用 adapter 里的构造函数:

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

在修复前,这个断言不会得到 None,而是直接报错:

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

这说明问题不是网络、不是 Gemini quota、也不是模型响应质量,而是 chunk 对象构造时少了一个字段。


根因:content 只有在有正文时才被写入

问题发生在 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)

这段代码对普通 content chunk 没问题。

如果 content="hello",最终 delta 里有:

role
content

如果同时有 reasoning,也会有:

reasoning
reasoning_content

但 reasoning-only chunk 的情况是:

content = "" 或 None
reasoning = "think"

于是 if content: 不成立,content 不会进入 delta_kwargs

最终 delta 只有:

role
reasoning
reasoning_content

没有:

content

下游一访问就炸。


为什么 reasoning-only delta 很正常?

很多新模型的流式协议不再是“每个 chunk 都有 visible text”。

它可能先发:

reasoning / thinking / thought / analysis

然后才发:

content / text / answer

也可能中间穿插:

tool_call_delta

这意味着适配层不能假设每个 chunk 都有可见正文。

但它应该保证:对外伪装成 OpenAI-style ChatCompletionChunk 时,字段形状要稳定。

也就是:

delta.content 可以是 None
但 delta.content 这个属性应该存在

这就是兼容层和业务层之间的契约。


一行修复:默认给 contentNone

issue 给出的修复非常小:

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

这样 reasoning-only chunk 也会得到:

delta.content is None

而不是:

AttributeError

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

因为 OpenAI-style stream delta 里,content 常见语义是:

Optional[str]

没有新增正文时,用 None"" 更能表达“这一块没有可见文本”。


tool-call-only chunk 也要一起测

issue 里还提到了一个很好的 regression test 思路:不要只测 reasoning-only。

应该覆盖三类 chunk:

from agent.gemini_cloudcode_adapter import _make_stream_chunk

cases = [
    ("reasoning-only", dict(model="m", reasoning="think", content="")),
    ("content-only", dict(model="m", content="hello")),
    ("tool-call-only", dict(
        model="m",
        tool_call_delta={"index": 0, "name": "foo", "arguments": "{}"},
    )),
]

for label, kwargs in cases:
    d = _make_stream_chunk(**kwargs).choices[0].delta
    assert hasattr(d, "content"), f"{label}: missing .content"

原因很简单:tool-call-only chunk 也可能没有可见正文。

如果只修 reasoning-only,而不建立“delta schema 必须完整”的测试,类似问题还会在 tool call、function call、metadata-only chunk 上复发。


为什么日志里会出现 retry,反而更难定位?

issue 里提到,AttributeError 被分类成:

reason=unknown
retryable=True

于是 Hermes 会重试。

重试机制本身是好东西,但在这种本地 adapter bug 上,它可能让现象变复杂:

  • 第一次真正的错误是 missing .content
  • 后续重试可能撞到 quota、429、超时;
  • 用户最后看到的错误变成 provider rate limit;
  • 根因被埋在第一段 streaming failed 日志里。

所以遇到 streaming 问题时,要特别留意第一条异常,而不是只看最后一次 retry 的结果。


相关修复:完整 delta schema 才是重点

评论里维护者提到,这类问题可能已由相关修复覆盖:

#21856 fix(cloudcode): always populate full delta schema in stream chunks
#21609 fixes content field omission in Gemini CLI ACP path

关键词是:

always populate full delta schema

也就是说,不只是补一个 content 字段,而是要让 stream chunk 对外呈现稳定结构。

对 Hermes 这种要同时适配多 provider、多 CLI、多模型输出形态的系统来说,稳定 schema 比“刚好能跑通某个模型”更重要。


和 OpenAI-compatible API 的关系:看的是同一层契约

如果你把 Hermes 接到 OpenAI-compatible API,或者通过 DeepAI API 中转站统一管理 Base URL、API Key、模型路由和调用统计,streaming 兼容层也要遵守类似原则。

模型可以有不同的内部事件:

reasoning
content
tool_call
metadata
usage

但对上层 agent loop 来说,最好被规整成稳定的 chunk shape。

例如:

delta.content 始终存在,值可以是 None
delta.tool_calls 始终存在,值可以是 None 或 []
delta.reasoning_content 有就填,没有就 None

这样上层消费逻辑可以专注处理语义,而不是到处写 hasattr(delta, "content")

DeepAI API 中转站适合放在这一层之后观察模型调用:请求是否成功、流式是否持续、错误码是什么、模型是否可用、延迟和用量是否异常。adapter 的 chunk schema 则是进入 provider 前后都要保持一致的接口契约。


排查 Gemini streaming AttributeError 的顺序

如果你遇到类似:

Streaming failed before delivery: 'types.SimpleNamespace' object has no attribute 'content'

可以按这个顺序看:

1. 找第一条异常,不要只看最后一次 retry; 2. 确认是不是 reasoning-only 或 tool-call-only chunk; 3. 检查 adapter 构造 delta 时是否始终写入 content; 4. 对 _make_stream_chunk() 写无 quota 的最小复现; 5. 用 regression test 覆盖 reasoning-only、content-only、tool-call-only; 6. 再去看 provider quota、429、网络超时等外部因素。

这个顺序能避免把本地 schema bug 当成模型服务不稳定。


总结

#24974 的根因不复杂:

_make_stream_chunk() 只在 content truthy 时写入 delta.content。
Gemini reasoning-only chunk 没有 visible content。
SimpleNamespace 缺少 content 属性。
下游访问 delta.content 时抛 AttributeError。

修复也很小:

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

但它背后的经验很重要:

streaming adapter 不能假设每个 chunk 都有正文;
却应该保证每个 chunk 都有稳定的字段形状。

当模型开始输出 reasoning、tool call、metadata 等多种事件时,兼容层的 schema 稳定性就是 agent 能不能持续工作的地基。

Related Post

Gemini 报 missing thought_signature?Hermes Agent 工具调用历史丢签名的排错指南Gemini 报 missing thought_signature?Hermes Agent 工具调用历史丢签名的排错指南

Hermes Agent 使用 Gemini 3 / preview 模型时报 Function call is missing a thought_signature?这通常不是 API Key 或网络问题,而是 tool call 历史消息丢失 extra_content / thought_signature。本文整理升级、复现和源码排查清单。

Matrix 机器人能登录却不回消息?Hermes Agent 静默收不到新消息的排查思路Matrix 机器人能登录却不回消息?Hermes Agent 静默收不到新消息的排查思路

Hermes Agent Matrix bot 能登录、同步旧消息、接受邀请,却完全不回复新消息?这通常不是模型 API 问题,而是 Matrix Gateway 的 sync / receive_response / callback 事件分发链路问题。本文基于真实修复讨论拆解 password login、device_id、initial sync、时间戳过滤和 DeepAI/OpenAI-compatible 分层排查。

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

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