源码级对比 DeepSeek Harness PiCodexClaude Code OpenCodeHermesOpenClaw

第七家入场:
DeepSeek Harness 一样吗

结论先给:压缩的骨架和前六家完全同构,连摘要提示词的分段都几乎逐条对应。但它多出一条正交的路——把超大输出在产出的那一刻就赶出上下文,并且把 KV cache 复用写成了全仓库的强制契约。这两点是现有六个维度装不下的。

它的一次模型请求装了什么

System / Tools / 路由信封
+ <compacted-summary> 检查点(替换旧区间)
+ Recent Tail(默认 16% 窗口)
+ 预览+路标(spill 过的工具输出)
+ 当前输入
= Surface(append-only 日志的投影)

同构在哪 / 分歧在哪

同构

阈值触发 → 无模型剪枝 → 保留尾巴 → 结构化摘要 → 替换旧区间。行业已收敛的共识。

分歧

spill:大输出在产出时就外部化为「预览 + 可回取路标」,而非事后截断丢弃。

分歧

摘要请求逐字节重放原对话,只为把 provider 的 warm prefix cache 用到最后一刻。

一、三层降压:顺序是有意义的

这是理解它与前六家差别的唯一入口。DeepSeek Harness 不是「一个压缩器」,而是三道依次降级的闸,前两道都不调模型,且严格按下面的顺序尝试。

1

spill —— 工具产出的那一刻

超过 maxInlineBytes 的纯文本工具结果,在 tools/post-execute 阶段被整份写入会话级本地文件,模型侧只留下「头尾预览 + 省略字节数 + 文件路径 + 检索方法」。信息没有丢,只是从上下文搬到了磁盘,并留下模型可自行回取的路标。

不调模型追加式,不破坏 KV cache可回取包:spill / spill-local / spill-policy
2

tool-result pruner —— 压力触发后、选区间前

已经躺在历史里的超大 tool/result 被重写为「定长头 + 固定省略标记 + 定长尾」。默认 thresholdChars 8192、headChars 4096、tailChars 1024。剪完会重新测量,压力若已降到阈值以下就直接跳过第三层,省掉那次 LLM 调用。

不调模型从改动点起 KV cache 失效原事件仍在日志可选插件
3

summarizer —— 剪枝后仍超压才动

辅助模型生成八段式结构化检查点,替换选中的旧区间,保留近期尾巴。默认 thresholdRatio 0.8 触发、retainRatio 0.16 保留。

一次 llm/stream 调用从替换点起失效原事件仍在日志

对照你已经梳理过的六家:它们在「压缩前减负」这一格里做的事——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.)

三个设计细节值得注意:

前六家的做法

事后截断。信息永久丢失于模型侧,且改写历史必然让 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: 压缩指令(八段式模板)              ]   ← 唯一未缓存的输入

配套的工程约定还有几条:

对比之下,你表里 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 保持会话表面完全不变,但失败的尝试仍然留在日志里。


五、同构的部分:这些不算新

为免误判,下面这些是它与六家共有的,属于行业已收敛的做法。

检查点前言也走了同一条路:「这是自动生成的检查点……把捕获的上下文当作既定背景,直接继续任务,不要复述,也不要提及这个检查点。」——与 Claude Code、OpenClaw 的处理方式属同一思路。


六、七系统总表

沿用你原有的六个维度,并新增两行(表尾加粗的两行)——因为原维度接不住 spill 和 prefix replay。

维度PiCodexClaude CodeOpenCodeHermesOpenClawDeepSeek Harness
触发策略 Window − Reserve模型限制 + 硬窗口接近限制窗口超满估算Gateway 85% + Agent 50%Turn 后 + Overflow 路由窗口 × 0.8;agent/pre-step 串行检查
压缩前减负 Tool Output 截断移除最旧项重试先清旧 ToolPrune 旧 Tool无语义清理Transcript Byte Guard pruner 头/尾定长重写 + 重测后可跳过摘要
摘要 / Checkpoint 当前模型结构化本地摘要或远程 CPStructured SummaryCompaction Agent 滚动摘要Auxiliary Model 滚动摘要Built-in / Provider 可审计 一次 llm/stream,八段式,滚动合并
Recent Tail ~20K Token~20K User Msg官方保留关键内容Token Budget 切分Token Budget + 消息锚点Token Budget 原文 窗口的 16%(可切换绝对值)
原始记录 Session Tree EntryCheckpoint 元数据Session Resume(闭源)投影分离Soft Archive 可搜索SQLite Transcript 完整 append-only 日志 + surface 投影;压缩=追加 replace 指令
长期记忆协作 本身较轻外部规则 / HookCLAUDE.md + Auto Memory 重注入插件 / 外部文件Memory Provider 观察Silent Memory Flush spill 文件 + session-reference 跨会话快照
可扩展性 Hook 接管双路径选择PreCompact HookPrompt / Context / ContinueContextEngine 整个替换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 没有发明新东西——它落在六家已经收敛的同一条流水线上。真正的分歧在三个更工程化的地方:

  1. 它把「不让上下文变满」当成了压缩之前的独立问题,并用 spill 给了一个不丢信息、不破坏缓存的答案。
  2. 它把缓存经济学当成一等设计约束,为此宁可让摘要请求重放一遍完整对话。
  3. 它把压缩定义成日志上的一条可审计事务,而不是对历史的一次修改。

前两条是可以直接借鉴到任何 harness 上的;第三条要求整套会话模型是 append-only + 投影,属于架构级选择,不是能后加的补丁。

Surface模型这次真正看到的内容,是日志折叠出的投影,不是日志本身。
Shadowed被替换指令遮蔽的旧事件,仍在日志中,可回放、可审计,但模型看不到。
Spill被搬到磁盘的完整工具输出,模型侧只剩预览与路标,需自行 read/grep 回取。