一、三层降压:顺序是有意义的
这是理解它与前六家差别的唯一入口。DeepSeek Harness 不是「一个压缩器」,而是三道依次降级的闸,前两道都不调模型,且严格按下面的顺序尝试。
spill —— 工具产出的那一刻
超过 maxInlineBytes 的纯文本工具结果,在 tools/post-execute 阶段被整份写入会话级本地文件,模型侧只留下「头尾预览 + 省略字节数 + 文件路径 + 检索方法」。信息没有丢,只是从上下文搬到了磁盘,并留下模型可自行回取的路标。
tool-result pruner —— 压力触发后、选区间前
已经躺在历史里的超大 tool/result 被重写为「定长头 + 固定省略标记 + 定长尾」。默认 thresholdChars 8192、headChars 4096、tailChars 1024。剪完会重新测量,压力若已降到阈值以下就直接跳过第三层,省掉那次 LLM 调用。
summarizer —— 剪枝后仍超压才动
辅助模型生成八段式结构化检查点,替换选中的旧区间,保留近期尾巴。默认 thresholdRatio 0.8 触发、retainRatio 0.16 保留。
对照你已经梳理过的六家:它们在「压缩前减负」这一格里做的事——Pi 截断到 2000 字符、Hermes 把超 200 字符的旧结果替换成标记、OpenCode 的 prune、Claude Code 的阶段一清理——全部等价于上面的第 2 层。没有一家有第 1 层。
二、分歧一:spill,把大输出外部化而不是丢弃
这是六家都没有的独立能力族,而且被拆成了三个可替换的包:spill(Service Definition,定义「什么是 spill 后端」)、spill-local(把文本存进会话级本地文件)、spill-policy(决定何时 spill)。未来换成 spill:// URI、数据库主键或远程后端,policy 一行不用改。
模型实际看到什么
<保留的头部预览……保留的尾部预览>
(Omitted 123456 bytes. Full formatted result stored at:
/…/session-…/…-web_fetch.txt.
Use read with offset/limit, or grep this path to search within it.)
三个设计细节值得注意:
- 提示语的字节成本预先扣除。整个替换体(预览 + 空行 + 提示)保证不超过
maxInlineBytes,预览会自动缩短去适配。如果连提示本身都塞不下,策略放弃替换,保留原始结果——它绝不吐出一个超过上限的替换体。 - 刻意跳过
read工具。否则会形成「read → spill → 再 read」的死循环。 - 失败是「尽力而为」。没有后端、没有会话归属、写盘失败,都只记一条警告并返回原结果,绝不把一次成功的调用变成
isError。
前六家的做法
事后截断。信息永久丢失于模型侧,且改写历史必然让 KV cache 从改动点起失效。模型不知道自己错过了什么。
DeepSeek Harness 的做法
产出时外部化。追加式,不破坏缓存;模型拿到的是可执行的回取指令,需要细节时自己 read/grep 那个路径。
这条路的代价是:它只处理纯文本的最终结果。含非文本块的混合结果、被拦截的反馈、以及
read自身的输出,全部原样通过——文档在 Known Limitations 里明确列出了这一点。
三、分歧二:把 KV cache 复用写进契约
摘要请求的做法相当反直觉。常规思路是另起一个精简 prompt 去总结;它不这么做。
它把原对话逐字节重放——同样的 system prompt、同样的 tool schema、同样的被遮蔽区间消息(连图片引用一起),然后只在最末尾追加一条压缩指令作为 user message。这样 provider 的 warm prefix cache 能一路命中到最后那条指令为止,只有指令本身和摘要输出是未缓存的。
[ 与上一次路由请求逐字节相同的 system prompt ]
[ 与上一次路由请求逐字节相同的 tool schemas ]
[ 被遮蔽区间的原始消息,含图片引用 ] ← 全部命中 prefix cache
──────────────────────────────────────────────
[ user: 压缩指令(八段式模板) ] ← 唯一未缓存的输入
配套的工程约定还有几条:
- 请求打
GenerateOptions.purpose = 'compaction',DeepSeek 适配器据此发出x-deepseek-harness-compact: 1头做归因,且不触碰模型可见的 body。 - 只有返回的文本进入检查点。推理内容和工具调用被显式排除——前者会泄漏私有推理,后者会产生孤儿调用。图片输出直接以
UNSUPPORTED_CONTENT失败,而不是静默消失。 - 文档明说了这个优化的失效条件:把摘要路由到别的 provider/model,或压缩非头部区间,就放弃了这份复用。
- 整个仓库里,每一个包的 README 都有一个独立的「KV Cache effect」小节,逐项声明自己是追加式还是替换式。
对比之下,你表里 Claude Code 那格的「可利用 Prompt Cache 前缀」是一句观察;这里是把它做成了跨仓库的强制文档字段和明确的设计约束。这个差别值得在对比表里单开一个维度。
四、分歧三:压缩是「往日志追加一条替换指令」
最接近的是 OpenCode 的「投影分离」和 OpenClaw 的「SQLite 完整 transcript」,但这里更彻底。
三个 compaction/* 事件全部是 log-only,一律不进 surface。SurfaceEventType 被刻意不做扩展——只有产生消息的事件才能到达模型。摘要本身骑在一条独立的 user/message 上,携带 surfaceOp:
user/message {
surfaceOp: { op: 'replace', start: 起始seq, end: 结束seq },
content: "<compacted-summary> …… </compacted-summary>"
}
剪枝同理——不是原地改写,而是追加一条新的 tool/result,带 surfaceOp: replace 和 sourceEventSeqs,并且完整保留原事件的 turn、step、callId、错误字段与 meta。
所以「压缩」在这套体系里的语义是:往只追加的日志里写一条替换指令,surface 是日志折叠出来的投影。被压掉的内容叫 shadowed(遮蔽)而非删除,原始事件永远在,可完整回放、可精确审计。
事务括号:比「压缩是持久事件」更进一步
| 事件 | 时机 | 作用 |
|---|---|---|
compaction/start | 最先,同步追加 | 取得日志记录的锁;数字标识开放的自动 turn,null 标识独立的手动尝试 |
compaction/summary | 摘要产出后 | 记录摘要、遮蔽区间与 seqs、遮蔽 token 数、provider/model/maxTokens/usage——使这次一次性请求可从「日志 + 代码」重建 |
compaction/end | 最后 | 释放锁;error 字段记录失败的尝试 |
锁最后释放是刻意的:中途崩溃会留下一个可检测的孤儿锁(有 start 无 end),而不是一个谎称「压缩已完成」的 end。更细的一层——一个出现在更新的 session/end-seed 之前的未匹配 start,被判定为上一个生命周期的陈旧证据,不阻塞;之后的才报 busy。
失败也分得很细,ManualCompactionErrorCode 有六种:busy / cancelled / changed / summary / commit / persistence。其中 changed 和 summary 保持会话表面完全不变,但失败的尝试仍然留在日志里。
五、同构的部分:这些不算新
为免误判,下面这些是它与六家共有的,属于行业已收敛的做法。
- 比例制阈值。
thresholdRatio 0.8触发、retainRatio 0.16保留,都是相对路由到的模型窗口的比例。与 Hermes 的 85% / 50% 双层、Pi 的window − reserve属同一类。 - 溢出恢复独立路径。provider 返回
CONTEXT_WINDOW_EXCEEDED时绕过正常压力与保留策略,直接做一次最大化的头部削减,只留最新的不可分割单元。与 Codex、OpenClaw 的 Overflow Recovery 同构。 - 工具调用配对不拆开。
toolPairingBalancedBefore/After保证切点不会把 tool call 与 result 分开。六家都有。 - 滚动摘要。提示词明确规定:若对话里已有
<compacted-summary>块,那是上一个检查点,不得原样抄录,要保留仍成立的、丢弃过期的、把新信息合并成同结构的单一摘要。与 OpenCode、Hermes 一致。 - 八段式摘要模板。Primary Request and Intent / Key Technical Concepts / Files and Code / Errors and Fixes / Pending Jobs / Current Work / Next Step / Critical Context——与 Claude Code 那套几乎逐段对应,并同样强制「空段写 (none),绝不删段」。
检查点前言也走了同一条路:「这是自动生成的检查点……把捕获的上下文当作既定背景,直接继续任务,不要复述,也不要提及这个检查点。」——与 Claude Code、OpenClaw 的处理方式属同一思路。
六、七系统总表
沿用你原有的六个维度,并新增两行(表尾加粗的两行)——因为原维度接不住 spill 和 prefix replay。
| 维度 | Pi | Codex | Claude Code | OpenCode | Hermes | OpenClaw | DeepSeek Harness |
|---|---|---|---|---|---|---|---|
| 触发策略 | Window − Reserve | 模型限制 + 硬窗口 | 接近限制 | 窗口超满估算 | Gateway 85% + Agent 50% | Turn 后 + Overflow | 路由窗口 × 0.8;agent/pre-step 串行检查 |
| 压缩前减负 | Tool Output 截断 | 移除最旧项重试 | 先清旧 Tool | Prune 旧 Tool | 无语义清理 | Transcript Byte Guard | pruner 头/尾定长重写 + 重测后可跳过摘要 |
| 摘要 / Checkpoint | 当前模型结构化 | 本地摘要或远程 CP | Structured Summary | Compaction Agent 滚动摘要 | Auxiliary Model 滚动摘要 | Built-in / Provider 可审计 | 一次 llm/stream,八段式,滚动合并 |
| Recent Tail | ~20K Token | ~20K User Msg | 官方保留关键内容 | Token Budget 切分 | Token Budget + 消息锚点 | Token Budget 原文 | 窗口的 16%(可切换绝对值) |
| 原始记录 | Session Tree Entry | Checkpoint 元数据 | Session Resume(闭源) | 投影分离 | Soft Archive 可搜索 | SQLite Transcript 完整 | append-only 日志 + surface 投影;压缩=追加 replace 指令 |
| 长期记忆协作 | 本身较轻 | 外部规则 / Hook | CLAUDE.md + Auto Memory 重注入 | 插件 / 外部文件 | Memory Provider 观察 | Silent Memory Flush | spill 文件 + session-reference 跨会话快照 |
| 可扩展性 | Hook 接管 | 双路径选择 | PreCompact Hook | Prompt / Context / Continue | ContextEngine 整个替换 | Provider 插槽 + 审计 | Definition / Provider / Consumer 三段 seam,整体可换 |
| 超大输出的处理路径 新增维度 |
截断丢弃 | 截断丢弃 | 截断丢弃 | 截断丢弃 | 标记替换 | 字节守卫 | 产出时外部化 + 可回取路标(唯一一家) |
| KV cache 策略 新增维度 |
未声明 | 未声明 | 可利用前缀(观察) | 未声明 | 未声明 | 未声明 | 逐字节重放原对话;每包强制声明缓存影响 |
七、两个值得抄的细节,一个必须知道的短板
值得抄之一:收敛保证
摘要若不比源文本更短,直接拒绝;重试 compactionRetries(默认 1)次后仍在阈值之上就抛错,而不是假装成功。六家的公开文档里都没有写这一条。它同时还有一个 compaction-loop-repro 的回归测试专门盯这个死循环。
值得抄之二:按模型分策略
modelPolicies 支持对精确的 {provider, model} 对做覆盖,同一个压缩插件能同时服务 128K 和 1M 窗口的模型,各用各的阈值与保留预算,且匹配不依赖 listModels()(模型发现只作参考,不参与决策)。
- name: '@deepseek-ai/dsh-compaction-basic'
config:
thresholdRatio: 0.8
retainRatio: 0.16
modelPolicies:
- provider: local
model: small-context
thresholdRatio: 0.7
retainTokens: 2048
短板:token 计量仍是启发式
它自己在 Known Limitations 第一条就承认了:没有真 tokenizer,缺少可复用的 provider usage 时,回退到字符数加结构开销。只有当最近一次成功调用的规范请求信封未变时,才用真实 usage 作为锚点。这一点相对 Hermes / OpenCode 并无本质优势。
另外三条明确的边界:compactRegion 在完全关闭的会话上会抛 no open turn;系统提示、工具定义、请求前缀本身无法被压缩;一个不可分割的非工具节点仍可能单独超出预算。
八、回答那个问题:一样吗
算法一样,工程契约不一样。
如果只看「怎么把长历史变短」,DeepSeek Harness 没有发明新东西——它落在六家已经收敛的同一条流水线上。真正的分歧在三个更工程化的地方:
- 它把「不让上下文变满」当成了压缩之前的独立问题,并用 spill 给了一个不丢信息、不破坏缓存的答案。
- 它把缓存经济学当成一等设计约束,为此宁可让摘要请求重放一遍完整对话。
- 它把压缩定义成日志上的一条可审计事务,而不是对历史的一次修改。
前两条是可以直接借鉴到任何 harness 上的;第三条要求整套会话模型是 append-only + 投影,属于架构级选择,不是能后加的补丁。