压缩把模型正等着的那条命令结果吃掉了


你让 agent 跑一遍集成测试,它老老实实执行了,日志刷了几千行。等它回来,你问「挂在哪几个用例」,它说:抱歉,我看到的输出是一行占位符。你没有看错——命令确实跑了,但在模型读到结果之前,上下文压缩把那条结果替换成了一行 stub,附带的截图也一并退役了。这听起来像玄学,其实是三个很具体的漏洞,2026-09-26 被一次性修掉(PR #124208、#124207、#124231)。这篇把三个补丁讲清楚:哪些内容现在受保护、什么情况下仍然会被压缩、以及你需不需要做点什么。

先讲清楚:什么叫「回合内压缩」

一个跑得久的会话,上下文迟早会满。Hermes 的处理办法是压缩(compaction):把旧的历史换成摘要,腾出空间继续干活。平时压缩发生在两次回合之间,你看不出什么。

问题出在另一种时机——发请求之前的准备阶段(代码里叫 preflight)。此刻上下文里躺着一条刚刚拿到、但模型还没读到的工具结果:命令已经执行完,结果已经写进对话,只是模型的下一次请求还没发出去。

原来的实现按「除了最新的那个,其余全部降级」的规则来处理这一轮(pressure pass),图片退役逻辑完全没考虑这个还没被读的回合,尾部裁剪的下界又设成了 0。结果就是:那条结果可能被降级成一行 stub,或者它的图片被退役。模型醒来时,手里空空的。

修复一:正在等待的那一轮,原样保留(PR #124208)

补丁的核心是一个新函数 _pending_tool_round:它会找出对话结尾那一段连续的 tool 结果,并且跳过紧邻的 /steer 行——因为你在子代理运行时插话,会在结果和模型读取之间插进 steer 消息,处理不好就会把这一轮切断。

找到这一轮之后,它会在四个地方被豁免:

处理阶段 之前会发生什么 现在
pass 2(压力压缩) 把这一轮降级成 stub 或摘要 整轮原样保留
图片退役(两次扫描) 只保住最新一张,多图回合丢图 多图回合的图片全部保留
尾部裁剪 下界为 0,可裁掉这一轮 裁不到这一轮
pass 3(参数截断) 可能截断发起调用的那条 assistant 行 拥有这一轮的 tool_calls 行一并保护

还有一条边界条件值得记住:如果这一轮本身就占掉了「有效输入窗口」的 20% 以上,就不再豁免。这是故意的——超大输出仍然要走原来的溢出逃生通道,否则压缩会在真正需要它的场景里失效。

修复二:所有「模型看得见的省略」,都走同一个带守卫的标记(PR #124207)

以前的截断提示写得很朴素:...[truncated]。问题在于,Hermes 在工具分发边界有一道守卫(agent/tool_dispatch_helpers.py 里的 _COMPRESSION_MARKER_RE),专门用来识别「模型把省略标记抄进了写文件的调用」。它只认那个特殊标记,认不出 ...[truncated]。

后果不是「少写一行日志」,而是数据损失级别的事故:模型把自己没看到全的那段话,连同省略标记一起写进 write_file 或 patch,守卫放行,文件真的被写坏。

现在所有「模型可见的截断」统一走非拟真的标记 ⟪HERMES-CONTEXT-COMPRESSION: …⟫,覆盖范围包括压缩器本身、技能内联 shell 的输出预处理、LSP 报告、verification stop、memory context,以及面向 agent 的 Slack 序列化和 trajectory 压缩文本。少部分 tools/* 里的位置仍然是后续工作——这点官方在 PR 里也明说了。

修复三:读了却「没变」的假象(PR #124231)

read_file 和 skill_view 都带去重缓存:同一个文件读第二遍,会直接回一句「未变更」,省下 token。这本身没问题,出问题的是缓存和正文不同步。

有三条改写路径会丢掉工具结果的正文,却没有重置这个缓存:

  • 回合结束后的微压缩(micro-compaction);
  • 原生的 Responses checkpoint 回放;
  • Codex app-server 侧的压缩。

于是模型既看不到正文(被丢掉了),又收到「未变更」(缓存以为还在)——两头落空。现在这三条路径都会重置缓存;微压缩只在确实改写了 transcript 时才重置,避免无谓的失效。

你需要做什么

  • 这三个补丁合并于 2026-09-26,晚于 v0.21.5 的发布标签,目前位于 main。用 hermes update 就能拿到;只跟着发行版走的部署要等下一个 tag。
  • 不需要改任何配置。这次没有新增开关,全是行为修正。
  • 如果你用超长会话跑批处理任务,判断「模型是不是被压缩坑了」的一个简单信号是:它说没看到某条工具结果,但对话里明明执行过。这种矛盾以前需要翻源码才说得清,现在应该见不到了。

小结

回合内压缩从「除最新之外一律降级」变成了「模型还没读到的那一轮不许碰」;省略标记从各写各的变成了统一走守卫;读缓存和正文重新对齐。三件事都指向同一个原则:压缩可以牺牲旧信息,但绝不能牺牲模型正在使用中的信息。 想继续往下挖,可以先看一次压缩请求代替十九次讲的速度问题,再看四十轮压缩之后会话还剩什么的实测数据,以及瘦尾压缩的取舍。v0.21.5 这一窗口里还有哪些变化,见本站的 v0.21.5 发版页。