# 上下文压缩：让 AI 拥有"无限记忆"的四种手段


<!-- more -->


Think-Act-Observe 循环有一个天然的敌人：**上下文窗口是有限的。**

每次循环迭代都在消耗 token——模型的思考、工具调用的请求、工具返回的结果。读一个大文件可能消耗几千 token，跑一次测试的输出可能上万。循环跑个十几轮，上下文窗口就被撑满了。

窗口满了怎么办？模型会说"抱歉，对话太长超出了我的上下文限制"。对普通聊天来说这无所谓——换个新窗口就行。但对编程助手来说，这意味着**失忆**——它忘了项目结构、忘了之前的修改、忘了用户的偏好。

Claude Code 的解法是一套四层上下文压缩管道。目标不是"塞更多"，而是**"保留最重要的，丢弃不重要的"**。

## 第一层：消息裁剪（Message Trimming）

最粗暴也最有效的一层。当对话历史接近窗口上限时，裁剪掉**最早的消息**。

这听起来简单，但有一个关键细节：不是所有早期消息都 equally 可裁剪。系统提示（system prompt）永远保留，因为它定义了 Agent 的行为规则。用户的第一条消息（初始请求）优先保留，因为它定义了任务目标。被裁剪的是中间的"过程消息"——那些已经执行完毕的工具调用和结果。

这背后的假设是：**已完成的工具调用的详细信息比未完成的任务描述更重要。** 你已经读了文件、改了代码、跑了测试——这些操作的"结果"体现在当前项目状态里，不需要在对话历史中保留完整记录。

消息裁剪的代价是模型"忘了细节"。它记得"我改过 src/main.ts"，但忘了具体改了什么。如果后续需要回看那个修改，模型可能需要重新读文件。这是可接受的——文件还在那里，对话历史里的副本是冗余。

## 第二层：对话压缩（Conversation Compaction）

裁剪是删，压缩是**概括**。

对话压缩的核心思路是：当对话历史太长时，调用模型自己对自己的前半段对话做一个**摘要**，用摘要替换原始对话。

具体流程大概是这样的：
1. 检测到对话历史超过某个阈值（比如窗口容量的 70%）
2. 将对话历史分成"已稳定的前半段"和"活跃的后半段"
3. 让模型对前半段生成一个结构化摘要：做了什么、发现了什么、决定了什么、还有什么没做
4. 用摘要替换前半段的原始消息
5. 保留后半段不变

这个过程在 Claude Code 里对应 `/compact` 命令——用户可以手动触发，系统也可以在必要时自动执行。

压缩的代价是**信息损失**。摘要不可能保留所有细节，一些微妙的上下文可能在概括中被丢弃。但和消息裁剪相比，压缩保留了**语义**而不仅仅是"操作记录"。裁剪后模型忘了"做了什么"，压缩后模型记得"做了什么事以及为什么"。

这本质上是一个**时间换空间**的交换——用模型的推理能力（生成摘要）换取上下文空间（摘要比原始对话短得多）。

## 第三层：上下文窗口管理（Window Management）

这一层不是压缩，是**规划**。它在每次模型调用前估算 token 用量，确保不超过窗口上限。

具体做法：
1. 计算当前对话历史的 token 数
2. 加上 system prompt 的 token 数
3. 预留模型输出的 token 空间（通常预留 4K-8K）
4. 如果总和超过窗口上限，触发裁剪或压缩
5. 确保最终发送给模型的请求在安全范围内

这一步像操作系统的内存管理——在分配新内存前先检查是否有足够空间，不够就回收。区别是操作系统的内存是均匀的（每字节一样），上下文的 token 是不均匀的（有些 token 承载的信息量远大于其他）。

窗口管理还有一个技巧：**动态预留**。不同的任务需要不同的输出空间。如果模型正在生成代码，预留多些；如果模型在回答问题，预留少点。Claude Code 会根据当前循环阶段（工具调用 vs. 文本回复）动态调整预留量。

## 第四层：智能注入（Smart Injection）

这是最精巧的一层。它不压缩已有内容，而是**控制新内容的注入量**。

前面聊过 system prompt 的分层架构——核心身份、工具定义、项目上下文、会话状态。每一层都占 token。智能注入的策略是：

- **核心身份**：永远全量注入（几百 token，值得）
- **工具定义**：只注入当前可能用到的工具（渐进式披露）
- **项目上下文**：只注入与当前任务相关的 CLAUDE.md 片段
- **记忆**：只注入相关性最高的几条（语义匹配）
- **Skill**：只注入已激活的 Skill

每一层都在做同样的事：**在"信息完整"和"token 节省"之间做取舍。** 这个取舍不是固定的，它取决于当前任务的性质。写代码时需要完整的工具定义，回答问题时可能只需要核心身份。

## 四层叠加的效果

四层管道叠加后的效果是：一个名义上 200K 上下文窗口的模型，可以处理等效于 500K 甚至 1000K token 的对话量——通过不断裁剪、压缩、重新注入。

代价是**渐进的信息损失**。随着对话越来越长，模型知道的越来越少。它从"知道每个文件的每行代码"退化到"知道项目的大致结构和当前任务"。如果任务足够复杂、对话足够长，模型最终可能退化到接近"初次见面"的状态。

这时最有效的操作不是压缩，而是**开一个新会话**。不是因为技术限制，而是因为信息损失已经积累到影响决策质量的程度。

## 上下文压缩的本质

上下文压缩的本质不是技术问题，是**认知科学问题**。

人类的记忆也在做类似的事。你不会记住昨天早餐的每个细节，但你会记住"昨天吃了早餐"。你不会记住每次会议的字幕，但你会记住"上周开会决定了用 TypeScript 重构"。大脑通过概括、遗忘、提取，在有限的神经资源下维持对世界的有效认知。

Claude Code 的上下文压缩管道是对人类记忆机制的粗糙模拟：
- 消息裁剪 ≈ 遗忘
- 对话压缩 ≈ 概括记忆
- 窗口管理 ≈ 注意力分配
- 智能注入 ≈ 选择性回忆

不精确，但方向对了。

## 一个未被解决的问题

上下文压缩有一个 Claude Code 没有完全解决的问题：**压缩时机的主观性。**

什么时候该裁剪？什么时候该压缩？什么时候该开新会话？当前实现依赖硬编码的阈值（比如"超过 70% 触发压缩"），但这些阈值不是万能的。一个 200K 窗口的模型和一个 200K 窗口的模型，信息密度可能完全不同——取决于项目大小、任务复杂度、工具返回的数据量。

理想的方案可能是**自适应阈值**——根据对话的信息密度动态调整压缩策略。信息密度高的对话（大量工具调用、大文件内容）更早触发压缩；信息密度低的对话（主要是文字交流）更晚触发。

但这需要实时评估"信息密度"，这本身又是一个未解决的研究问题。

## 延伸阅读：BYF 的 Observation Masking 与 CacheStakingStrategy

[BYF](https://github.com/ByronFinn/byf) 在上下文压缩上做了比 Claude Code 更精细的工作，将"上下文最小化"提升为"一级工程关注点"。

**1. 基于重要性的观察掩码。** 不是等到上下文快满才压缩，而是设置一个**阈值带**（60-85% token 压力），在不同压力等级下触发不同粒度的掩码。低压力时（60%）优先掩码 `Glob`/`Grep` 结果（低持久价值）；压力上升到 80% 时掩码 `Bash` 输出；`Write`/`Edit` 结果保留最久。掩码后的替换格式是紧凑的结构化摘要——`[Bash: 'npm test', exit=0, 127 lines, stderr: none]`——保留元数据和头尾片段，让模型可以决定是否需要重新读取完整输出。

**2. 输出卸载（Output Offloading）。** 超过 ~8000 token 的完整工具输出被写入临时文件，工具结果中只保留 1000 字符的预览 + 文件引用。BYF 对临时文件做了大小/数量限制（50MB/会话，最多 100 文件，FIFO 淘汰），防止无限制增长。

**3. Turn 边界缓存桩策略（CacheStakingStrategy）。** BYF 在 ADR 0011 中定义了"3+1"缓存桩模型——除了 system prompt 和工具数组的缓存边界外，新增了"上一轮最后一条助手消息"的缓存桩，将整个前序对话冻结到缓存中。在典型 CLI 会话中，这可以降低 50-80% 的输入 token 成本。更精巧的是，BYF 将这个策略抽象为 provider 无关的逻辑标签（`CacheHint`），Anthropic 适配器翻译为 `cache_control` 显式断点，OpenAI 适配器自动匹配前缀缓存。**同一策略，不同翻译**——这是"策略与实现分离"在缓存领域的体现。

## 下一篇

上下文压缩解决的是"短期记忆"的问题——当前会话里的信息怎么管理。但编程助手还需要**长期记忆**——跨会话记住用户偏好、项目约定、历史反馈。下一篇文章我们要拆解 Claude Code 的**记忆系统**：四种记忆类型、语义召回、持久化存储——AI 是怎么"记住你"的。

---

> 本系列基于 [Claude Code 官方源码](https://github.com/anthropics/claude-code)项目进行架构分析，聚焦设计思想而非代码实现。

