日常:hermes上下文750K失控,无法自行压缩

结论

这是一个严重缺陷,不是用法问题。hermes 的上下文压缩在会话超过某个临界点后进入发散循环:每次压缩尝试都因摘要写不完而整体回滚、消息一条不删,会话继续长大,下一次更写不完。20261013_125040_a801a3(nixos v0.2.0 五项进度,deepseek-v4.1-flash,1M 窗)就是卡死在这个循环里的完整现场:576→644 条消息,66 万→89.6 万 token,自动手动各死多次,prompt 实测最高 779,970 token,离硬顶只剩约 20 万。调参(threshold 0.4)只能把临界点往后推,消灭不了它。不 new 也一定能解决:技术上就是让压缩收敛(分片、递归、预算内),不存在原理性不可能;拿 new 当答案等于把子系统的正确性义务转嫁给用户的操作习惯。

现场时间线(实测)

时间触发输入规模结果
23:09–23:10 自动preflight—冷却中跳过(剩余 98s / 36s)
23:11 自动automessages=576,tokens≈767,37241 秒后摘要截断 abort
23:14 自动post_toolmessages=600,tokens≈789,031;middle 439K token,539 条记录进摘要42 秒后同样截断 abort
23:15 手动/compressmessages=605,tokens≈663,361;middle 434K41 秒后同样截断 abort
23:31 手动(桌面,带主题)/compress 聚焦主题focus=kvm优化和nixos继续完善细节38 秒后同样截断 abort
23:33 网关重启hermes gateway restart新 PID 766120,Telegram 重连正常0.4 生效(日志出现 >= 400,000)
23:35 手动(CLI 注入)/compresshistory=603preflight 因无真实用量锚点 defer;命令被当普通对话,模型陪聊回一段小结,压缩器一次没启动
23:35 后—messages=644,tokens≈896,107;真实 prompt 779,970继续涨,锚点已建立,下一轮会自动再试大概率再死

根因链(五步,每步都有判据)

1. 单遍摘要架构,没有分片。每轮压缩把整个 middle window 一次塞进一个摘要调用。遥测里 chunking:false,chunk_count:1 恒成立,代码里没有 map-reduce 或分段压 middle 的路径。middle 43 万 token 时,输入靠 lean 采样截到约 158K 字符(_SUMMARY_INPUT_MAX_CHARS=160_000),但输出侧没有对应的缩放机制。

2. 输出预算顶死 10K,撞的是提供商侧上限。摘要预算 = 内容×0.20,上限 min(上下文×0.05, 10_000),1M 模型即 10K token。调用故意不传 max_tokens(注释:硬 cap 会截断摘要),于是顶在 opencode-go 那一侧:finish_reason=length。aux 实测 prompt 71–72K token,写到顶没写完。

3. 截断被定为 terminal failure,整轮回滚。与空内容、拒答同级(_TERMINAL_SUMMARY_FAILURES):abort,消息原样保留。这是为了不让半截摘要覆盖真记忆,代价是失败不产生任何进度——605 条进去,605 条出来。

4. 失败写冷却,冷却期内自动跳过。_SUMMARY_FAILURE_COOLDOWN_SECONDS=600,实测叠加后日志里出现 700–900 秒剩余。用户看到的就是「自动上下文控制失效」。手动 /compress 走 force=True 先清冷却所以真跑了,但撞的是同一堵墙——「手动发起压缩失效」。

5. 发散:越失败越大,越大越失败。每次 abort 后新对话继续进 middle,539 条记录→更多,下一次摘要更长。这是一个不收敛的循环:压缩能力不随会话长度伸缩,存在一个过了就永久失控的临界点。本次现场临界点大约在 60 万 token / 600 条消息附近(首死于 576 条 767K)。

为什么调参救不了

参数值作用结论
threshold(本模型)0.4(23:30 你亲手改的,已 live)触发线 500K→400K,早压、每次 middle 小一点推迟临界点,不消除;当前 middle 已 43 万,压完一次还是超输出顶
threshold(全局)0.75其它模型的触发线与本次无关
target_ratio名义 0.1lean 模式下是死旋钮(只在 legacy tail 用,且被钳在 [0.10, 0.80],0.05 写了也按 0.10 生效)改了等于没改,保持别碰
压缩模型opencode-go / deepseek-v4.1-flash,reasoning_effort none输出顶在这一侧唯一从顶侧下手的变量是换摘要模型(输出上限更大的),需另议
底座+tail(25K+41K≈66K,占 1M 约 6–7%,且底座走缓存价)—固定开销合理,别动;病根在 middle,不在这 66K

修法方向(收敛性改造,不是调参)

  • 分片摘要:middle 按 token 切段分别摘要再合并(map-reduce),每片的输出独立封顶,段数随 middle 增长,总输出不再顶穿单次上限。这是根治单遍架构的正面解法。
  • 递归压摘要:固定单次摘要长度上限,超了就把摘要本身再压一遍;previous summary + new turns 的迭代形式已存在,缺的是「输出超限时回退到再压一次」而不是直接 abort。
  • 预算内 middle:触发时若 middle 超过「可写完」的规模,先按时间切多轮压(每轮只压最老一段),而不是一轮吃全部。0.4 是这个方向的弱版本(只提前触发,不限制单轮规模)。
  • 降级路径可选:abort_on_summary_failure=false 时走静态 fallback(丢 middle 但会话能继续),现在默认 abort 是保记忆、舍继续;至少应让用户在「保记忆」和「能继续」之间有选择,而不是看起来像死机。
  • 冷却期诚实上报:冷却内自动跳过时只在 debug 日志里写,用户侧显示的是上下文继续涨。应在使用量面板给出「压缩失败×N,下次重试 HH:MM」的可判定状态,而不是让用户猜。

为什么必须修,而不是 new

new 绕过的是这一次失控,不是临界点的存在。任何跨天、跨数百轮的长任务(正是这个 nixos 五项进度会话的形态)都会走到 600 条 / 60 万 token 以上——到了就死,new 只是把计时器清零重跑一遍。把「用户必须自己定期 new + 自己做交接」当成答案,等于要求用户替压缩子系统做它本该做的收敛保证,这就是侵入操作习惯。判据也很直接:修完之后,同一会话从 60 万涨到 90 万的过程中压缩应该成功至少一次且 prompt 回落到触发线附近;达不到这一点,1M 上下文的标称值对长任务就是虚的。

复核命令

# 最近几次压缩尝试(触发源/规模/成败一眼可见)
grep -n 'compression' ~/.hermes/logs/agent.log | grep '20261013_125040_a801a3' | tail -n 6
 
# 当前规模与失败状态
python3 -c "import sqlite3;db=sqlite3.connect('/home/cat/.hermes/state.db');print(db.execute(\\"select message_count, compression_failure_cooldown_until, compression_failure_error from sessions where id='20261013_125040_a801a3'\\").fetchone());print(db.execute(\\"select count(*), sum(length(content)) from messages where session_id='20261013_125040_a801a3'\\").fetchone())"
 
# 配置快照
sed -n '54,61p' ~/.hermes/config.yaml

关键代码位置:agent/context_compressor.py(单遍入口 5275 force 清冷却、截断判定 3970、预算 3468/2248、冷却 600 秒见 1123、terminal 失败表见 748);遥测字段 trigger_source / middle_window_tokens / summary_input_record_count / commit_status:aborted / failure_class:summary_truncated_failure。