ByteNoteByteNote
DeepSeek Harness 上下文压缩子系统解析
字

字节笔记本

2026年10月6日 · 约 8 分钟读完

DeepSeek Harness 上下文压缩子系统解析

API中转
¥120

长会话是 agent 的宿命:工具结果、文件内容、多轮往复不断堆积,上下文窗口迟早见顶。怎么在不动核心循环的前提下,把旧内容安全地换成一份摘要?DeepSeek 开源的 agent 框架 DeepSeek Harness 给出了自己的做法:压缩不是主干上的必需件,而是一条可选的能力接缝,谁需要谁挂载。该项目在 GitHub 上以 MIT 协议开源,底层跑在 Cordis 插件框架上,目前处于开发者预览阶段。本文基于它的官方子系统文档,拆解这套上下文压缩机制的设计。

DeepSeek Harness 压缩接缝架构:服务定义、后端与命令消费者

一、像 bash 接缝一样拆成三段

文档把压缩接缝与 bash 接缝类比,同样一分为三:服务定义是 dsh-compaction 包,以 ctx.compaction 的形式暴露给插件;服务提供方是具体后端,比如内置的 dsh-compaction-basic,而基于分词器或模板的实现可以作为兄弟包顶上来,只要实现同一接口;人类消费者则是 dsh-command-compact,对应用户手敲的 compact 命令。

与 bash 接缝不同的是,压缩接口天然依赖 dsh-session 与 dsh-llm 两个包:它的动词都作用在 agent 持有的会话对象上,落盘的摘要事件也要借用 ContentBlock 词汇表来描述内容。也正因为压缩只是可选能力而非主干,这套词汇被收在压缩子系统自己的文档里,没有挤进核心文档。

二、三条日志事件与一把锁

压缩向会话事件表追加了三种事件:compaction/start、compaction/summary 与 compaction/end。三者都只记日志、不进表面,表面事件类型被刻意保持不动,因为只有产生消息的事件才会抵达模型。摘要本身搭载在一条单独的 user 消息上,用 surfaceOp 的 replace 操作标记替换区间,这是摘要压缩执行的唯一一次表面变更。

compaction/start 记录锁的取得,负载里的 turn 为数字时代表某轮自动压缩正在进行,为 null 则代表一次独立的手动尝试。compaction/end 负责释放锁。中间的 compaction/summary 负载最重:安全摘要投影、被遮蔽的表面区间与按表面顺序排列的序号清单、被遮蔽内容的估算 token 数,以及总结调用的 provider 与 model,可选地附上完整原始输出与用量统计。这些字段全部落盘,为的是让那次一次性请求仅凭日志加代码就能复原。

锁的时序是刻意安排的:先追加 start,再跑总结,然后 summary 记录与 user 消息替换先后落地,最后才追加 end。把释放锁放在最后,中途崩溃就只会留下一条没有配对 end 的 start,也就是一把可识别的孤锁,而不是一条谎称压缩已完成的 end。

对孤锁的判读也有讲究:活跃的未配对 start 会挡住所有压缩入口;而出现在更新的 session/end-seed 之前的未配对 start,只是上一段生命周期的陈旧证据,直接忽略。另外,这对标记并不是排他容器,手动压缩等总结期间,无关的空闲注入可以插进 start 与 end 之间;手动路径只校验自己选中的区间位置,因此被注入的上下文能在替换检查点之后安然存活。

三、引擎的三个入口

服务定义暴露的 CompactionEngine 有三个入口。compactIfNeeded 面向自动策略,触发原因由 CompactionTrigger 描述:pressure 是普通压力,context-overflow 是提供商确认的上下文溢出,实现方可以对确认溢出处理得更激进。compactNow 面向空闲会话,即使没到压力阈值也做一次有用的缩减;它作为 agent 维护任务在轮次之间运行,没有可用区间时不写任何东西,直接返回 null。compactRegion 则接收一个明确的表面位置区间,强制把区间压成单个摘要节点,区间两端必须平衡,保证助手的工具调用始终与结果配对。

压缩执行时序:锁、日志事件与表面替换

每次压缩成功都会返回 CompactionResult,里面有 start、summary、end 三条事件的序号,摘要内容块,被遮蔽的区间、序号与 token 估算。其中有个反直觉的细节:shadowedRange 是表面位置区间,不是数字序号区间,先前的替换会在旧位置落下一个序号更高的摘要节点,所以 start 完全可能大于 end,权威清单要看按表面顺序排列的 shadowedSeqs。

还有一个统一约定:所有后端创建替换 user 消息时,都必须用 compactCheckpointSource 构造来源,带上本次事务的压缩标识;客户端与线上消费者从无依赖的 checkpoint 子路径导入识别函数,做到不依赖具体后端就能认出并关联这份检查点。计费也不归这条接缝管:估算与重放由 ctx.tokenMeter 单例负责,保留策略、事件排序与路由总结调用则归 dsh-compaction-basic 所有。

四、手动压缩的六种失败

手动压缩定义了六种预期错误码:busy、cancelled、changed、summary、commit、persistence。changed 表示选中的区间已经变化,summary 表示总结或缩水环节失败,这两种情况下表面保持原样,但失败尝试仍会闭合并落盘。commit 可能发生在部分变更之后,persistence 则代表内存里的括号已经闭合、只是冲刷失败。取消是独立路径,在完成必要清理后按原样抛出中止原因。

五、压力压缩与工具结果修剪

压力压缩挂在 agent 每轮前置步骤的串行位置上,先于请求派生。一旦压力或确认溢出达标,压缩后端会先调用可选的 ctx.toolResultPruner 修剪工具结果,再用 tokenMeter 重新测量,甚至可以不产生摘要就推进表面。请求失败后的恢复走 agent/request-error,只有当表面替换代数前进时才给出重试动作,取消始终优先。

修剪服务做的是确定性的头中尾裁剪:measureContent 按 Unicode 码点统计文本块大小,非文本块记零;pruneContent 替换超预算的文本中段,切片按码点而不是 UTF-16 编码单元进行,保留边界因此不会劈开代理对,字形簇仍可能被切开;pruneSession 对一份稳定的表面快照修剪所有超预算的工具结果。每次替换都保留原事件除内容外的全部数据,引用被遮蔽的节点以便重放复原,并在紧邻位置追加一条 compaction/prune 影子计价事件,让纯消费方无需逐节点维护状态就能扣减这部分开销。返回结果列出每次替换前后的码点数与总削减量。

区间边界保持工具调用与结果的配对,但不保持整轮完整,所以一轮超长任务里较早闭合的步骤可以被提前压缩。服务定义还导出 toolPairingBalancedBefore 与 toolPairingBalancedAfter 两个函数,用于校验某个序号前后的工具调用配对状态。

六、值得借鉴的三个取向

跳出实现,这套设计有三点很值得抄。第一,把压缩当成可选能力而不是主干件,接口词汇留在子系统本地,核心循环保持精瘦。第二,全程只用日志事件,先加锁、最后释放,失败与崩溃都在日志里留下可判读的痕迹,可复原性优先于便利。第三,摘要不作为模型事件存在,而是以一条带替换标记的 user 消息落盘,识别靠与后端无关的检查点来源谓词,任何消费方都不必知道底下跑的是哪个实现。

相关文章

分享: