# 上下文压缩

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

- 作者：David（道雾轩）
- 专栏：Kimi Code 源码解读（https://daiw.org/manual/kimi-code.md）
- 最后更新：2026-09-19
- 原文：https://daiw.org/manual/kimi-code/compaction
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

## 配置与阈值

```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`）：

```ts
  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`）：

```ts
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`）：

```ts
    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

```ts
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`），告诉模型被压掉的内容仍在磁盘上：

```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`）：

```ts
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()` 剥掉[动态工具上下文](https://daiw.org/manual/kimi-code/context)，摘要用不上工具 schema。

## micro compaction

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

## 与循环的边界

压缩服务在三处接入[循环](https://daiw.org/manual/kimi-code/agent-loop)：

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

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

下一篇：[会话与存储](https://daiw.org/manual/kimi-code/session)。
