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 Definition:CompactionEngine extends Service(src/index.ts:96)——抽象压缩引擎 |
packages/compaction/compaction-basic/ | 6 文件 / 1621 行 | Provider:static 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
- 触发:token-meter 累计用量超过阈值(config 可调——AGENTS.md 的「无硬编码可调参数」约定),或用户
/compact命令。 - 执行:走
agent.runMaintenance()(05 页)——压缩在真 idle 阶段运行,对外仍是 idle 状态,与 turn 不打架。 - 摘要:compaction-basic 把历史交给 LLM 生成摘要(它的 inject 里有
llm——压缩本身也是一次模型调用,走 10 页的同一 llm/stream)。 - replace:摘要作为一条带
surfaceOp: {op:'replace', start, end}的user/message落盘——06 页的 surface 校验(assertProvenance要求 sourceEventSeqs 覆盖每一个被遮蔽节点)在此生效。 - 重建:
replaceGeneration变化 →deriveMessages缓存重建——模型下一请求看到的就是摘要(09 页 507 行 messages 的来源)。
§4关键代码
96export abstract class CompactionEngine extends Service {
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」的具体场景。