本文是「Agent 开发实践与思考」系列第 16 篇。系列目录:
- 2024
- 2025
- 01-28 长任务不失忆:把 Vue 转 React 做成可验证的迁移流程
- 02-25 MCP 用了三个月:工具标准化之后 Agent 设计变了什么
- 03-25 拆解 Coding Agent:为什么”写代码 + 文件系统”是通用 Agent 的内核
- 05-13 Agent 如何用代码完成推理、校验、适配与展示
- 06-03 能看会说的 Agent:语音交互与 Computer Use 这半年
- 06-24 从同步问答到事件驱动:异步 Agent 的架构改造笔记
- 07-08 没有评估就没有迭代:我的 Agent 评估落地记
- 09-09 后训练扫盲:SFT 记知识、RL 学长处,和 Agent 有什么关系
- 10-01 AI Coding 的上下文摘要:几家产品到底在压缩什么(本篇)
AI Coding 的上下文摘要:几家产品到底在压缩什么
写代码的 Agent 很容易把上下文窗口填满。读文件、搜索、执行命令、反复修改,都是必要过程,但工具返回和构建日志会很快挤掉最早的用户要求。窗口足够大只会把问题推迟一些,不能让历史一直原样留下。
几家 AI Coding 产品给出的答案已经很接近:压缩较早的对话,保留最近一段原文;下一次超过阈值时,把旧摘要和新增对话一起再压缩。这不是把聊天记录简单截断,而是在有限窗口里维护一份可继续工作的状态。
我把 Claude Code、Gemini CLI 和 OpenHands 的做法放在一起看,差异主要落在五件事上:何时触发、压缩哪一段、摘要写什么、摘要如何接回消息,以及历史能否回退。
这篇把调研中的产品行为、请求样本和可读源码信息整理进正文。Claude Code 的细节会随版本调整,下面记录的是调研时观察到的实现,不把它当作稳定接口。
先说共同形态:递归摘要加最近原文
长对话第一次超过阈值时,系统把较早的一段历史交给摘要模型,生成结构化摘要;离当前任务较近的几轮消息仍保留原文。之后对话继续增长,系统不必重新拿全部原始记录做摘要,而是将上一次的摘要、新产生的消息和更早的可压缩部分再次归纳。
第一次压缩后,早期历史变成摘要,最近原文仍然留在上下文里。第二次压缩时,系统将上一份摘要和之后新增的可压缩历史归纳成新摘要,再保留一段更新后的最近原文。
真正重要的是保留最近原文。摘要总会损失细节,最近几轮保留原文,是为了让 Agent 还看得到用户刚刚说过的话、正在修改的文件、工具的最新输出和未完成的动作。实践里常见的保留范围是最近 5 到 20 轮,具体数字会随上下文余量调整。
这个结构也给了异常时的降级路径。应当先尝试摘要,摘要后仍然超限再裁剪不重要的内容。把裁剪放在前面,可能直接删掉用户意图或任务状态,之后没有办法从摘要中找回来。
触发点没有统一答案
产品对阈值的选择不同。
Gemini CLI 的自动压缩阈值是模型上下文上限的 70%。它压缩较早的约 70% 历史,保留后面的约 30%,同时会把切分位置移动到完整对话轮次的边界,避免截断在模型回复或工具返回中间。它也支持手动强制压缩。
Claude Code 的实现中,阈值约为可用上下文的 92%。它会保留最近若干轮原文,轮数不是固定常量,而是根据剩余空间调整。
早触发和晚触发各有代价。70% 触发得早,给摘要和后续工作留了更多余量,但调用摘要模型更频繁。接近上限再触发,压缩次数会少一些,却要求系统准确估计本轮输入、工具描述和模型输出将占多少 token。工程上不该只写一个百分比,还要考虑系统提示词、工具定义、当前用户输入和预留输出长度。
调研里的完整对比
原始调研表不只记录了阈值,还区分了压缩范围、保留或恢复的内容、首次和再次压缩的输入、摘要插入位置,以及提示词的字段。下面按这些字段展开。Claude Code 的部分来自调研时的请求样本与逆向记录,版本变化后可能不同;Gemini CLI 和 OpenHands 的部分可与当时的实现对应。
| 产品 | 压缩时机和阈值 | 压缩多少,保留或恢复多少 | 首次和多次压缩策略 | 压缩后的消息拼接 | 压缩 Prompt 或结构 |
|---|---|---|---|---|---|
| Gemini CLI | 自动阈值是模型窗口的 70%,也支持强制压缩。 | 压缩约前 70%,保留约后 30%。切点会向后移动到下一条用户消息,不会在模型回复或工具响应中间断开。 | 首次将前 70% 历史压成结构化摘要 1。再次把摘要 1、保留的约 30% 历史和新增对话中的前段压成摘要 2,继续保留后 30%。 | 先建立环境上下文,再将摘要作为一条用户消息写入,插入一条简短的模型确认消息,最后接保留的原始历史。 | 五段 XML:目标、关键知识、文件系统状态、近期动作、当前计划。 |
OpenHands 的 LLMSummarizingCondenser |
当历史事件数超过 max_size 时触发,默认 max_size = 100;单个事件还可设置最大长度。 |
默认保留头部 1 个事件。目标历史大小为 max_size / 2,其中一格给摘要事件,剩余位置给尾部事件。 |
首次总结头尾之间的中间事件。再次压缩时,已有摘要事件和本次需要遗忘的中间事件一起进入提示词,新摘要替换旧摘要。 | 历史固定成“头部事件 + 摘要事件 + 尾部事件”;摘要还记录被遗忘事件的起止 ID 和插入位置。 | 以已有摘要和待遗忘事件为输入,保留事件边界和 ID,不使用固定字段模板。 |
OpenHands 的 RecentEventsCondenser |
按配置执行,不调用摘要模型。 | 默认保留头部 1 个事件和尾部 9 个事件。 | 每次都直接舍弃中间事件,不递归总结。 | 返回由头部和尾部组成的新事件视图,没有摘要消息。 | 无 Prompt,只按事件位置裁剪。 |
| Claude Code | 自动阈值为可用上下文的 92%;用户执行 /compact 或系统检测到内存压力时也会压缩。 |
压缩输入会先过滤进度、系统等消息。压缩后保留系统和元消息,恢复最多 5 个重要文件和最近 5 条消息。另一份重建指南把最近消息数做成配置,默认是 10。 | 首次将过滤后的完整上下文交给摘要模型。再次将摘要 1、恢复的文件和新增消息过滤后再压成摘要 2。 | 将八段结构化摘要作为一条标记为压缩摘要的消息,后接恢复的文件和待办。界面展示压缩后的消息列表,原始历史保存到后台。 | 八段:用户目标、技术概念、文件和代码段、错误与修复、问题处理、全部用户消息、待办、当前工作;提示还要求下一步。 |
各家的摘要 Prompt 和结构
Gemini CLI 固定为五段 XML:overall_goal、key_knowledge、file_system_state、recent_actions 和 current_plan。提示要求先检查用户目标、Agent 操作、工具输出、文件改动和未解决问题,再输出高密度快照。它明确把快照当成后续 Agent 唯一可见的历史,因此要求去掉对话填充,保留关键事实、计划、错误和指令。
Claude Code 的提示更长,最终仍是八段结构。除了用户目标、技术概念、文件与代码段、错误与修复、问题处理、所有用户消息、待办和当前工作外,还会要求给出紧贴当前任务的下一步。调研样本中,提示要求按时间顺序检查对话,特别保留文件名、代码片段、函数签名、文件改动、错误处理,以及用户否决过的做法。
OpenHands 的摘要格式不是按固定的业务字段分段,而是把已有摘要和待遗忘事件组成输入。它保留事件的边界和 ID,使运行时知道这一条摘要替代了哪一段历史。RecentEventsCondenser 则没有 Prompt,因为它只做事件裁剪。
OpenHands 还把压缩拆成三层。ConversationWindowCondenser 处理显式压缩请求并保留重要的头尾事件,BrowserOutputCondenser 先限制浏览器输出,最后才由 LLMSummarizingCondenser 生成摘要。这个顺序会减少送进摘要模型的浏览器正文,也避免无关页面内容进入长期上下文。
摘要不是会议纪要
普通聊天摘要关心结论,Coding Agent 的摘要需要能让另一个时刻的 Agent 接着干活。因此它要保存的内容更像工作现场的交接记录。
Claude Code 的摘要提示要求覆盖的信息包括:
- 用户最初的目标,以及后续明确修改过的要求。
- 技术栈、代码约定、架构决定和关键限制。
- 已读取、创建、修改、删除的文件,以及重要代码位置。
- 重要工具调用、报错、已经尝试过的修复和结果。
- 未完成的任务,以及此刻正在做的事。
Gemini CLI 把这些字段固定成 XML:overall_goal、key_knowledge、file_system_state、recent_actions 和 current_plan。Claude Code 的结构更细,会单列用户消息、错误与修复、当前工作和下一步。两者都围绕目标、事实、文件状态、过程和待办组织摘要。
格式并不决定质量,字段约束却很有用。没有结构时,模型很容易把大量篇幅花在已经完成的讨论上,漏掉一个文件路径、一次失败原因或用户刚刚否决的方案。结构化摘要把这些容易丢的东西显式列出来,也让后续评估有了明确检查项。
压缩对象要按消息边界选
Agent 对话不是只有用户和助手两种文本。一次模型回复可能紧跟多个工具调用,工具结果又是下一步推理的依据。如果压缩时只按 token 位置切分,很容易留下一个没有调用来源的工具结果,或保留一个没有结果的工具请求。
所以切分应该以完整交互单元为界。助手消息和其工具调用、工具结果要一起进入摘要或一起保留。Gemini CLI 在选择压缩边界时会继续向后寻找下一条用户消息,本质上就是避免破坏一轮对话。对有并行工具调用的实现,边界还需要覆盖同一批调用及其全部返回。
工具输出也不必一视同仁。长日志、目录列表和网页正文通常是最先膨胀的部分,OpenHands 的思路是先对浏览器输出做限制,再处理整段对话。把明显噪音在进入主摘要前缩小,可以减少摘要模型的输入,也降低无关信息进入长期记忆的概率。
摘要放在哪里
摘要通常作为一条合成消息插入系统提示词和当前用户问题之间,再接上保留下来的原始消息。Gemini CLI 会先放入一段环境上下文,再以用户消息写入结构化快照,并用一条简短的模型确认消息衔接之后的历史。这样主模型拿到的上下文仍是一段正常的对话,而不是在运行时额外理解一套特殊存储协议。
用户界面是否展示摘要,则是另一层选择。
服务端隐藏摘要的做法是,前端继续显示完整聊天记录,摘要只用于拼接给模型。这种方式对用户最自然,聊天历史没有被自动改写,但用户也无法检查摘要是否漏了东西。
另一种做法是把摘要返回给插件端,历史区域可以在原始消息和摘要之间切换。用户能看到系统到底记住了什么,摘要出错时也有机会反馈;代价是界面里出现了一段并非用户原话的内容,必须把它和原记录区分清楚。
无论哪种界面,原始消息都不该因为生成了摘要就直接丢弃。摘要是模型的工作记忆,不是审计记录。
回退和持久化比摘要本身更麻烦
只做一次摘要并不难。难的是用户回退到较早的对话分支,或者在同一个会话里连续压缩多次。
一条摘要记录至少应当知道:它属于哪个会话、由哪些对话轮次生成、压缩前后各有多少 token、使用什么策略、当前是否有效、为何失效、由用户还是系统触发。保存原始内容或至少保存可追溯的轮次范围,才能在回退时找到受影响的摘要。
递归摘要场景下,新的摘要生效后,旧摘要未必应该被物理删除。它可能仍对应用户后来切回的一个分支。比较稳妥的做法是记录状态和失效原因:因生成新摘要失效,还是因用户回退失效。回退发生时,包含被回退轮次的摘要失效,再恢复那条分支上最近仍可用的摘要。
这里还要区分两份数据:用户可见的完整对话和供模型使用的压缩上下文。前者是产品记录,后者是一次调用的派生状态。把两者混在同一份可变消息列表中,后面做回退、重放和排查都会很痛苦。
摘要模型的约束
摘要模型不一定需要最强,但必须能吃下要压缩的输入。它的上下文上限至少要大于主模型允许的历史输入,否则主模型还没满,摘要模型先拒绝了请求。
选择时通常在五个维度间权衡:输入窗口、摘要质量、延迟、成本和稳定性。摘要会出现在用户等待链路上,过慢会直接拉长一次正常问答;摘要质量太差,后续任务又会出现无缘由的遗忘。适合摘要的模型应当输出稳定、速度足够快,并能处理主链路可能交给它的最大历史段。
摘要提示词也不应鼓励模型把所有内容同等保留。较早的讨论可以更短,重复工具输出和已经无关的尝试可以省略,但用户指令、未解决错误、文件改动和当前计划不能靠猜。摘要模型的职责是删去冗余,不是补全它没有看到的事实。
最后
上下文压缩不是一个 summarize() 调用。它至少包含消息筛选、边界选择、结构化摘要、递归替换、提示词拼接、持久化、回退和可观测性。
几家产品的细节不同,但收敛到同一条原则:旧历史可以变短,当前工作不能只剩一段模糊概述。递归摘要保存已经发生过什么,最近原文保住此刻正在发生什么。把这两部分分开,长任务才不会每次压缩后都像换了一个人继续做。

