Agent 上下文压缩设计笔记
上下文压缩解决什么问题
Agent 的上下文窗口不是无限的。随着多轮对话、工具调用、文件读取、报错日志和代码 diff 不断累积,模型会逐渐接近上下文上限。上下文压缩的目标不是简单地“变短”,而是在尽量少损失任务连续性的前提下,把历史对话整理成下一轮 Agent 可以继续工作的状态。
可以把上下文压缩理解为一次“工作交接”:
- 保留用户真正想做什么
- 保留项目约束、技术栈和关键决策
- 保留已经读过、改过、创建过的文件状态
- 保留报错、修复方案和仍未解决的问题
- 丢弃重复、过时、冗长的工具输出
- 让新的上下文窗口可以接着做,而不是重新探索
一个好的压缩系统应该回答三个问题:
- 什么时候压缩:由 token 阈值、消息长度、工具输出规模等调度策略决定
- 压缩什么:决定保留用户消息、系统约束、工具结果、文件状态还是计划
- 如何压缩:使用 LLM 摘要、规则裁剪、检索重建,或组合方案
经典方案一:LLM 摘要压缩
Claude Code 和 Gemini CLI 都采用了一个重要思路:当上下文过长时,把历史消息交给一个模型,让模型输出结构化摘要。这个摘要会成为新上下文窗口中的核心记忆。
这类方案的优点是语义保留能力强,能够把分散在历史中的目标、约束、错误和计划重新组织起来。缺点是压缩结果依赖模型判断,如果提示词设计不好,可能丢失文件路径、代码片段、用户偏好或未完成任务。
Claude Code 风格:详细结构化摘要
Claude Code 的压缩提示词偏“完整交接文档”。它强调按时间顺序分析历史,并关注用户请求、技术细节、文件变更、错误修复和下一步。
适合保留的字段可以设计为:
| 字段 | 作用 |
|---|---|
| 主要请求和意图 | 保留用户最初目标和后续意图变化 |
| 关键技术概念 | 记录技术栈、框架、架构模式、依赖 |
| 文件和代码部分 | 记录读过、改过、创建过的文件,以及关键代码片段 |
| 错误和修复 | 避免压缩后重复踩坑 |
| 问题解决 | 区分已经解决的问题和仍在排查的问题 |
| 用户消息 | 保留用户原始反馈,减少意图被摘要扭曲 |
| 待处理任务 | 让 Agent 知道还有哪些明确任务没做 |
| 当前工作 | 记录压缩发生前正在做什么,停在哪里 |
| 可选下一步 | 只保留与当前任务直接相关的后续动作 |
这个方案的核心不是“总结得漂亮”,而是“让下一个上下文窗口能继续干活”。尤其是 coding agent 场景,文件路径、函数名、测试命令、失败日志和用户纠正非常关键。
可以抽象成下面的压缩模板:
请将历史对话压缩为一份可继续执行任务的工作交接摘要。
必须保留:
1. 用户的主要目标和明确请求
2. 项目技术栈、架构约束和关键决策
3. 已读取、修改、创建、删除的文件及其原因
4. 关键代码片段、函数签名、配置项
5. 已遇到的错误、报错信息、修复方式
6. 用户的重要反馈和偏好
7. 已完成事项、待处理事项、当前停顿位置
8. 下一步建议,但只能包含与当前任务直接相关的动作
必须删除:
1. 重复解释
2. 过时的工具输出
3. 对后续没有帮助的中间尝试
4. 无关寒暄
Gemini CLI 风格:状态快照
Gemini CLI 的压缩提示词更像是生成一个精简的 state_snapshot。它保留的字段更少,但密度更高。
典型字段包括:
| 字段 | 作用 |
|---|---|
overall_goal | 用一句话描述用户的高层目标 |
key_knowledge | 记录必须记住的事实、约束、约定 |
file_system_state | 记录文件系统层面的创建、读取、修改、删除 |
recent_actions | 记录最近关键动作和结果 |
current_plan | 记录当前计划,以及哪些步骤已完成 |
这个方案适合做“运行状态快照”,尤其适合 Agent 在任务中断后恢复执行。它比 Claude Code 风格更短,但对细节保留的要求更严格。
可以抽象成:
<state_snapshot>
<overall_goal>用户当前想完成的高层目标</overall_goal>
<key_knowledge>关键事实、约束、偏好、技术决策</key_knowledge>
<file_system_state>文件读取、修改、创建、删除状态</file_system_state>
<recent_actions>最近执行过的重要动作及结果</recent_actions>
<current_plan>当前计划、已完成步骤、未完成步骤</current_plan>
</state_snapshot>
经典方案二:工具消息裁剪
在真实 Agent 系统里,最占上下文的往往不是用户消息,也不是助手回复,而是工具调用结果。例如读取文件、搜索代码、运行测试、查看日志,都会产生大量文本。
因此,工具消息裁剪是非常实用的压缩策略:
- 保留系统消息
- 保留普通用户消息和助手消息
- 删除过时的工具调用和工具结果
- 只保留最近 N 轮工具调用
- 对关键工具结果先摘要,再删除原始长输出
一个简单策略是:识别所有工具调用轮次,只保留最后 N 轮工具调用,其余工具输入和输出全部移除。
伪代码如下:
type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
interface Message {
role: MessageRole;
content: string;
tool_calls?: unknown[];
tool_call_id?: string;
}
interface CompressionOptions {
enabled: boolean;
keepLastToolRounds: number;
}
function compressToolMessages(
messages: Message[],
options: CompressionOptions
): Message[] {
if (!options.enabled) return messages;
const toolRounds = identifyToolRounds(messages);
const roundsToKeep = toolRounds.slice(-options.keepLastToolRounds);
const keepIndexes = new Set(roundsToKeep.flatMap(round => round.indexes));
return messages.filter((message, index) => {
if (message.role === 'system') return true;
if (keepIndexes.has(index)) return true;
const isToolRelated =
message.role === 'tool' ||
(message.role === 'assistant' && Boolean(message.tool_calls));
return !isToolRelated;
});
}
这个方案的关键判断是:工具输出是不是还能帮助后续决策。如果已经被模型吸收成结论,或者只是中间探索结果,就可以删;如果是最新测试结果、关键报错、重要文件内容,则应该保留或先摘要。
经典方案三:中间移除、最旧移除与混合策略
除了让 LLM 总结,也可以用规则算法直接裁剪消息。这种方案更可控、成本更低,但语义理解能力弱一些。
常见三种裁剪方式:
| 策略 | 做法 | 适用场景 |
|---|---|---|
| 中间移除 | 保留开头和结尾,删除中间消息 | 开头有系统约束、结尾有当前任务 |
| 最旧移除 | 从最早消息开始删除,保留最近消息 | 长对话、近期上下文最重要 |
| 混合策略 | 根据对话特征动态选择 | 不同模型、不同任务混合使用 |
中间移除策略
中间移除适合这种结构:
开头:系统提示词、项目规则、用户目标
中间:大量工具调用、搜索过程、尝试过程
结尾:当前问题、最近代码、最新错误
它的优势是保留“任务框架”和“当前现场”。缺点是中间可能包含关键决策,如果没有先做摘要,容易丢失重要信息。
最旧移除策略
最旧移除更像传统滑动窗口。它默认最近消息最重要,适合长对话持续推进的场景。
它的优势是简单直接,能保持当前任务连续性。缺点是可能丢掉早期用户约束、架构决策或项目目标。
混合策略
混合策略可以根据以下特征选择:
- 当前 token 数与目标 token 数的压缩比例
- 消息总数
- 最近几条消息占总 token 的比例
- 是否包含长消息
- 是否包含系统消息
- 是否包含大量工具消息
- 当前使用的模型和上下文窗口大小
一个可落地的选择规则:
| 条件 | 推荐策略 | 原因 |
|---|---|---|
| 轻度压缩且对话较短 | 中间移除 | 开头和结尾通常最重要 |
| 重度压缩且对话很长 | 最旧移除 | 最新上下文优先级更高 |
| 最近消息 token 占比很高 | 中间移除 | 需要保护最近现场 |
| 有系统消息或工具消息 | 中间移除 | 保留开头规则和结尾状态 |
| 不确定 | 同时试两种,按评分选择 | 用数据而不是拍脑袋 |
可以用一个简单评分函数评估裁剪结果:
效率分数 = token 减少率 * 0.6 + 消息保留率 * 0.4
如果系统更重视“压到目标 token 以下”,就提高 token 减少率权重;如果系统更重视“少丢上下文”,就提高消息保留率权重。
推荐的组合式压缩架构
单一压缩方式往往不够稳。更适合 Agent 的做法是组合:
原始历史消息
↓
统计 token 和消息结构
↓
判断是否达到压缩阈值
↓
先裁剪过时工具消息
↓
对关键历史做 LLM 结构化摘要
↓
生成 state snapshot / handoff summary
↓
重建新上下文窗口
推荐保留四层上下文:
| 层级 | 内容 | 存放方式 |
|---|---|---|
| 稳定规则层 | 系统提示词、项目规则、安全约束 | 常驻 prompt 或规则文件 |
| 工作记忆层 | 当前目标、计划、待办、用户偏好 | 结构化摘要 |
| 证据层 | 最新工具结果、关键错误、关键代码片段 | 最近 N 轮工具消息或摘要 |
| 外部知识层 | 文档、代码库、历史记录 | RAG / 文件检索 |
压缩后新上下文可以这样组织:
系统提示词
项目规则
压缩说明开篇语
结构化摘要
最近几轮完整对话
最近关键工具结果
当前用户请求
其中“最近几轮完整对话”很重要。摘要可以保留大局,但最新几轮的原始表达通常包含微妙意图、语气、纠正和边界条件。
压缩提示词设计要点
设计压缩 prompt 时,重点不是让模型自由发挥,而是给它一个稳定的交接格式。
建议包含:
- 明确角色:你是上下文压缩器,不是任务执行者
- 明确目标:生成下一轮 Agent 可以继续工作的状态
- 明确保留项:目标、约束、文件、代码、错误、计划、用户反馈
- 明确删除项:重复内容、无关工具输出、寒暄、中间噪声
- 明确输出格式:Markdown、XML、JSON 或自定义标签
- 明确禁止行为:不要编造文件状态,不要添加未发生的决策,不要开始执行下一步
一个实用压缩 prompt:
你是 Agent 的上下文压缩器。
请把历史对话压缩成一份中文工作交接摘要。这个摘要将成为新上下文窗口继续执行任务的主要依据。
必须保留:
- 用户的主要目标、明确请求和重要反馈
- 技术栈、项目约束、架构决策、工具偏好
- 已读取、修改、创建、删除的文件路径
- 关键代码片段、函数名、配置项、命令
- 已遇到的错误、失败测试、修复过程
- 已完成任务、未完成任务、当前停顿位置
- 下一步建议,但只能包含与当前任务直接相关的动作
必须删除:
- 重复解释
- 无关寒暄
- 已无价值的工具输出
- 没有影响最终决策的中间尝试
不要编造历史中没有出现的信息。
不要执行任务,只输出压缩摘要。
工程落地建议
触发时机
可以在这些情况下触发压缩:
- 当前 token 超过模型上下文窗口的 70% 到 85%
- 单次工具输出超过阈值
- 工具调用轮次超过阈值
- 任务阶段完成,需要生成阶段性 handoff
- 用户主动输入
/compact或类似命令
压缩顺序
推荐顺序:
- 先清理明显无价值的工具输出
- 再保留最近 N 轮完整对话
- 对旧消息生成结构化摘要
- 将摘要、规则、最近消息重新组装为新上下文
- 记录压缩统计,如压缩前后 token、删除消息数、保留工具轮次
风险控制
上下文压缩最常见的失败不是“压缩率不够”,而是“关键事实丢失”。尤其要防止:
- 丢失用户明确限制
- 丢失文件路径
- 丢失最新报错
- 丢失已经尝试过但失败的方案
- 把推测写成事实
- 把已完成任务和待办任务混在一起
因此,压缩结果最好保留“状态标签”:
[已完成] 修复登录页表单校验
[失败尝试] 直接修改 schema 会破坏旧接口
[待确认] 是否保留旧版导出格式
[下一步] 运行 pnpm test 验证 auth 模块
我的总结
上下文压缩本质上是 Agent 的“记忆管理”和“工作交接系统”。Claude Code 风格更适合保留完整开发上下文,Gemini CLI 风格更适合生成高密度状态快照,工具消息裁剪则是最直接有效的 token 降噪方案。
如果要实现一个稳定的 Agent 压缩模块,我会优先选择这套组合:
最近对话完整保留
+ 过时工具消息裁剪
+ LLM 结构化摘要
+ 文件状态快照
+ 当前计划和待办列表
+ 压缩统计和可观测日志
最终目标不是让上下文最短,而是让 Agent 在压缩之后仍然知道:用户要什么、项目是什么、我做过什么、哪里失败过、现在停在哪里、下一步该怎么走。