AI Coding 上下文摘要的递归压缩、原文保留与回退策略

📅
2 分钟阅读
·

本文是「Agent 开发实践与思考」系列第 16 篇。系列目录:

AI Coding 上下文摘要的递归压缩、原文保留与回退策略

编程 Agent 会持续读取文件、搜索、执行命令和修改代码。工具返回和构建日志会占用上下文窗口,并逐渐覆盖较早的用户要求。增大窗口只能延后这一问题,无法永久保留全部原始历史。

Claude Code、Gemini CLI 与 OpenHands 都会压缩较早的对话,并保留最近一段原文。再次达到阈值后,系统将旧摘要与新增对话再次压缩,在有限窗口内维护可继续执行任务的状态。

本文对照 Claude Code、Gemini CLI 和 OpenHands 的实现,讨论触发时机、压缩范围、摘要内容、摘要与消息的拼接方式,以及历史回退。

文中包含调研期间观察到的产品行为、请求样本和可读源码信息。Claude Code 的细节可能随版本调整,以下内容仅描述调研时的实现,不构成稳定接口说明。

递归摘要与最近原文的组合

长对话首次超过阈值时,系统将较早历史交给摘要模型,生成结构化摘要,同时保留接近当前任务的几轮原文。后续对话继续增长时,系统将上一次摘要、新产生的消息和更早的可压缩部分再次归纳,而不重新处理全部原始记录。

第一次压缩后,早期历史由摘要表示,最近原文仍保留在上下文中。第二次压缩时,系统将上一份摘要和新增的可压缩历史归纳为新摘要,并保留更新后的最近原文。

最近原文保留用户刚刚提出的要求、正在修改的文件、最新工具输出和未完成动作。摘要会损失细节,因此这些内容需要保留原文。实践中常见的保留范围为最近 5 到 20 轮,具体数量随上下文余量调整。

系统应先尝试生成摘要;摘要后仍超限时,再裁剪不重要的内容。提前裁剪可能删除用户意图或任务状态,后续无法从摘要中恢复。

上下文压缩的触发阈值

产品对阈值的选择不同。

Gemini CLI 的自动压缩阈值是模型上下文上限的 70%。它压缩较早的约 70% 历史,保留后面的约 30%,同时将切分位置移动到完整对话轮次的边界,避免在模型回复或工具返回中间截断。它也支持手动强制压缩。

Claude Code 的实现中,阈值约为可用上下文的 92%。它会保留最近若干轮原文;轮数根据剩余空间调整,不是固定常量。

70% 阈值会预留更多空间给摘要和后续工作,但需要更频繁地调用摘要模型。接近上限才触发可以减少压缩次数,却要求系统准确估计本轮输入、工具描述和模型输出将占用的 token。工程实现除百分比外,还应考虑系统提示词、工具定义、当前用户输入和预留输出长度。

Claude Code、Gemini CLI 与 OpenHands 的压缩实现

调研表除阈值外,还区分了压缩范围、保留或恢复的内容、首次和再次压缩的输入、摘要插入位置及提示词字段。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_goalkey_knowledgefile_system_staterecent_actionscurrent_plan。提示要求检查用户目标、Agent 操作、工具输出、文件改动和未解决问题,再输出高密度快照。后续 Agent 只能看到该快照所代表的历史,因此提示要求去掉对话填充,保留关键事实、计划、错误和指令。

Claude Code 的提示更长,输出仍为八段结构。除用户目标、技术概念、文件与代码段、错误与修复、问题处理、所有用户消息、待办和当前工作外,提示还要求给出紧贴当前任务的下一步。调研样本中,提示要求按时间顺序检查对话,特别保留文件名、代码片段、函数签名、文件改动、错误处理,以及用户否决过的做法。

OpenHands 不按固定业务字段划分摘要,而是将已有摘要和待遗忘事件组成输入。它保留事件边界和 ID,使运行时可以确定摘要替代的历史范围。RecentEventsCondenser 不使用 Prompt,只裁剪事件。

OpenHands 将压缩分为三层。ConversationWindowCondenser 处理显式压缩请求并保留重要的头尾事件,BrowserOutputCondenser 先限制浏览器输出,最后由 LLMSummarizingCondenser 生成摘要。该顺序减少送入摘要模型的浏览器正文,并限制无关页面内容进入长期上下文。

Coding Agent 摘要需要保留的状态

普通聊天摘要主要保留结论。Coding Agent 的摘要还需要支持后续任务继续执行,因此要保存目标、工作状态和待办。

Claude Code 的摘要提示要求覆盖以下信息:

  1. 用户最初的目标,以及后续明确修改过的要求。
  2. 技术栈、代码约定、架构决定和关键限制。
  3. 已读取、创建、修改、删除的文件,以及重要代码位置。
  4. 重要工具调用、报错、已经尝试过的修复和结果。
  5. 未完成的任务,以及当前正在进行的工作。

Gemini CLI 将这些信息固定为 XML:overall_goalkey_knowledgefile_system_staterecent_actionscurrent_plan。Claude Code 的结构更细,单列用户消息、错误与修复、当前工作和下一步。两者均按目标、事实、文件状态、过程和待办组织摘要。

字段约束有助于控制摘要内容。缺少结构时,模型可能将大量篇幅用于已完成的讨论,遗漏文件路径、失败原因或用户刚否决的方案。结构化摘要将这些信息显式列出,也为后续评估提供检查项。

以完整消息单元确定压缩边界

Agent 对话包含用户和助手消息、工具调用及工具结果。一次模型回复可能紧跟多个工具调用,工具结果又是下一步推理的依据。按 token 位置切分,可能留下没有调用来源的工具结果,或保留没有结果的工具请求。

压缩边界应以完整交互单元确定。助手消息、其工具调用和工具结果应一起进入摘要或一起保留。Gemini CLI 选择压缩边界时会继续向后寻找下一条用户消息,以避免破坏一轮对话。对于并行工具调用,边界还应覆盖同一批调用及其全部返回。

工具输出可以按类型处理。长日志、目录列表和网页正文通常首先占满上下文。OpenHands 先限制浏览器输出,再处理整段对话。先缩小明显噪音可以减少摘要模型输入,并降低无关信息进入长期记忆的概率。

摘要消息与用户界面中的历史

摘要通常作为一条合成消息插入系统提示词和当前用户问题之间,随后接保留的原始消息。Gemini CLI 会先放入环境上下文,再以用户消息写入结构化快照,并用一条简短的模型确认消息衔接后续历史。主模型接收到的仍是连续对话,不需要额外处理特殊存储协议。

用户界面是否展示摘要是另一项产品选择。

服务端隐藏摘要时,前端继续显示完整聊天记录,摘要只参与模型上下文拼接。用户看到的聊天历史不会被自动改写,但无法直接检查摘要是否遗漏信息。

另一种做法是将摘要返回插件端,使历史区域可以在原始消息和摘要之间切换。用户可以检查系统保留的信息,并在摘要错误时反馈;界面中需要将摘要与用户原话明确区分。

生成摘要后,原始消息仍应保留。摘要用于模型工作记忆,原始消息用于审计与回退。

递归摘要的回退和持久化

单次摘要的实现相对直接。用户回退到较早的对话分支,或同一会话连续压缩多次时,需要维护摘要与原始历史的对应关系。

一条摘要记录至少应包含:所属会话、生成摘要的对话轮次、压缩前后的 token 数、使用的策略、当前是否有效、失效原因,以及由用户还是系统触发。保存原始内容,或至少保存可追溯的轮次范围,才能在回退时找到受影响的摘要。

在递归摘要中,新摘要生效后,旧摘要仍可能对应用户后来切回的分支。实现可记录状态和失效原因,例如因生成新摘要失效或因用户回退失效。回退发生时,包含被回退轮次的摘要失效,再恢复该分支上最近仍可用的摘要。

需要区分两份数据:用户可见的完整对话,以及供模型调用的压缩上下文。前者是产品记录,后者是一次调用的派生状态。将二者放在同一份可变消息列表中,会增加回退、重放和排查的复杂度。

摘要模型的输入窗口、质量和延迟

摘要模型不一定需要使用能力最强的模型,但其上下文上限必须覆盖待压缩输入。摘要模型的上下文上限应大于主模型允许的历史输入,否则摘要模型会先拒绝请求。

选择摘要模型时,通常需要权衡输入窗口、摘要质量、延迟、成本和稳定性。摘要位于用户等待链路中,较高延迟会拉长正常问答;摘要质量不足会导致后续任务缺少必要状态。适合摘要的模型应输出稳定、速度足够快,并能处理主链路可能提交的最大历史段。

摘要提示词不应要求模型等量保留全部内容。较早讨论可以更短,重复工具输出和已无关的尝试可以省略;用户指令、未解决错误、文件改动和当前计划需要依据输入保留。摘要模型负责删除冗余,不能补全未见的事实。

上下文摘要的实现范围

上下文压缩包含消息筛选、边界选择、结构化摘要、递归替换、提示词拼接、持久化、回退和可观测性。

不同产品的实现细节不同,但都将早期历史压缩为摘要,并保留当前任务相关的最近原文。递归摘要记录已发生的工作,最近原文保留当前的用户要求、文件状态和工具输出。两部分分别维护,才能降低长任务在压缩后丢失任务状态的风险。


594 字 · 74 段落
ximing

Follow onGitHub

相关文章