第 40 讲:Compaction 与 Spill 内存管理

DeepSeek Harness 源码解析 · 第 40 讲

DeepSeek Harness 源码解析系列 第 40 讲:Compaction 与 Spill 内存管理 基于 DeepSeek Harness 源码 · 2026-09-23 💡 本讲一句话: Agent 跑得越久,上下文越满。DeepSeek Harness 用两套机制解决:Compaction 把旧历史"摘要成一个检查点"(带持久化锁、稳定性校验、KV cache 复用),Spill 把超大工具输出"搬到磁盘只留预览"——读完你能看懂一次压缩事务从触发到落盘的完整生命周期。 一、两种内存压力,两套机制 前几讲我们跟着 Agent Loop 走完了"请求 → 工具调用 → 结果回填"的完整循环。但有一个问题被刻意搁置了:这个循环每转一圈,session 日志就变长一点,发给模型的请求包也越来越大——直到某天 provider 直接回一句 "context window exceeded"。 DeepSeek Harness 把内存压力拆成两种性质完全不同的问题,分别交给两个能力域: 🔹 历史太长(compaction) :对话本身合法、每个节点都有价值,但总量超了窗口。解法是"有损压缩"——把旧的一段历史摘要成一个检查点节点,替换掉原区间。这是 模型参与 的操作,贵、慢、可能失败。 🔹 单条结果太大(spill / prune) :某一次 web_fetch 或 read 吐了 2MB 文本,一条消息就吃掉半个窗口。解法是"无损外置"——全文原样写到磁盘,上下文里只留头尾预览 + 一个可回读的定位符。这是 纯机械 操作,便宜、确定、永不丢数据。 本讲按"seam(抽象服务定义)→ basic backend(默认实现)→ policy(策略插件)"的顺序,把 packages/compaction 、 packages/spill 两个能力域的核心源码全部过一遍。 二、Compaction 服务定义:三个入口,一把锁 @deepseek-ai/dsh-compaction 是能力缝(capability seam):它只定义"压缩是什么",不规定"怎么压"。抽象类 CompactionEngine 暴露三个入口,分别对应三种触发场景: 📄 packages/compaction/compaction/src/index.ts (第 96-170 行) export abstract class CompactionEngine extends Service { // 抽象压缩服务:子类实现触发策略、保留策略与摘要方式 constructor(ctx: Context) { // 注册为 ctx.compaction 服务——每个 context 只允许一个实现 super(ctx, 'compaction') // 向 cordis 声明本服务的名字,重复加载第二个实现会抛错 } abstract compactIfNeeded( // 自动入口:步骤边界或溢出恢复时调用,"需要才压" agent: CompactionAgentContext, // 持有 session 与路由选项(provider/model)的 Agent 上下文 trigger: CompactionTrigger, // 'pressure' 常规压力 | 'context-overflow' provider 确认的窗口溢出 signal: AbortSignal, // 取消信号;基于模型的实现必须把它转发给摘要调用 ): Promise<CompactionResult | null> // 返回压缩结果,或 null(没有可安全压缩的区间) abstract compactNow( // 手动入口:/compact 命令走这里,低于阈值也强制压一次 agent: ManualCompactAgentContext, // 空闲 Agent 上下文,额外要求 runMaintenance 能力 signal: AbortSignal, // 只作用于本次请求的取消信号 sourceCommandId?: CommandId, // 发起这次压缩的手动命令身份(用于日志关联) ): Promise<CompactionResult | nul…

阅读全文:DeepSeek Harness 源码解析 第 40 讲