Agent Engineering 101|10|Compaction

Compaction 不是把历史随便删短,而是在长会话中用 summary entry 替代旧上下文,同时保留最近工作状态。

Agent Engineering 101|10|Compaction

核心结论

配套代码仓库: github.com/llm-101/mini-agent-harness

读完本文,你应该能回答

  • Compaction 为什么不是简单删除旧消息?
  • summary entry 如何接入 session tree?
  • keepLast 和 threshold 分别控制什么?
  • 压缩后下一轮模型如何继续当前任务?

本篇在系列中的位置

  • 上一篇:09 Context Builder 负责构造当前工作集。
  • 本篇:本文处理长会话里的预算压力:如何压缩而不丢任务连续性。
  • 下一篇:11 Skills 会把可复用任务知识作为运行时资源注入上下文。

贯穿例子

本系列会反复使用同一个任务来连接各章:

用户说:“帮我修复这个 repo 里的 failing tests。”

在这个任务里,Compaction 在会话变长后保留“目标、已读文件、失败原因、已尝试修改、还没验证的点”,把旧过程折成 summary,再接上最近几轮。读者要关注的是:压缩不是删短历史,而是让任务还能继续。

  • Compaction 是把旧消息折叠成 summary,并把 summary 作为 session tree 的边界节点。未来上下文从 summary 开始,再接上最近消息。
  • 上下文窗口有限,Agent 的会话却可能很长。没有 compaction,系统要么超出 token 预算,要么粗暴截断并丢失任务连续性。
  • mini-agent-harness 中,Compaction 被接到 session tree 上,是为了让压缩后的会话仍然能沿着同一条任务线继续。

定义

Compaction 是把旧消息折叠成 summary,并把 summary 作为 session tree 的边界节点。未来上下文从 summary 开始,再接上最近消息。

为什么要单独看这一层?

长会话不会因为上下文窗口有限而自动变短。Compaction 的价值,是把旧过程压成可继续工作的状态,而不是让系统在截断和爆窗之间二选一。

Compaction Boundary

边界

这一层的职责可以拆成几个稳定部分:

  • Token Estimate:用 JSON 字符串长度估算 token
  • Threshold:超过 compactionThreshold 才触发
  • keepLast:保留最近若干消息
  • SummaryEntry:旧消息摘要成为树节点
  • Re-parent:first kept message 指向 summary
  • Event:向产品层发出 compaction

这一层的边界可以用一个问题检验:如果它的内部实现变化,模型适配、工具执行、状态存储和产品外壳是否都不需要跟着重写?如果答案是否定的,说明这个边界还没有真正收束变化。

代码锚点

本篇主要对应这些模块:

  • src/compaction/compaction.ts
  • src/session/jsonl-session-store.ts
  • src/session/session-tree.ts
  • src/ai/tokenizer.ts

阅读代码时建议先看类型,再看运行路径。

类型定义告诉你这一层暴露什么 contract;运行路径告诉你这个 contract 在 Agent 执行中何时被消费、何时被写回、何时被产品层看见。

运行流程

From Long Branch to Summary Entry

一次典型执行可以概括为:

  1. Estimate:shouldCompact
  2. Summarize:compactMessages
  3. Insert Summary:createSummaryEntry
  4. Re-parent:firstKept.parentId
  5. Continue:messagesForActiveBranch
  • Continuity:保留任务上下文
  • Budget:控制窗口压力
  • Recoverability:summary 可持久化

这里最容易被忽略的是“中间态”。生产级 Agent 不是只关心最终答案;它还要在运行过程中展示进度、捕获错误、记录 usage、允许取消,并把可恢复状态写回 session。

读者抓手:压缩前后发生了什么

阶段 历史形态 风险 Harness 处理
压缩前 很长的 message branch 超出 token 预算 估算 token,判断 threshold
选择保留 最近几轮仍在工作 丢掉会破坏连续性 keepLast 保留最新上下文
生成摘要 较旧历史太长 粗暴截断会丢决策 summary 记录目标、已做、未做、关键发现
接回树上 新旧上下文断开 resume 时找不到来源 summary entry 成为 first kept message 的 parent
继续执行 上下文变短 模型忘记任务状态 summary + recent messages 组成新工作集

好的 compaction 不是“更短”,而是“短到能继续工作”。

可迁移伪实现:会话压缩

下面的伪代码是机制抽象,不对应真实 API 或文件结构。它只用来说明这一层的控制点:

if (shouldCompact(messages, threshold)) {
  const { summary } = compactMessages(messages, keepLast);
  const summaryEntry = createSummaryEntry(summary, firstKept.id, firstKept.parentId);
  firstKept.parentId = summaryEntry.id;
  yield { type: "compaction", summary };
}

这个草图的价值在于说明控制点,而不是提供可复制的库代码。

真正的工程实现还要处理错误、取消、并发、token 预算、日志、权限、序列化和 provider 差异。

工程原则

将这一层从 Agent 系统中拆出来,通常带来四个直接收益。

第一,可替换。

外部系统、模型 provider、工具集合或产品外壳变化时,核心运行时不必整体重写。

第二,可观测。

边界清晰后,事件、trace、usage、错误和状态迁移都有稳定落点。

第三,可恢复。

只要状态写入 session,运行时就可以在进程重启、工具失败或长任务中断后继续推理。

第四,可治理。

权限、脱敏、审批、路径保护和执行策略可以放在稳定 hook 或 runtime boundary 上,而不是只靠 prompt 约束。

和 Agent Harness 的关系

Agent = Model + Harness 这个公式的重点,不是把模型之外的所有东西都称为“工程杂活”。

相反,它提示我们:模型之外存在一套必须被设计的运行时系统。

Compaction 就是这套系统中的一个切面。它不替代模型能力,也不替代产品体验;它让模型能力可以被组织成可执行、可观察、可恢复、可治理的任务流程。

小结

Compaction 的核心价值,是把一类容易扩散的复杂性收束到明确边界中。

mini-agent-harness 中,这个边界被刻意写得较小,方便阅读和教学。但它对应的问题并不小:只要一个 Agent 要长期运行、调用工具、管理上下文、支持 UI、保存状态并处理失败,这个边界就会出现。

下一步可以继续沿着系列计划,把这些边界组合成完整 Agent Harness:模型边界、工具边界、状态边界、上下文边界、扩展边界和产品外壳边界。