上下文压缩

触发与阻塞阈值、HEAD+TAIL 保留策略、摘要与续接提醒、指回事件日志的 Context Recovery,以及压缩请求自身的失败处理。

作者 David更新于 第 9 篇(共 33 篇)

上下文压缩是长会话 agent 的生死线。v2 的实现分两处:src/agent/fullCompaction/(何时压、怎么调模型,fullCompactionService.ts 955 行)与 src/agent/contextMemory/compactionHandoff.ts(压完的上下文长什么样)。

配置与阈值

export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
  triggerRatio: 0.85,
  blockRatio: 0.85,
  reservedContextSize: 50_000,
  maxCompactionPerTurn: Infinity,
  maxOverflowCompactionAttempts: 3,
  maxRecentMessages: 4,
  maxRecentUserMessages: Infinity,
  maxRecentSizeRatio: 0.2,
  minOverflowReductionRatio: 0.05,
};

前五项与 v0.29 相同:用到窗口的 85% 触发,预留 5 万 token 给输出,单回合压缩次数不设限,连续溢出压缩最多 3 次。

变化在「可配置」。loop_control 里有三个相关键:compaction_trigger_ratio(0.5–0.99)、reserved_context_size、compaction_max_attempts。RuntimeCompactionStrategy 这样合成实际配置(strategy.ts:91):

  private config(model: ProfileModelContext = this.context()): CompactionConfig {
    const triggerRatio = model.compactionTriggerRatio ?? DEFAULT_COMPACTION_CONFIG.triggerRatio;
    const blockRatio = Math.max(triggerRatio, DEFAULT_COMPACTION_CONFIG.blockRatio);
    return {
      ...DEFAULT_COMPACTION_CONFIG,
      triggerRatio,
      blockRatio,
      reservedContextSize:
        model.reservedContextSize ?? DEFAULT_COMPACTION_CONFIG.reservedContextSize,
    };
  }

阻塞比例永远不低于 0.85。默认两者相等,压缩是同步的:到点就停下来压,压完再走。若把触发比例调低,比如 0.7,就有了后台压缩:用量过 70% 即在后台开压(step 开始前查一次,checkAfterStep 为真时 step 结束后再查一次),回合照常推进,到 85% 才停下来等。

后台压缩的竞态由落地前的一道检查兜住(fullCompactionService.ts:875):

function historySafeToCompact(
  current: readonly ContextMessage[],
  original: readonly ContextMessage[],
): boolean {
  if (current.length < original.length) return false;
  if (!original.every((message, index) => message === current[index])) return false;
  return current.slice(original.length).every(isRealUserInput);
}

压缩期间历史只多了用户输入,结果才会应用;多出 assistant 或工具消息,这次压缩就作废。

压缩后的上下文

buildContextCompactionShape() 产出的消息顺序(compactionHandoff.ts:118):

    messages: [
      ...keptMessages,
      createCompactionSummaryMessage(contextSummary),
      continuationMessage,
    ],
  1. 保留的用户消息,逐字,受 token 预算约束;
  2. 一条 user 角色的摘要,以 COMPACTION_SUMMARY_PREFIX 开头,origin 为 compaction_summary;
  3. 一条续接提醒,包在 <system-reminder> 里:「Context compaction is complete — continue the work that was in progress when it began.」让模型接着干活,而不是从头再来。

assistant 消息、工具调用与工具结果全部丢弃。用户的原话是意图唯一的权威来源,不能被摘要走样。什么算用户消息看来源:origin 为 user,或由用户以斜杠命令触发的 skill、插件命令,才算真实输入;注入、shell 命令、后台任务、cron、hook 结果、重试一律不留。

HEAD + TAIL

export const COMPACTION_SUMMARY_PREFIX = summaryPrefixTemplate.trimEnd();
export const COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000;
export const COMPACT_USER_MESSAGE_HEAD_TOKENS = 2_000;
export const COMPACTION_ELISION_VARIANT = 'compaction_elision';
export const COMPACTION_CONTINUATION_VARIANT = 'compaction_continuation';

用户消息总量超过 2 万 token 时,保留最早的 2000 token(HEAD,原始任务通常在这里)与最近的 18000 token(TAIL,当前焦点),中间插一条省略说明,告诉模型大约省掉了多少 token、内容已由摘要覆盖。省略说明同样是 injection 来源、<system-reminder> 包裹的 user 消息。

这套规则只有一份:实时压缩(contextMemoryService.ts)与回放时折叠 context.apply_compaction 记录(contextOps.ts)调用的是同一个 buildContextCompactionShape()。否则「模型看到的历史」与「重放出来的历史」会分叉,排障时无从对账。

摘要之外:指回原始日志

摘要必然有损。v2 在摘要后面追加一段 ## Context Recovery(模板 fullCompaction/context-recovery-footer.md),告诉模型被压掉的内容仍在磁盘上:

## Context Recovery
Everything before this note is still on disk in this agent's event log (read-only, append-only):

后面列出该 agent 的 wire.jsonl 路径与各个压缩窗口对应的行号区间,并说明怎么 grep、怎么只读一行。需要确切的命令输出或原话时,模型可以回去查,不必凭摘要猜。若有待办列表,摘要末尾还会附上 ## TODO List。

压缩请求自身的失败

压缩本身是一次 LLM 调用,会遇到普通请求的全部失败。相关常量(fullCompactionService.ts:76):

export const MAX_COMPACTION_RETRY_ATTEMPTS = 5;
const DEFAULT_COMPACTION_MAX_COMPLETION_TOKENS = 128 * 1024;
const OVERFLOW_CONTEXT_SAFETY_RATIO = 0.85;
const OVERFLOW_STATUS_RECOVERY_RATIO = 0.5;
const MAX_COMPACTION_OVERFLOW_SHRINK_ATTEMPTS = 3;
const COMPACTION_OVERFLOW_SHRINK_RATIOS = [0.7, 0.5, 0.35] as const;
  • 压缩请求自己溢出:只保留最近的消息,依次缩到原 token 量的 70%、50%、35%,最多缩 3 次。
  • 摘要被截断或为空:丢掉最旧的一条消息(连同开头的工具结果)再试。
  • 可重试错误:退避重试。
  • 总尝试次数不超过 compaction_max_attempts,默认 5(v0.43.0 新增的配置项)。

发请求前先用 stripDynamicToolContext() 剥掉动态工具上下文,摘要用不上工具 schema。

micro compaction

v0.29 的 micro.ts 早已禁用,只留着配置当设计记录。v2 里它彻底没有了:没有文件,也没有配置项。

与循环的边界

压缩服务在三处接入循环:

接入点做什么
onWillBeginStep(full-compaction)检查阈值,必要时开压;达到阻塞比例就等压缩完成
onDidFinishStep清零连续溢出计数;触发与阻塞比例不同时检查是否开压
registerLoopErrorHandler上下文溢出时压缩,再 retry()

循环本身不执行压缩——「压缩是宿主层的重试关切,不是停止原因」这条边界在 v2 里依然成立。手动压缩(source: 'manual')要求循环空闲,拿不到 tryAcquireQuiescence() 就报错「Cannot compact while a turn is active…」。

下一篇:会话与存储。

本页目录