
字节笔记本
2026年10月6日 · 约 9 分钟读完
DeepSeek Harness 怎么做上下文压缩

长对话跑到后半程,上下文窗口总会被历史记录慢慢填满:token 费用一路上涨,无关信息持续干扰模型,直到某一次请求直接溢出报错。多数框架的应对是压缩(compaction):把一段旧历史换成一条摘要,让对话轻装上阵。DeepSeek Harness 是 DeepSeek 开源的 agent 框架(MIT 协议,GitHub 244k star,口号是「一切皆插件」),它把压缩做成了一个独立子系统,事件、锁与失败处理的设计都相当完整。本文基于其官方文档,拆解这套值得自建 agent 运行时借鉴的机制。
可选能力,而非循环主干

压缩在 DeepSeek Harness 里是一个「能力接缝」(capability seam),由三个包分工协作。dsh-compaction 是 Service Definition,定义抽象的 CompactionEngine 接口,挂载后以 ctx.compaction 访问;dsh-compaction-basic 是默认的 Service Provider,实现具体策略;dsh-command-compact 是面向用户的 Consumer,把压缩暴露为手动命令。
有两个设计决定值得注意。其一,压缩是一项可选能力,不属于 agent loop 主干,所以它的类型词汇定义在子系统内部而非核心包;基于 tokenizer 或模板的后端都是实现同一接口的兄弟包,可以互相替换。其二,这个接口又必然依赖会话与模型两个模块:它的动词作用于 agent 的所有 Session,持久化摘要使用 ContentBlock 词汇。计价则被划在接缝之外,由单例 ctx.tokenMeter 直接负责 token 估算与事件回放。
三种只写日志的事件
压缩的每一步都以会话事件留痕,三种事件通过声明合并扩展进来:
| 事件 | 载荷要点 | 作用 |
|---|---|---|
compaction/start | turn | 获取日志锁;数字标识自动轮次,null 标识手动尝试 |
compaction/summary | 摘要、shadowedRange、shadowedSeqs、shadowedTokenCount、provider、model 等 | 记录安全摘要投影、被遮蔽范围、token 估算与模型调用信息 |
compaction/end | turn、error? | 用相同的归属值释放锁,error 记录失败尝试 |
关键在于:三种事件都只写日志,绝不进入 surface(模型可见的消息层),框架有意不扩展产生消息的事件类型。摘要本身承载在另一条带 surfaceOp: replace 的 user/message 上,这是压缩执行的全部表层变更:模型看到的是一条替换后的用户消息,完整审计信息留在日志里。
锁的语义值得细看。先追加 start,再生成摘要、写入 summary 记录、执行替换,最后才写 end。把释放锁放在最后意味着:操作中途崩溃,日志里留下的是一个有 start 而无匹配 end 的遗留锁,可以被检测到;绝不会出现一条虚假声称压缩已完成的 end。
这些标记是锁的时间点,不是排他的容器。摘要等待期间,不相关的空闲注入可以出现在手动 start 与 end 之间,手动路径只重新验证所选的位置跨度。活动的未匹配 start 会阻塞所有入口;出现在较新会话生命周期边界之前的未匹配 start,则是先前生命周期留下的陈旧证据,会被直接忽略。
summary 事件里还有一个细节:llmStreamCall: true 标记表示这次摘要恰好是通过当前上下文的 ctx.llm.stream() 发起的一次调用所生成,此时必须附带完整的原始输出 rawOutput 与 usage;反过来,一条没带标记的 rawOutput 并不能判定调用路径。
引擎的三个入口

type CompactionTrigger = 'pressure' | 'context-overflow'CompactionEngine 对外只暴露三个动词。
compactIfNeeded 是自动路径:pressure 对应常规压力策略,context-overflow 对应提供方确认的上下文溢出,此时实现可以比普通压力更激进。压力压缩在串行的 agent pre-step 中运行,先于请求推导。溢出发生时,compaction-basic 在选择范围前会先调用可选的工具结果剪枝服务,再用 tokenMeter 重新测量,有时不生成摘要就能把表面推回预算内。找不到安全范围就返回 null,不写入任何东西。
compactNow 是手动路径:即使未达到压力阈值,也对空闲会话执行一次有效缩减,作为轮次之间的维护任务运行。手动失败有六类预期错误码:busy、cancelled、changed、summary、commit、persistence。其中 changed 与 summary 保持会话表层不变,但仍会闭合失败尝试并持久化到日志;commit 可能发生在部分变更之后;persistence 表示内存中的标记对已闭合、但落盘失败。取消独立于这些失败,完成后抛出原始的 abort 原因。
compactRegion 处理显式范围:压缩一个两端均包含的表面区间。start 与 end 按表面位置命名,而不是数值 seq 顺序:先前的替换可能在旧位置落下新的高 seq 摘要节点,所以 start 完全可能大于 end,被遮蔽节点的权威集合以 shadowedSeqs 为准。两条边界必须保持工具调用与结果的配对,引擎提供 toolPairingBalancedBefore/After 两个校验函数,会拒绝缺失的 seq 与遗留结果;但边界不必保持整个轮次,因此一个过大的轮次里较早关闭的步骤同样可以被压缩。
还有一个跨后端约定:每个后端用 compactCheckpointSource 创建替换用 user/message 的源,携带 compactionId 等事务身份;客户端与协议层消费方从无框架依赖的 checkpoint 子路径导入这个构造函数与判定函数,检查点识别因此不依赖任何特定后端。
不惊动模型的轻量瘦身
压缩不必总是请模型写摘要。可选的 toolResultPruner 对当前表面上的超预算工具结果做确定性的 head/middle/tail 剪枝:measureContent 按 Unicode code point 度量文本,非文本块计为零;pruneContent 按 code point 切割超预算的文本中段并保留富块顺序,由于按 code point 而非 UTF-16 编码单元切,边界不会劈开代理对(字形簇仍可能被切开)。
pruneSession 对一份稳定的表面快照剪掉所有超预算的工具结果:每个替换保留除 content 外的完整事件数据,并引用被遮蔽的原节点,回放时可以恢复替换的输入;每条替换紧跟一条 compaction/prune 影子计价事件,纯消费者不用维护逐节点状态就能把剪掉的量扣除。剪枝服务还会汇报每次替换的账目:原事件 seq、替换事件 seq、callId 与替换前后的字符数,汇总成总削减量。
值得抄走的五件事
- 日志三事件加锁语义:崩溃可检测,成功不可伪造。
- 摘要进日志、不进表层:模型上下文与审计记录彻底解耦。
- 失败是一等公民:六类预期错误码,失败尝试同样持久化。
- 按 code point 而非 UTF-16 单元度量文本,切片边界安全。
- 事务身份(compactionId)让检查点识别与后端实现解耦。
如果你的 agent 运行时也在长会话里被 token 压垮,这套「事件、锁、替换」的协议,比「调一次模型做总结然后截断」要健壮得多。项目地址:github.com/deepseek-ai/deepseek-harness。



