close

Volume 2 · Chapter 21

压缩与上下文:surface replace 的消费者

06 页见过 SurfaceOp: replace 的机制(compaction 用它在 surface 上遮蔽历史)。这一章看 producer 侧:谁触发压缩、压缩出的摘要如何变成 replace 事件、token 计量如何驱动它。它还是 runMaintenance(05 页三态之一)的最典型调用者。

(卷一连接点) 05 页 maintenance 相 · 06 页 surface replace 本域:compaction / compaction-basic / token-meter / tool-result-pruner (回心脏) replace 后 deriveMessages 重建(06 页 replaceGeneration)

示例本次示例:长对话触发压缩的完整过程

示例轨迹 21-1 · 触发 → 摘要 → replace → 重建
# 假设我们的会话聊了 50 个 turn,token-meter 累计 100k(超过配置阈值 80k)
# ① 触发:token-meter 发通知 → compaction 走 agent.runMaintenance()(05 页 142 行)
#    phase 切 maintenance(对外仍 idle)——期间的 wake 被 latch(05 页 178 行)
# ② 摘要:compaction-basic 用 LLM 生成摘要(inject 里的 llm → 10 页同一条 llm/stream)
# ③ 落 replace 事件(假设施加在 seq 200):
{ "type":"user/message", "seq":200, "time":..., "surfaceOp":{ "op":"replace","start":2,"end":150 },
  "sourceEventSeqs":[2,5,8,...,150],   ← 必须覆盖每一个被遮蔽节点
  "data":{ "content":[{"type":"text","text":"[前情摘要] 用户要整理目录,已完成..."}] } }
# ④ surface 校验(06 页 assertProvenance):start/end 都在 surface 上、seq 全集 ✓
# ⑤ replaceGeneration+1 → deriveMessages 缓存重建(06 页 726 行)
# ⑥ 下一个请求的 boundaryMessages 里,seq 2-150 被一条摘要替代(09 页 507 行)
来源:21 页 §3 + 06 页 surface replace 校验 + 05 页 maintenance
示例轨迹 21-2 · 人类与模型看到的差异
# 模型(deriveMessages 的 surface):... 摘要 ... → 上下文大幅缩短
# 人类(isAppendSurfaceEvent 过滤):50 个 turn 的原文全部还在日志里
# UI 渲染 transcript 时用 append-origin 过滤 → 用户还能回看全部历史
# 这就是 06 页「surface 是模型可见投影,不是人类 transcript」的实例
来源:06 页 isAppendSurfaceEvent + 21 页易错点

§1挂载条目

  • compaction-basic → @deepseek-ai/dsh-compaction-basic(基础引擎实现)
  • token-meter → @deepseek-ai/dsh-token-meter(1027 行)——用量计量,压缩的触发依据
  • command-compact → @deepseek-ai/dsh-command-compact(136 行)——/compact 用户命令
  • spill-local / spill-policy → dsh-spill-local / dsh-spill-policy——工具输出溢出到磁盘的闸门(13 页 tail-keep 的姊妹机制)
  • tool-result-pruner → dsh-compaction-tool-result-pruner——工具结果的裁剪

§2包文件地图

规模角色
packages/compaction/compaction/6 文件 / 792 行Service DefinitionCompactionEngine extends Service(src/index.ts:96)——抽象压缩引擎
packages/compaction/compaction-basic/6 文件 / 1621 行Providerstatic inject = ['llm', 'tokenMeter', 'sessions'](src/index.ts:104)——用 LLM 生成摘要的引擎
packages/llm/token-meter/10 文件 / 1027 行TokenMeter extends Service(src/index.ts:74)——从 usage 事件累计 token 用量

§3机制:触发 → 摘要 → replace

  1. 触发:token-meter 累计用量超过阈值(config 可调——AGENTS.md 的「无硬编码可调参数」约定),或用户 /compact 命令。
  2. 执行:走 agent.runMaintenance()(05 页)——压缩在真 idle 阶段运行,对外仍是 idle 状态,与 turn 不打架。
  3. 摘要:compaction-basic 把历史交给 LLM 生成摘要(它的 inject 里有 llm——压缩本身也是一次模型调用,走 10 页的同一 llm/stream)。
  4. replace:摘要作为一条带 surfaceOp: {op:'replace', start, end}user/message 落盘——06 页的 surface 校验(assertProvenance 要求 sourceEventSeqs 覆盖每一个被遮蔽节点)在此生效。
  5. 重建replaceGeneration 变化 → deriveMessages 缓存重建——模型下一请求看到的就是摘要(09 页 507 行 messages 的来源)。

§4关键代码

packages/compaction/compaction/src/index.ts压缩引擎的 Service Definition96
96export abstract class CompactionEngine extends Service {
packages/compaction/compaction-basic/src/index.ts引擎实现的依赖面104
104  static inject = ['llm', 'tokenMeter', 'sessions']
96

又是 seam 模板:抽象引擎(Definition)+ 实现(Provider)。换压缩策略 = 换 provider(比如未来一个「本地小模型摘要」引擎)。

104

依赖面说明一切:llm(生成摘要)、tokenMeter(判断该不该压)、sessions(落 replace 事件)。

§5易错点

摘要事件是模型可见的 replace,不是人类可见的删除——人类读历史用 isAppendSurfaceEvent(06 页),摘要事件是 replacement 副本,只属于模型。UI 显示摘要时同样要靠 append-origin 过滤。

压缩期间的新输入会怎样?maintenance 相的 wakeRequested latch(05 页 178 行)——压缩中被唤醒的请求挂起,压缩结束后重放。这就是 05 页「maintenance 与 aborted 的 driver 无法送达唤醒,必须 latch」的具体场景。