专题篇章 · 拆开 DeepSeek Harness

Compaction 双路径与 replaceGeneration

压力触发与溢出恢复两条正交路径,以及用世代号证明「压缩确实发生过」才允许重试

本页解决的问题

先给结论

「Compaction 双路径与 replaceGeneration」要解决的关键问题是什么?

压力触发与溢出恢复两条正交路径,以及用世代号证明「压缩确实发生过」才允许重试

判断标准

让这个结论先证明自己值得留下。 把这一页当成决策工具,而不是需要背下来的定义。把概念连到一个真实任务、一个可观察结果,以及一个能改变你判断的失败上。

下一步

写下一个问题:试完这个方法后,你能用什么证据回答它?

常见误区

结论听起来很完整,却没有检查最关键的假设。

课程目标读完你能说清两个思路:DSH 为什么把自动压缩拆成主动和被动两个触发器,各管一段、互不重叠;以及溢出后允许重试的凭证,为什么是 replaceGeneration 这个只增不减的世代号,插件自己的返回值为什么不算数。
交互演示 · 行李箱收纳模拟器

把上下文窗口想成一只行李箱:每条消息是一件衣物,虚线是八分满警戒线。左下角的收拾次数就是 replaceGeneration(世代号),只增不减。三个情景对应压缩的三种命运,每一步都有字幕解说。

八分满线(阈值 0.8)
请求被拒 · CONTEXT_WINDOW_EXCEEDED
收拾次数 replaceGeneration3
选一个情景点播放,或滚动到此处自动播放情景 A。
逻辑轨迹(行号对应 compaction-basic/src/index.ts,动画走到哪一步,哪一行亮)
PRESSURE 路径
on('agent/pre-step')L147
measure().totalTokens ≥ thresholdTokens ?L304
剪枝 → 摘要 → surface 替换L308-323
CONTEXT-OVERFLOW 路径
on('agent/request-error')L179
code ≠ CONTEXT_WINDOW_EXCEEDED → next()L183
generation = surface.replaceGenerationL191
compactIfNeeded('context-overflow')L194
replaceGeneration > generation ?L218-219
是 → return { kind: 'retry' }L222
否 → return next(),保留原始错误L219
演示是教学化模拟:容量条、衣物块与世代号都是课程化抽象。情景 C 里谎报成功的压缩后端是教学假设,真实的 compaction-basic 不会谎报,只是 compaction 是一个开放接缝,第三方后端接进来之后什么都可能发生,世代号对账防的就是它们。出处:packages/compaction/compaction-basic/src/index.ts 第 147 至 223 行。
设计思路一 · 收拾东西要分两个触发器

它解决什么问题

假设只做主动阈值这一条路:每次请求前量一下,超过八成就收拾。听起来够了,实际不够。token 数是估出来的,估算和 provider 的真实计数总有出入。一条超大的工具结果突然塞进来,测量还没到阈值,请求已经超限,provider 直接拒绝。这时候没有任何补救逻辑接手,turn 就地报错终止,用户看到的是一次莫名其妙的失败。

一次算错就直接撞墙,没有第二道防线。这就是单触发器的问题。

思路是什么

DSH 把这件事拆成两个独立的触发器。快满了主动收,撞墙了被动救,两条路挂不同的事件、用不同的条件、有不同的失败语义。

pressure 路径挂在每个 Step(一次模型请求)开始之前。先量总 token,超过容量的 0.8 就动手收拾,收拾时给最近的对话留 16% 的原文尾巴。它的失败语义很松:收拾中途出了错,日志里记一句就继续走,提前收拾失败了天塌不下来。

context-overflow 路径挂在请求报错之后,只认适配器规范化过的错误码 CONTEXT_WINDOW_EXCEEDED,其他错误一律放行。它不看阈值,保留预算直接清零,强制做一次真实的缩减。它的失败语义很严:必须给出决定,要么重试,要么保留原始错误上报。重试上限默认 1 次,每收到一条成功的模型回复就清零计数,正常干活的会话不会被卡住。

同一件事,两个触发器,各管一段 快满了(请求前测量) 总 token 超过容量的 80% 主动收拾 留 16% 最近对话原文 旅程继续 失败只记一句日志 撞墙了(请求被拒) CONTEXT_WINDOW_EXCEEDED 强制收拾 不看阈值,能压全压 对账后决定 重试,或上报原始错误
上排是预防,下排是兜底。两条路各自独立,一条失效了另一条照常工作。

出处:pressure 监听器在 packages/compaction/compaction-basic/src/index.ts 第 147 至 165 行,overflow 监听器在第 179 至 223 行;0.8 与 0.16 两个默认值在同包 config.ts 第 20 与 23 行,重试上限默认 1 次在第 93 行,都可按 provider 加 model 的组合逐一覆盖。

为什么长期成立

把预防和兜底分开,是可靠性工程的通则。备份和恢复是两套系统,限流和熔断是两道闸门,道理相同:预防路径追求便宜、常跑、失败无所谓;兜底路径追求可靠、少跑、失败必须有交代。这两种诉求塞进同一段逻辑里必然互相迁就。所以哪怕换个语言重写整个 harness,只要模型有上下文上限、token 靠估算,这两个触发器就都得在。

设计思路二 · 重试要出示证据

它解决什么问题

撞墙之后收拾了一次,接下来要重发请求。问题是:怎么确认收拾真的起效了?compaction 是一个开放接缝,第三方可以接自定义后端。假设某个后端每次都报告成功,但从来没真正改过模型可见的内容:如果只看返回值就重试,请求原样超限,再报错,再压缩,再重试,每一圈都是白花的 API 钱,死循环烧到天亮。

思路是什么

DSH 的答案是一个世代号。先说背景:surface 是会话日志里模型可见事件的实时投影,可以理解成模型眼里的那份对话。replaceGeneration 是它身上的一个只读计数器,记的是这份对话被替换过几次。整个代码库只有一处会让它加一:一段旧消息真的被摘要替换、真的落盘的那一刻。没有任何 API 能把它改小或重置。所以世代号前进了,在数学上等价于至少发生过一次真实的、已落盘的替换。

溢出恢复的用法就三步:动手收拾之前先拍快照记下当前值;收拾;收拾完拿新值和快照比。严格变大才允许重试,否则放行,原始错误原样上报。插件说什么不重要,账本上的数字变没变才重要。

重试前先看收拾次数有没有变 动手前拍快照 收拾次数 = 3 收拾一次 交给压缩后端执行 再看一眼计数 现在是几? 变成 4,箱子确实动过 允许重试 还是 3,收拾没起效 拒绝重试,上报原始错误
计数只增不减,全库只有替换真正落盘的那一处会加一,所以数字变大就是硬证据。

还有一个反方向的细节。就算收拾中途抛了异常,只要前面的免费剪枝已经落盘、世代号已经前进,这份进展照样够格授权重试。凭证据放行,凭证据拒绝,两边用的是同一条标准。

世代号没前进,一次重试都不放行。

出处:快照与比对在 packages/compaction/compaction-basic/src/index.ts 第 191 行与第 218 至 222 行,异常后凭已落盘进展重试在第 195 至 208 行;replaceGeneration 的定义在 packages/core/session/src/surface.ts 第 136 至 142 行,全库唯一的加一处在第 361 至 371 行。项目的 Agent Note(.agents/notes/implemented/architecture/2026-07-10-after-call-compaction-pressure-and-overflow-recovery.zh.md)明确否决过只看返回值的写法,理由是自定义后端可能报告成功却没有改变模型可见状态。

为什么长期成立

用单调递增的版本号证明状态确实变了,这个套路数据库的乐观锁用了几十年,Git 的 commit 链、分布式系统里的 epoch 也都是它的变体。它的好处是把信任问题变成算术问题:执行者可以撒谎,账本不会。只要系统里存在不受信任的扩展点,重试之前核对一个改不了的计数器,永远是最便宜的防线。

横向对比 · 三家怎么防烧钱
Grok Build

主动阈值路径和 DSH 的 pressure 同构:默认 85% 触发,另有默认关闭的 two-pass 预摘要,细节见站内 Compaction:85% 阈值与可选 two-pass。请求报错后的被动恢复加世代号对账这条路,在已核对的 Grok Build 材料里没有见到等价机制。这一条基于已公开证据,保留未知项。

Claude Code

主动方向做得最厚,每次调 API 前要过裁剪、微压缩、折叠、全量摘要四道工序。防烧钱的答案是计数熔断:自动压缩连续失败 3 次就停手。这个 3 来自真实事故,源码注释记载曾有 1279 个 session 连续失败 50 次以上,全球每天浪费约 25 万次 API 调用。出处:claude-code-sourcemap-main/study/chapters/03-context-management.md 第 78 至 81、121 至 124 行。

对比焦点就一个:压缩失败会不会循环烧钱。Claude Code 数失败次数,数到 3 就熔断,止损线是拿事故数据校准出来的,属于计数器止损:先允许问题发生几次,再靠上限兜住。DSH 不数次数,要求每次重试都出示世代号前进的证据,一次无效重试都不放行,属于结构性证明:让无效重试从机制上发不出去。前者的 3 需要事故喂出来,后者的 0 是推导出来的。再加上双路径正交这一点,这两处是 DSH 在三家对比里独有的设计。

课堂练习
01

手推一个谎报成功的后端

设重试上限为 1,装一个自定义压缩后端:每次都报告收拾成功,但从不真正替换模型可见的内容。现在第一次溢出报错发生了。

问题一:DSH 会发起第二次压缩尝试吗?提示:对账失败后原始错误直接上报,turn 就结束了,重试计数根本没机会增加。

问题二:如果把判定标准改成只看后端的返回值,同一场景下每圈耗时 5 秒,第一分钟会发出多少次注定失败的请求?Claude Code 的 3 次熔断又会在第几次请求后止损?

Takeaway:快满了主动收,撞墙了被动救,预防和兜底各走各的触发器。溢出重试的凭证是 replaceGeneration 的单调前进,插件的返回值不算数。要证据,别信口头汇报。

「交互演示 · 行李箱收纳模拟器」的能力藏在每次交接里

「把上下文窗口想成一只行李箱:每条消息是一件衣物,虚线是八分满警戒线。左下角的收拾次数就是 replaceGeneration (世代号),只增不减。三个情景对应压缩的三种命运,每一步都有字幕解说」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。

先写清状态,再增加能力

从「假设只做主动阈值这一条路:每次请求前量一下,超过八成就收拾。听起来够了,实际不够。token 数是估出来的,估算和 provider 的真实计数总有出入。一条超大的工具结果突然塞进来,测量还没到阈值,请求已经超限,provider 直接拒绝。这时候没有任何补救逻辑接手,turn 就地报错终止,用户看到的是一次莫名其妙的失败」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。

成功路径不能代表系统可靠

用「问题二:如果把判定标准改成只看后端的返回值,同一场景下每圈耗时 5 秒,第一分钟会发出多少次注定失败的请求?Claude Code 的 3 次熔断又会在第几次请求后止损」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。

从「交互演示 · 行李箱收纳模拟器」走到「它解决什么问题」

「交互演示 · 行李箱收纳模拟器」先把问题落在「把上下文窗口想成一只行李箱:每条消息是一件衣物,虚线是八分满警戒线。左下角的收拾次数就是 replaceGeneration (世代号),只增不减。三个情景对应压缩的三种命运,每一步都有字幕解说」上;到了「它解决什么问题」,讨论继续推进到「假设只做主动阈值这一条路:每次请求前量一下,超过八成就收拾。听起来够了,实际不够。token 数是估出来的,估算和 provider 的真实计数总有出入。一条超大的工具结果突然塞进来,测量还没到阈值,请求已经超限,provider 直接拒绝。这时候没有任何补救逻辑接手,turn 就地报错终止,用户看到的是一次莫名其妙的失败」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。

把这条判断带到下一个场景

分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。

  • 「交互演示 · 行李箱收纳模拟器」:把上下文窗口想成一只行李箱:每条消息是一件衣物,虚线是八分满警戒线。左下角的收拾次数就是 replaceGeneration (世代号),只增不减。三个情景对应压缩的三种命运,每一步都有字幕解说
  • 「它解决什么问题」:假设只做主动阈值这一条路:每次请求前量一下,超过八成就收拾。听起来够了,实际不够。token 数是估出来的,估算和 provider 的真实计数总有出入。一条超大的工具结果突然塞进来,测量还没到阈值,请求已经超限,provider 直接拒绝。这时候没有任何补救逻辑接手,turn 就地报错终止,用户看到的是一次莫名其妙的失败
  • 「最后的要点」:出处:pressure 监听器在 packages/compaction/compaction-basic/src/index.ts 第 147 至 165 行,overflow 监听器在第 179 至 223 行;0.8 与 0.16 两个默认值在同包 config.ts 第 20 与 23 行,重试上限默认 1 次在第 93 行,都可按 provider 加 model 的组合逐一覆盖

最后的「最后的要点」把讨论落到「出处:pressure 监听器在 packages/compaction/compaction-basic/src/index.ts 第 147 至 165 行,overflow 监听器在第 179 至 223 行;0.8 与 0.16 两个默认值在同包 config.ts 第 20 与 23 行,重试上限默认 1 次在第 93 行,都可按 provider 加 model 的组合逐一覆盖」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。

标记为已学完 阅读进度会自动记录
← 上一篇下一篇 →

继续阅读

同一条线上的下一篇。

文章讨论

读到这里,留下一个判断。

把刚想明白的地方、还没想通的问题,留给下一位一起学习的人。

正在讨论 Compaction 双路径与 replaceGeneration 拆开 DeepSeek Harness
3条讨论文章讨论 · 与共学社区同步
在共学社区查看
AM
Asha Morgan内容编辑
观点实践记录

我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。

文章讨论7 有帮助
LH
Lin Harper独立开发者
观点观点

读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。

文章讨论5 有帮助
KM
Kiki Moore产品运营
问题问题

如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。

文章讨论4 有帮助