学 AI 产品 · 专业 AI 产品经理播客第 4 章 · T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座 · EP 06
第 4 章 · EP 06

Compaction 双路径与 replaceGeneration 等 2 节

时长 15:21音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 两套数各干各的1:37
1:37收拾东西要分两个触发器2:53
4:30重试要出示证据1:51
6:22凭证据放行,也凭证据拒绝1:28
7:50三家怎么防烧钱1:18
9:09为什么是两套计量1:53
11:02投影的巧思 · 答下一次不答上一次1:15
12:18分子分母不构成原子对1:42
14:00可带走的原则1:20
解读全文

ep38 · Compaction 双路径与 replaceGeneration 等 2 节

本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
模块:T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座
来源:xueai.miyang.cn(小山学堂 · 洛小山)

本集为两节合辑(素材 8006 字),已做取舍。

统一主线:两节都在讲同一件事 —— 不同消费方要用不同的数,别让一个口径伺候所有人。

节决策不信什么决策信什么
压缩双路径插件的返回值账本上的世代号
Token 计量界面上的百分比自己在边界上现算的数

一句话速览

上下文快满了就主动收拾,真撑爆了就先收拾再重试。重试前先对一遍世代号,收拾没起效就不许重试。

决策用重放(measure()),展示用投影(projectedTokens)。


能力地图

两条压缩路径(正交)

pressure(预防)context-overflow(兜底)
挂载点agent/pre-step,每个 Step 开始前agent/request-error,请求报错之后
触发条件measure().totalTokens ≥ thresholdTokens错误码 恰为 CONTEXT_WINDOW_EXCEEDED
阈值容量的 0.8不看阈值,保留预算直接清零
保留尾巴给最近对话留 16% 原文强制做一次真实缩减
失败语义松 —— 记一句日志就继续走严 —— 必须给出决定(重试 or 保留原始错误)
重试上限—默认 1 次,每条成功回复清零

出处:compaction-basic/src/index.ts L147–165、L179–223;config.ts L20、L23、L93(均可按 provider+model 组合覆盖)。

世代号对账(三步)

  1. 动手收拾之前先拍快照:generation = surface.replaceGeneration;
  2. 收拾;
  3. 拿新值和快照比 —— 严格变大才允许重试,否则放行、原始错误原样上报。
插件说什么不重要,账本上的数字变没变才重要。

出处:index.ts L191、L218–222;surface.ts L136–142(定义)、L361–371(全库唯一加一处)。

两套 Token 计量

重放 measure()(决策用)投影 projectedTokens(展示用)
回答什么此刻发请求会有多大给用户看个占用率
要求必须准,可以贵必须便宜、持久、重连后立即可用
代价每次 O(surface)普通持久投影,便宜
谁用只有压缩这类决策方,在自己的请求边界调UI 状态行

设计思路一 · 收拾东西要分两个触发器

问题 · 单触发器的缺口

只做主动阈值:每次请求前量一下,超过八成就收拾。听起来够了,实际不够 —— token 数是估出来的,估算和 provider 真实计数总有出入。

一条超大的工具结果突然塞进来 → 测量还没到阈值,请求已经超限,provider 直接拒绝 → 没有任何补救逻辑接手,turn 就地报错终止,用户看到的是一次莫名其妙的失败。
一次算错就直接撞墙,没有第二道防线。

提示1 · 两种诉求不能塞进同一段逻辑

上排是预防,下排是兜底。 两条路各自独立,一条失效了另一条照常工作。

这是可靠性工程的通则:备份和恢复是两套系统,限流和熔断是两道闸门。预防路径追求便宜、常跑、失败无所谓;兜底路径追求可靠、少跑、失败必须有交代。

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

问题 · 谎报成功的后端

compaction 是一个开放接缝,第三方可以接自定义后端。假设某个后端每次都报告成功,但从来没真正改过模型可见的内容:

只看返回值就重试 → 请求原样超限 → 再报错 → 再压缩 → 再重试 —— 每一圈都是白花的 API 钱,死循环烧到天亮。

机制 · 世代号

surface 是会话日志里模型可见事件的实时投影(模型眼里的那份对话)。replaceGeneration 是它身上的一个只读计数器,记这份对话被替换过几次。

整个代码库只有一处会让它加一:一段旧消息真的被摘要替换、真的落盘的那一刻。没有任何 API 能把它改小或重置。

世代号前进了 ⟺ 至少发生过一次真实的、已落盘的替换(数学等价)。

提示2 · 反方向细节:异常也算,只要证据在

就算收拾中途抛了异常,只要前面的免费剪枝已落盘、世代号已前进,这份进展照样够格授权重试。

它不看过程是否顺利,只看账本上有没有真实进展。异常也算,只要证据在;反过来,过程顺利但账本没动,也不放行。两边用的是同一条标准 —— 凭证据。

出处:index.ts L195–208。

提示3 · 手推一个谎报成功的后端

设重试上限为 1,装一个自定义后端:每次都报告成功,但从不真正替换。第一次溢出报错发生了。

  • DSH 会发起第二次压缩尝试吗? 不会 —— 世代号不变,对账失败,原始错误直接上报,turn 就结束了,重试计数根本没机会增加。
  • 若改成只看返回值,每圈 5 秒 → 第一分钟发出 12 次注定失败的请求。

为什么长期成立

用单调递增的版本号证明状态确实变了 —— 数据库乐观锁用了几十年,Git commit 链、分布式系统的 epoch 都是它的变体。

把信任问题变成算术问题:执行者可以撒谎,账本不会。 只要系统里存在不受信任的扩展点,重试之前核对一个改不了的计数器,永远是最便宜的防线。

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

主动路径防循环烧钱
DSHpressure(0.8)+ overflow 兜底世代号对账,0 次无效重试
Grok Build主动 85% 触发,另有默认关闭的 two-pass 预摘要已核对材料中未见等价机制(保留未知项)
Claude Code最厚:裁剪 / 微压缩 / 折叠 / 全量摘要四道工序计数熔断:连续失败 3 次停手

提示4 · 两种止损的差别

3 来自真实事故:源码注释记载曾有 1279 个 session 连续失败 50 次以上,全球每天浪费约 25 万次 API 调用。出处:study/chapters/03-context-management.md L78–81、L121–124。

类型止损线来源
Claude Code计数器止损 —— 先允许问题发生几次,再靠上限兜住拿事故数据校准
DSH结构性证明 —— 让无效重试从机制上发不出去推导出来的 0
前者的 3 需要事故喂出来;后者的 0 是推导出来的。

第二节 · Token 计量:决策用重放,展示用投影

为什么是两套

决策用的数要准,可以贵。展示用的数要便宜,可以糙。
  • 压缩决策要回答「此刻这个会话如果发请求会有多大」→ 答案必须准,可以贵;
  • UI 状态行只要给用户看个占用率 → 必须便宜、持久、重连后立刻能显示,准到小数点没有意义。

重放路径 measure()

每次调用把持久日志的当前尾部折叠成一份不可变快照:

  1. 最近一次成功请求的 provider usage 若能匹配当前请求信封、且总量不低于它的完整启发式锚点 → 拿来当锚;
  2. surface 此后的增减用有符号 delta 重新定价;
  3. 没有可复用的锚 → 整体按固定启发式定价。

代价:每次 O(surface) → 只有压缩这样的决策方在自己的请求边界调它。

投影路径 projectedTokens

普通持久会话投影状态,只有两个各自后者胜的字段:

字段含义出处
pressureTokens最近一次请求报告的提示词侧规模(输入 + 缓存读写,不含输出)usage-projection.ts L70–72
contextWindow来自最新一条 request/context 日志记录同文件

分子分母各写各的,从不凑成一次原子观测。

提示5 · 投影的巧思:答下一次,不答上一次

光有 pressureTokens 有个尴尬:它只在请求报 usage 时更新,Turn 流式期间一动不动,更看不见压缩 —— 压缩替换了一大段 surface,状态行上的数却纹丝不动,用户会以为压缩没干活。

所以 fold 顺手带一份 surface 的运行总量,公布的是样本加上此后 surface 的有符号变动。

源码注释写得很直白:occupancy answers for the next request rather than the last one(占用率回答下一次请求的大小,而不是上一次)。
效果:压缩刚落盘、一个新请求都没发,投影已经掉下来了。

时序细节:usage 样本在同一事件加入 surface 之前盖章(stamped BEFORE),所以 assistant/message 锚定的是它自己那次请求看到的 surface,增量的起点不会错位。

出处:usage-projection.ts L150–161。

关键证据 · 投影公式原文(7 行)


view: ({ contextWindow, pressureTokens, surfaceTokens, sampledSurfaceTokens }) => ({
  ...contextWindow === undefined ? {} : { contextWindow },
  ...pressureTokens === undefined ? {} : { pressureTokens },
  ...pressureTokens === undefined || sampledSurfaceTokens === undefined
    ? {}
    : { projectedTokens: Math.max(0, pressureTokens + surfaceTokens - sampledSurfaceTokens) },

题眼是第三个展开项:pressureTokens + surfaceTokens - sampledSurfaceTokens —— 样本加上取样之后 surface 的净变动,再用 Math.max(0, …) 兜住下界。两个来源字段缺一个,对应的输出干脆不出现。

出处:usage-projection.ts L198–204。


两处边界

边界一 · provider 不回 usage

  • 投影侧:pressureTokens 一直缺席 → 第二、三个展开项都不出现 → UI 干脆不显示占用率(Agent Note 明确:只有压力与容量都已知时才显示占用率);
  • 决策侧不受影响:measure() 退化为 estimated 启发式锚,照常给数。

提示6 · 边界二:分子分母不构成原子对

换模型时,新 contextWindow 立刻生效,pressureTokens 还是上一个路由的旧样本 → 占用率此刻是近似值,直到下一个请求报 usage。

文档明说这是取舍,不当 bug 处理。辩护词原文(设计笔记 L35):
*确实需要同一边界精确数字的消费方,应在自己的请求边界调用 ctx.tokenMeter.measure(),那里两个值同时可得,而不是读取该投影。*

口径提醒

pressureTokens 只算提示词侧(inputTokens + 缓存读写,不含输出)—— 描述的是发出去的请求有多大,和计费总量 tokenUsage 是两个投影单元,别混。

⚠️ 最要紧的一句:harness 中没有任何环节依据占用率百分比做决策,压缩直接读取 measure()。UI 上那个数再漂亮,也进不了决策函数的参数表。

横向对比 · 一套数的活法和两套数的活法

口径特点
Grok Build一个数走天下 —— 专门开纯函数 crate 当唯一口径决策与展示天生一致,永远不打架;代价是决策也只能用粗启发式(BYTES_PER_TOKEN = 4,图片一张记 765),provider 真实 usage 不参与占用率计算
Claude Code展示口径自己也要防坑子 Agent 进度的 input_tokens 是逐轮累计(只留最新)、output_tokens 是增量(累加)—— 直接相加会把 input 重复计好几遍;压缩阈值用缓冲区常量(autocompact 预留 13000、手动 /compact 预留 3000)
DSH彻底分开决策读 measure(),展示读投影,并在文档里承认展示那份就是近似值

出处:xai-token-estimation/src/lib.rs L3–19;study/chapters/06-task-system.md L169–181。

三家其实答了同一道题:占用率这个数给谁用、错了谁买单。 Grok 选了永远一致的粗数;Claude Code 给展示计数打了防重复的补丁;DSH 把两个消费方彻底分开,然后在文档里承认展示那份就是近似值。

课堂练习

  1. 手推一次分叉现场:压缩刚落盘,surface 从 92k 缩到 8k,还没有任何新请求;用户紧接着把模型从 100k 窗口切到 200k 窗口。此刻回答四个数:UI 状态行显示的分子和分母各是多少、来自哪个字段;压缩决策方如果此刻调 measure(),拿到的总量大约是多少、锚点是哪一类。最后用设计笔记 L35 那句原文,解释这两组数为什么允许不相等、真需要精确值的消费方该怎么办。
  1. 设重试上限为 1,装一个自定义后端:每次报告成功但从不真正替换。DSH 会发起第二次压缩尝试吗?

审查清单

  • 压缩是否拆成了主动 + 被动两个独立触发器?
  • 预防路径的失败语义是否松、兜底路径是否严?
  • 溢出重试前是否核对了世代号严格变大?
  • 是否存在「只看插件返回值就重试」的路径?
  • 世代号是否只增不减、且只有真实落盘那一处会加一?
  • 重试上限是否会在成功回复后清零?
  • 决策用的 token 数是否来自重放 measure() 而非投影?
  • 是否有任何决策读了 UI 上的占用率百分比?
  • 投影是否回答「下一次请求」而非「上一次」?
  • pressureTokens(不含输出)与 tokenUsage(计费总量)有没有被混用?
  • 文档里是否为「占用率是近似值」写好了辩护词与出路?

排查路径

  1. 压缩后 UI 数字纹丝不动 → 查投影是否带了 surface 运行总量(应回答下一次)。
  2. 超限后反复重试烧钱 → 查世代号对账是否缺失。
  3. 换模型后占用率突然异常 → 这是设计内的非原子分叉,等下一个 usage 即可。
  4. provider 不回 usage 时占用率消失 → 设计内行为(压力与容量都已知才显示)。
  5. 决策用了错的 token 数 → 查是否读了投影而非 measure()。

约束说明

  1. 取材约束:本集全部内容取自小山学堂《学 AI 产品,从入门到精通》对应课节,未跨集取材,未虚构源码行号或产品行为。
  2. 取舍说明:两节合计 8006 字,口播稿按 4600–5300 字容量取舍,保留最能带走的原理与踩坑点,未为凑字数硬扩。
  3. 题库隔离:题库页(quizFiles)一律不进入口播稿,仅作为集页下方的文字自测卡渲染。
  4. 源码时效:依据本地仓库 deepseek-harness-master,核对日期 2026-08-13;横向对比部分基于已公开材料,未知项已明确标注。
  5. 演示说明:原文交互演示的容量条、衣物块与世代号为教学化抽象;情景 C 里「谎报成功的压缩后端」是教学假设 —— 真实的 compaction-basic 不会谎报,但 compaction 是开放接缝,第三方后端接进来之后什么都可能发生,世代号对账防的就是它们。
  6. 解读边界:本文为二次演绎的解读稿,用于配合音频理解;具体行为以实际运行版本为准。

Takeaway

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

决策用重放,measure() 在自己的边界现算,准而贵;展示用投影,pressureTokens 加 surface 净变动,糙而便宜持久。 两个数可以不一样,占用率的非原子性是写进文档的设计决策。看到 UI 上的百分比,记住没有任何决策读它。


*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*