上下文压缩对照:DeepSeek Harness、Claude Code、Codex、Pi 各自压掉了什么
有一类现象在长会话里很常见:一段长会话压缩完,agent 突然不记得项目里那条「所有新文件必须走某个工厂函数」的规矩了。很容易把它当成模型变笨,但翻这几家自己写的材料会发现,这件事是有明确机制解释的——Claude Code 官方文档里就有一张表,逐条写明哪一类规则在压缩之后会丢,丢到什么时候为止。规矩是靠什么机制进上下文的,直接决定了它压完还在不在。
所以「压缩」这件事真正该问的不是「压不压」,而是三个具体问题:什么时候压、压掉哪一段、压完之后你会丢什么。这四个项目的公开材料里,这三个问题的答案彼此不一样,而且不一样的地方恰好就是会咬人的地方。
先声明范围:DeepSeek Harness 的部分来自仓库源码与 docs/,该仓库 README 在「Developer preview」一节里自述处于开发者预览阶段,并用大写强调会有破坏兼容性的变更,下面提到的配置键与默认值随时可能变;Claude Code 是闭源产品,只能引官方文档写明的内容,不推断实现;Pi 与 Codex 引仓库内相对路径。三家不排名。
一、什么时候压:比例、绝对预留、还是一个绝对阈值
DeepSeek Harness 的默认策略写死在 packages/compaction/compaction-basic/src/config.ts 里,两个常量一眼可见:DEFAULT_THRESHOLD_RATIO = 0.8、DEFAULT_RETAIN_RATIO = 0.16。这是比例口径。同一文件的 resolveCompactSpec() 把比例落成绝对值:thresholdTokens = Math.floor(contextWindow * policy.thresholdRatio),其中 contextWindow 由拥有当前路由的适配器提供。也就是说它不写死「到 16 万 token 就压」,而是写「到窗口的八成就压」,换模型自动跟着变。同一份配置还有 maxTokens(默认 8192,摘要那次调用的生成上限)、compactionRetries(默认 1)、maxOverflowRetries(默认 1)、auto(默认 true)。
Pi 的口径是绝对预留。packages/coding-agent/docs/compaction.md 把触发条件直接写成一行公式:contextTokens > contextWindow - reserveTokens,reserveTokens 默认 16384,文档说明这是给模型回复留的余量,可在 ~/.pi/agent/settings.json 或项目内 .pi/settings.json 里改。
Claude Code 官方文档给的是第三种形态:它把这个东西命名为 auto-compact window。文档写明,如果你不设,默认是「会话达到模型上下文上限时压缩」,并列出了若干例外——云端会话在接近上限时就压,某些在 200K 窗口下运行的配置在 200K 边界压,把 CLAUDE_CODE_DISABLE_1M_CONTEXT=1 打开后原生 1M 窗口的模型也回到 200K 边界压。要自己定,文档写明有三个地方:/autocompact 带一个 token 数(如 /autocompact 500k)会存进用户设置的 autoCompactWindow;--autocompact 命令行参数只对这一次启动生效;环境变量 CLAUDE_CODE_AUTO_COMPACT_WINDOW 优先级高于前两者。
Codex 这一侧要说实话:它的 docs/ 目录下 15 个 md 文件里,我们 grep 不到 compact 这个词,也就是说公开文档没有讲压缩。但源码里配置键是有的——codex-rs/config/src/config_toml.rs 第 166 行 model_auto_compact_token_limit,紧跟着第 170 行还有一个 model_auto_compact_token_limit_scope。这个 scope 的取值定义在 codex-rs/protocol/src/config_types.rs:Total(默认)与 BodyAfterPrefix,源码注释写的是前者「把整个活跃上下文计入限额」,后者「只计入携带前缀之后采样输出与后续增长」。这一层「阈值到底拿什么去比」的区分,我们在另外三家的公开材料里没有找到对应说明,不比。
这个差异什么时候咬你:你从一个 200K 窗口的模型换到更大窗口的模型时。比例口径会自己跟着放大,绝对预留口径的「留多少给回复」不变但触发点跟着窗口走,而一个写死的绝对阈值不会自己变——你得记得回去改。
二、保留哪一段:两家按 token 往回数,一家按加载机制分类
DeepSeek Harness 的选段逻辑在 packages/compaction/compaction-basic/src/region.ts 的 selectCompactableRange():从 surface 最后一个节点往前累加每个节点的 token,累到 >= retainTokens 就停,这个位置记为 keepFromIdx;然后往前挪,直到 toolPairingBalancedBefore() 通过为止;如果最后 keepFromIdx === 0 就返回 null,什么都不压。要压的范围是 head-anchored 的——从 surface 第一个节点一直到保留段前一个节点,也就是从头压到尾巴之前,不是压中间那一块。
Pi 的做法结构上是一致的:文档里「Find cut point」那一步写的就是从最新消息往回走累加 token 估算,直到达到 keepRecentTokens(默认 20000)。区别只是它是绝对值,而 DeepSeek Harness 那 0.16 是比例。
Claude Code 官方文档没有讲「保留最近多少 token」,它讲的是另一个维度,而且这个维度更贴近开头那个坑。文档里「What survives compaction」有一张表,逐条写明每种加载机制压缩之后会怎样:system prompt 与 output style 不变,因为它们本来就不在消息历史里;项目根 CLAUDE.md、无路径限定的 rules、auto memory 从磁盘重新注入;带 paths: frontmatter 的 rules 会丢失,直到再次读到匹配的文件;子目录里的嵌套 CLAUDE.md 同理;已调用过的 skill 正文会重新注入,但文档写明单个 skill 上限 5,000 token、总计上限 25,000 token,超了先丢最老的,而且截断保留文件开头(所以官方文档的建议是把最重要的指令写在 SKILL.md 靠前的位置);hooks 不适用,因为它们是代码不是上下文。
同一份文档还写明一条容易被忽略的:skill 的描述清单在启动时加载,但 /compact 之后不会重新注入,只有你真正调用过的 skill 会被保留。
开头那种情况,对应的就是这张表的第四行。这一维度我们在 DeepSeek Harness、Pi、Codex 的公开材料里没有找到对应说明——它们讲的是 session 层的消息保留,不是「项目规则以哪种机制进来」,不比。
这个差异什么时候咬你:当你把关键约束写进带路径限定的规则文件或子目录 CLAUDE.md 的时候。按官方文档的说法,要让它跨压缩存活,得去掉 paths: 或者挪到项目根 CLAUDE.md。
三、切在哪个边界:不许拆开的东西各家不一样
DeepSeek Harness 的硬约束只有一条:工具调用和它的结果不能被切开。packages/compaction/compaction/src 下导出了 toolPairingBalancedBefore(session, seq) 和 toolPairingBalancedAfter(session, seq) 两个判定函数专门做这件事。而 docs/subsystems/compaction.md 明确写了它不保证的东西:区域边界保住 tool-call/result 配对,但不保住整个 turn,一个超大 turn 里已经结束的早期 step 是可以被压掉的。
Pi 的边界规则写得更像一张白名单:合法切点是 user 消息、assistant 消息、BashExecution 消息、自定义消息(custom_message、branch_summary),永远不切在 tool result 上。另外它多处理了一种情况——单个 turn 就超过 keepRecentTokens 时切点会落在 turn 中间的 assistant 消息上,文档管这叫 split turn,此时它生成两份摘要(历史摘要 + turn 前缀摘要)再合并。
Claude Code 与 Codex 的公开材料里,我们没有找到关于切分边界的对应说明,不比。
四、压之前先剪:一层容易被忽略的中间态
DeepSeek Harness 在 summarize 之前还有一层可选的、不调模型的处理:packages/compaction/compaction-tool-result-pruner。它的默认值在该包的 src/config.ts 里:thresholdChars: 8192、headChars: 4096、tailChars: 1024,超阈值的 tool result 只留头尾,中间换成一个固定标记 PRUNE_MARKER,原文是 [... tool result middle pruned ...]。要特别注意计量单位——measureContent 的文档注释写的是 Unicode 码点,不是 token,也不是 UTF-16 code unit;文档同时写明按码点切不会切开代理对,但仍可能切开字素簇。按 docs/subsystems/compaction.md 的说法,剪完会通过 ctx.tokenMeter 重新测量,如果压力已经回到安全区,就直接跳过摘要那次模型调用。
Pi 文档里有个数字看起来像这一层,但其实不是同一件事:serializeConversation() 把 tool result 截断到 2000 字符。文档写明这是序列化给摘要模型看的转录上的截断,目的是控制摘要请求的体量,它并不改写会话本身。而 DeepSeek Harness 的 pruner 是往 session 里追加替换事件、改的是后续所有请求看到的内容。这两者放在一起容易混,我第一次读的时候就串了。
Claude Code 与 Codex 的公开材料里,我们没有找到对应机制的说明,不比。
五、摘要里留下哪些字段,以及能不能指挥它
DeepSeek Harness 的摘要提示词是硬编码的常量 COMPACTION_INSTRUCTION,就在 packages/compaction/compaction-basic/src/summarizer.ts 里,八个小节按顺序固定:Primary Request and Intent、Key Technical Concepts、Files and Code、Errors and Fixes、Pending Jobs、Current Work、Next Step、Critical Context。提示词里明写空小节要填 (none),一节都不许丢。落进会话的那条替换消息被 frameSummary() 包在 <compacted-summary> 与 </compacted-summary> 之间,前面还有一段 checkpoint 前言。另有两条限制值得记:摘要只取 text 类型的块,reasoning 与 tool call 一律不进 checkpoint;如果模型输出里含图像,直接抛 UNSUPPORTED_CONTENT。
Pi 的摘要格式在 packages/coding-agent/docs/compaction.md 里是完整贴出来的:Goal、Constraints & Preferences、Progress(Done / In Progress / Blocked)、Key Decisions、Next Steps、Critical Context,再加两个标签块 <read-files> 与 <modified-files>。文档还写明文件操作是累计跟踪的——新一次摘要会把上一次 details 里的文件列表并进来。
至于能不能给压缩下指令,这一条三家写得都很明确,而且不一样:
| 手动压缩能否带指令 | 依据 | |
|---|---|---|
| DeepSeek Harness | 不能,/compact 不接任何参数 | packages/compaction/command-compact/README.md 命令契约表写明 /compact <anything> 直接返回 Usage: /compact (no arguments) |
| Claude Code | 能 | 官方文档给的例子是 /compact focus on the auth bug fix |
| Pi | 能 | 文档写的是 /compact [instructions],可选指令用于聚焦摘要 |
| Codex | 文档里查不到 | docs/ 下无相关说明;源码 codex-rs/config/src/config_toml.rs 有 compact_prompt 配置键,另有一个 experimental_ 前缀的 experimental_compact_prompt_file |
那张表最后一行请当心:experimental_ 前缀是源码里就有的,不要把「字段存在」读成「功能可用」。
六、一个方向相反的取舍:缓存
这是我觉得最值得单独拎出来的一处,因为两边都白纸黑字写了理由,而且是相反的。
DeepSeek Harness 在 summarizer.ts 的注释里自述:压缩指令是作为最后一条 user 消息发出去的,而不是换一套摘要器专用 system prompt,这样会话原本的 system prompt、tools 和消息前缀都留在前面,摘要这次辅助调用就是上一次路由请求的一个真前缀,provider 的 KV 缓存可以复用而不是被打穿。同时 compaction-basic 的 README 也如实写了代价:替换式压缩会让被替换范围的第一个 token 起的缓存复用失效,那之前的请求前缀仍可复用。
Pi 的文档自述的是另一个方向:压缩与分支摘要请求使用全新的 routing session ID,并且在 provider 支持的情况下关闭 prompt-cache 写入,理由是这类一次性 prompt 不太可能被复用。
Claude Code 这一侧公开写明的相关信息是另一件事:从 v2.1.198 起,摘要请求继承你会话的 extended thinking 配置,会话开了它就带思考做摘要,关了就不带;文档同时写明这只影响摘要怎么生成,不改你的会话设置。缓存策略我们在其官方文档里没有找到对应说明,不比。Codex 同样不比。
怎么用这几条线索去判断
不要拿这几张表去排名,按你手上的具体症状倒着找:
- 压完之后它忘了项目规矩——先看那条规矩是以什么机制进上下文的。Claude Code 官方文档的存活表直接给了答案:带
paths:的 rule 和子目录 CLAUDE.md 会丢到下次读到匹配文件为止。 - 压完之后它忘了「我最初要干什么」——去看摘要模板有没有一节专门装这个。DeepSeek Harness 的第一节是 Primary Request and Intent,Pi 的第一节是 Goal,两家都把它排在最前面。
- 一条 tool result 就把窗口顶爆——看有没有「先剪再压」这一层。DeepSeek Harness 有一个独立的 pruner 包,默认对超过 8192 码点的 tool result 只留头 4096、尾 1024。
- 一个 turn 自己就超预算——看边界规则允不允许切进 turn 内部。DeepSeek Harness 的文档明说 turn 边界不保护 runaway turn 里的早期 step,Pi 有专门的 split turn 双摘要路径。
- 换了模型之后触发点不对——回去看阈值是比例还是绝对值,以及是不是有一个更高优先级的地方在覆盖它(Claude Code 官方文档写明环境变量会压过命令与设置)。
- 怀疑摘要那次调用本身很贵——两家写了相反的缓存取舍,先确认你用的那家是哪一种,再决定要不要在意。
最后重复一遍限定:DeepSeek Harness 处于开发者预览阶段、README 明写会有破坏兼容性的变更,上面所有配置键和默认值都以仓库最新内容为准;Claude Code 的部分只复述官方文档,实现细节我们不推断;Codex 的压缩配置目前只在源码里能查到,公开文档没有覆盖。
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。
文中涉及的 Claude Code 内容依据其官方文档(code.claude.com/docs)整理,该产品闭源,本文不推断其实现;
Codex 依据 github.com/openai/codex 快照 c6058cc、Pi 依据 github.com/earendil-works/pi 快照 027a5847 整理。
本文只对照各方公开写明的机制,不对三者做优劣排名。