辅助压缩调用返回空响应导致 Hermes 网关崩溃 / Auxiliary compression empty response crashes Hermes gateway
辅助压缩调用返回空响应导致 Hermes 网关崩溃 / Auxiliary compression empty response crashes Hermes gateway
作者: cosmoslife
日期: 2026/04/18 14:30:00
博文链接: https://blog.csdn.net/cosmoslife
仓库: https://github.com/NousResearch/hermes-agent
创建时间: 2026-04-18 |关闭时间: 未关闭
修复 PR: 无(尚未修复)
关联 Issue: #11914(同类根因:压缩失败后静态文本注入)、#11906(空文本内容块致 HTTP 400)
关联 PR: #11929(_build_api_kwargs空文本消毒防御)
问题概述
用户使用glm-5.1通过自定义 provider(cmkey.cn/v1)运行 Hermes Agent v0.10.0,当对话触发上下文压缩时,辅助压缩模型返回空响应。Hermes 的 fallback 模型(glm-5)同样返回空。两次调用均未产生有效摘要,导致网关进入异常状态并最终崩溃。
这是一个辅助客户端 provider 兼容性 + 压缩 fallback 链脆弱性的复合问题:自定义 OpenAI 兼容端点在特定 prompt 模式下返回空 content,而压缩流程对空响应的容错不足。
复现路径
- 配置
config.yaml:model provider=custom,base_url=https://cmkey.cn/v1,default=glm-5.1 - 配置 auxiliary.compression:provider=custom,model=
glm-5.1,base_url=https://cmkey.cn/v1 - 正常对话直到上下文超过阈值(默认 85%)
- 压缩触发 →
call_llm(task="compression")→ cmkey.cn 返回空 content - fallback 到 glm-5 → 同样返回空
_generate_summary返回 None → 插入静态 fallback 文本- 但后续操作中空 content 累积,最终网关崩溃
根因分析
置信度: 🟡中(cmkey.cn 为第三方代理,无法直接检查其端行为;Hermes 侧的代码路径已完整追踪)
触发条件
- 用户配置自定义 OpenAI 兼容端点作为辅助压缩 provider
- 对话上下文超过压缩阈值(默认 85% context length)
- 自定义端点对压缩 prompt 返回
content: ""或content: null
执行路径追踪
对话超过阈值 ↓ context_compressor.py:compress() [L999] ↓ 检查 should_compress() ↓ 分离 head/middle/tail 消息 ↓ context_compressor.py:_generate_summary() [L551] ↓ 构建 summary prompt(~670行结构化 prompt) ↓ 调用 call_llm(task="compression", ...) ↓ auxiliary_client.py:call_llm() [L2410] ↓ _resolve_task_provider_model() 解析 provider/model ↓ _get_cached_client() 获取 OpenAI client ↓ client.chat.completions.create(**kwargs) ↓ *** cmkey.cn 返回 response,但 choices[0].message.content = "" 或 null *** ↓ context_compressor.py:_generate_summary() [L700-704] ↓ content = response.choices[0].message.content ← 空字符串或 None ↓ summary = content.strip() ← 空字符串 ↓ return self._with_summary_prefix(summary) ← 只返回 SUMMARY_PREFIX ↓ context_compressor.py:compress() [L1088-1096] ↓ if not summary: ← 空字符串被视为 falsy ↓ 插入静态 fallback 文本 ↓ 后续轮次:网关累积异常状态 → 崩溃代码位置
| 角色 | 文件 | 函数/模块 | 关键行 |
|---|---|---|---|
| 压缩入口 | agent/context_compressor.py | compress() | L999 |
| 摘要生成 | agent/context_compressor.py | _generate_summary() | L551 |
| LLM 调用 | agent/auxiliary_client.py | call_llm() | L2410 |
| 响应验证 | agent/auxiliary_client.py | _validate_llm_response() | L2379 |
| 静态 fallback | agent/context_compressor.py | compress() | L1088 |
| 空文本消毒 | run_agent.py | _build_api_kwargs() | (PR #11929) |
缺陷本质
问题出在三层防御的缺口:
缺口 1:_generate_summary不区分"空摘要"和"调用失败"
# agent/context_compressor.py:L700-704content=response.choices[0].message.contentifnotisinstance(content,str):content=str(content)ifcontentelse""summary=content.strip()当content为空字符串时,summary = "",函数返回_with_summary_prefix(""),结果为只有前缀的空摘要。但调用者compress()中:
# agent/context_compressor.py:L1088ifnotsummary:# 插入静态 fallback空摘要被正确识别为 falsy 并触发 fallback。但问题在于:如果自定义端点返回的不是空字符串,而是" "(空白)或某个无意义的短字符串(如"ok"),summary会被视为 truthy,一个无用的"摘要"会被注入到上下文中,后续对话质量严重下降。
缺口 2:fallback 模型使用同一 provider
# agent/context_compressor.py:L736-742if_is_model_not_foundandself.summary_modelandself.summary_model!=self.model:self._summary_model_fallen_back=True