上下文压缩:让 AI 拥有"无限记忆"的四种手段
Think-Act-Observe 循环有一个天然的敌人:上下文窗口是有限的。
每次循环迭代都在消耗 token——模型的思考、工具调用的请求、工具返回的结果。读一个大文件可能消耗几千 token,跑一次测试的输出可能上万。循环跑个十几轮,上下文窗口就被撑满了。
窗口满了怎么办?模型会说"抱歉,对话太长超出了我的上下文限制"。对普通聊天来说这无所谓——换个新窗口就行。但对编程助手来说,这意味着失忆——它忘了项目结构、忘了之前的修改、忘了用户的偏好。
Claude Code 的解法是一套四层上下文压缩管道。目标不是"塞更多",而是**“保留最重要的,丢弃不重要的”**。
第一层:消息裁剪(Message Trimming)
最粗暴也最有效的一层。当对话历史接近窗口上限时,裁剪掉最早的消息。
这听起来简单,但有一个关键细节:不是所有早期消息都 equally 可裁剪。系统提示(system prompt)永远保留,因为它定义了 Agent 的行为规则。用户的第一条消息(初始请求)优先保留,因为它定义了任务目标。被裁剪的是中间的"过程消息"——那些已经执行完毕的工具调用和结果。
这背后的假设是:已完成的工具调用的详细信息比未完成的任务描述更重要。 你已经读了文件、改了代码、跑了测试——这些操作的"结果"体现在当前项目状态里,不需要在对话历史中保留完整记录。
消息裁剪的代价是模型"忘了细节"。它记得"我改过 src/main.ts",但忘了具体改了什么。如果后续需要回看那个修改,模型可能需要重新读文件。这是可接受的——文件还在那里,对话历史里的副本是冗余。
第二层:对话压缩(Conversation Compaction)
裁剪是删,压缩是概括。
对话压缩的核心思路是:当对话历史太长时,调用模型自己对自己的前半段对话做一个摘要,用摘要替换原始对话。
具体流程大概是这样的:
- 检测到对话历史超过某个阈值(比如窗口容量的 70%)
- 将对话历史分成"已稳定的前半段"和"活跃的后半段"
- 让模型对前半段生成一个结构化摘要:做了什么、发现了什么、决定了什么、还有什么没做
- 用摘要替换前半段的原始消息
- 保留后半段不变
这个过程在 Claude Code 里对应 /compact 命令——用户可以手动触发,系统也可以在必要时自动执行。
压缩的代价是信息损失。摘要不可能保留所有细节,一些微妙的上下文可能在概括中被丢弃。但和消息裁剪相比,压缩保留了语义而不仅仅是"操作记录"。裁剪后模型忘了"做了什么",压缩后模型记得"做了什么事以及为什么"。
这本质上是一个时间换空间的交换——用模型的推理能力(生成摘要)换取上下文空间(摘要比原始对话短得多)。
第三层:上下文窗口管理(Window Management)
这一层不是压缩,是规划。它在每次模型调用前估算 token 用量,确保不超过窗口上限。
具体做法:
- 计算当前对话历史的 token 数
- 加上 system prompt 的 token 数
- 预留模型输出的 token 空间(通常预留 4K-8K)
- 如果总和超过窗口上限,触发裁剪或压缩
- 确保最终发送给模型的请求在安全范围内
这一步像操作系统的内存管理——在分配新内存前先检查是否有足够空间,不够就回收。区别是操作系统的内存是均匀的(每字节一样),上下文的 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 在上下文压缩上做了比 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 官方源码项目进行架构分析,聚焦设计思想而非代码实现。