加载中...
  • 上下文快爆了怎么办:压缩、预算与 Offload loading

    吃透 AI Agent 开发 · 第 17 篇 · 第四章 · Context Engineering

    对话、工具输出和子任务结果都会增长。压缩保留语义,预算控制注入,Offload 保存完整正文,三者不能混为一谈。

    你和 Agent 讨论了半小时,明确说过“不要改数据库结构”。它前面一直遵守,后面却突然提出新增字段。看上去像模型忘了,其实更可能是旧消息已经装不进这次请求,或者压缩摘要漏掉了约束。

    上下文窗口再大也不是无限的。更大的窗口只是把问题推迟,不能替代治理。

    工作台、便签和档案柜

    可以把上下文想成一张工作台。

    • 正在处理的文件摊在桌面上。
    • 已经讨论过的结论写成便签。
    • 十万行日志放进档案柜,只在桌面留编号和摘要。
    • 不再相关的草稿从桌上拿走。

    压缩负责把一摞历史整理成便签;预算负责限制每类材料占多少空间;Offload 负责把大文件移到档案柜。三者解决的问题不同。

    先知道钱花在哪里

    不要只看一个“上下文用了 80%”。至少要按来源统计:

    interface ContextUsage {
      system: number
      history: number
      toolResults: number
      projectFiles: number
      memory: number
      reservedForOutput: number
    }
    

    如果大头是工具结果,压缩聊天历史帮助有限;如果稳定规则本身占了一半,频繁总结也治标不治本。预算报告让优化从猜测变成定位。

    还要为模型输出预留空间。把窗口全部塞满输入,模型可能没有足够 token 完成回答或生成工具参数。

    什么时候压缩

    常见触发点有三个:

    1. 请求前检查。 发现下一次请求会超预算,先压缩再调用模型。
    2. 一轮结束后整理。 当前任务阶段刚完成,适合生成边界清楚的摘要。
    3. 用户主动触发。 长会话进入新阶段,用户明确要求整理上下文。

    请求前压缩最必要,却也是体验最敏感的时刻。应该保留取消信号和进度事件,避免界面看起来突然卡住。

    一份合格摘要要保留什么

    摘要不是“把聊天变短”,而是生成后续执行所需的状态。至少包括:

    • 当前目标和完成标准。
    • 用户明确约束与禁止事项。
    • 已确认的事实和关键决定。
    • 已修改的文件或外部状态。
    • 未完成事项和下一步。
    • 尝试过但失败的方法,以及失败原因。

    最后一项很重要。只记录成功结论,Agent 可能在压缩后重新走一遍已经证明不通的路。

    可以让摘要使用固定结构,而不是一段自由散文:

    目标:
    约束:
    已完成:
    关键决定:
    失败尝试:
    待办:
    

    结构稳定也方便写自动检查,例如摘要中必须出现所有未完成任务 ID。

    Offload:完整内容不必进模型

    Shell 输出十万行时,最差的做法是先全部塞进消息,再指望下一轮压缩。更好的协议是工具执行完成后就判断体积:

    interface LargeResultHandle {
      preview: string
      artifactFile: string
      originalChars: number
      truncated: true
      sha256: string
    }
    

    模型先看到错误附近的片段、总字符数和文件句柄。需要更多信息时,再调用读取工具按行恢复。完整数据仍然存在,只是不常驻上下文。

    这个模式同样适用于子 Agent 报告、网页正文、数据库导出和评测结果。

    记忆也要有预算和新鲜度

    长期记忆不是免费外挂。一次注入二十份“可能相关”的旧笔记,会把当前问题淹没。

    实用流程是:先只读取标题、描述、类型和更新时间;根据当前问题选择少量候选;最后在单文件、单轮和单会话预算内加载正文。注入时带上来源和年龄,提醒模型旧信息需要验证。

    “用户两年前偏好 npm”不能自动压过当前仓库里的 pnpm-lock.yaml。长期记忆是线索,不是高于现场证据的事实。

    四种压缩失败

    约束被总结成背景

    “用户提到不要改数据库”听起来像历史描述,不像仍然有效的约束。摘要应写成“禁止修改数据库结构”。

    状态和正文混在一起

    把完整代码粘进摘要,会迅速让摘要再次膨胀。状态保留路径、符号名和关键 diff,正文留在文件中。

    压缩结果覆盖原始记录

    摘要用于后续上下文,不应删除原始会话。原始记录是审计和重新压缩的依据。

    每轮都压缩

    压缩本身需要模型调用,也会逐步损失细节。没有达到阈值时反复总结,只会增加成本和信息损耗。

    用 On-demand Restore 做个小练习

    准备一段包含目标、两个约束、一次失败尝试和三个待办的对话。写一个 validateSummary

    function validateSummary(summary: string, taskIds: string[]): string[] {
      return taskIds.filter((id) => !summary.includes(id))
    }
    

    让摘要漏掉一个任务 ID,确认验证能发现。然后再思考:自然语言约束应该怎样结构化,才能做类似检查?

    Long History 到 On-demand Restore 的小结

    上下文治理不是定期“删聊天”。先用预算看清来源,再用摘要保存执行状态,用 Offload 保存大体积原文,用带时间和来源的选择机制注入记忆。这样窗口变小了,任务连续性反而更强。

    下一篇轮到 Agent 的手脚:工具。我们会看到,真正重要的不是多写几个函数,而是让所有副作用经过同一个可控制、可观察的执行入口。

    用预算账本决定压缩顺序

    上下文接近上限时,先删什么不能靠感觉。稳定规则、当前任务、最近工具结果和历史闲聊的恢复价值不同。给每类内容记录字符数、token 估算、是否可恢复和过期时间,才能把“压缩”从一次字符串处理变成预算决策。

    稳定规则        8 KB   必须保留       不可从会话恢复
    当前任务        2 KB   必须保留       来自任务状态
    旧对话         40 KB   可摘要         原文在 transcript
    工具长输出     90 KB   应 offload     原文在 artifact
    项目记忆       12 KB   按相关性选择   原文在 memory 文件
    

    Offload 与摘要解决的问题不同。Offload 保存完整原件并返回句柄,摘要只为当前继续执行提供紧凑状态;拿摘要替代原件,会让后续验证失去证据。

    四处实现对应四种责任

    读取编号 源码路径 需要确认
    1. 压缩器 src/context/compressor.ts 摘要输入和输出保留哪些任务状态
    2. Offload src/context/offload.ts 大内容存放在哪里,句柄如何恢复
    3. 记忆选择 src/context/memory/selection.ts headers、正文预算和年龄怎样结合
    4. 子 Agent 产物 src/agents/final-output-artifact.ts 长结果如何只回传 preview

    失败反例:摘要覆盖原始记录

    摘要覆盖原始记录,或把压缩结果当成绝对事实,导致约束和未完成事项丢失。最危险的是摘要仍然语句通顺,团队很难察觉它漏掉了一个否定条件或任务 id。

    可靠做法是让摘要引用原始范围,并经过可验证字段检查。任务 id、待办状态、用户明确约束可以结构化断言;自然语言细节无法完全验证时,至少保留原文 artifact 和“摘要可能不完整”的状态。

    练习:故意压坏一次摘要

    准备包含三个任务、两个失败尝试和一条禁止条件的对话,生成摘要后故意删掉其中一个任务。验证器应指出缺失 id,并阻止把摘要当成唯一恢复来源。随后把一段 50KB 工具输出改为 artifact,确认模型仍能按句柄恢复需要的片段。

    压缩对象应该是一段已封口的历史

    模型还在流式输出、工具仍在运行时就压缩,会拿到半条 assistant 消息或没有结果的 tool call。压缩边界最好落在完整用户轮次之后:本轮所有前台工具已进入终态,消息账本协议完整,任务状态已经 checkpoint。

    sequenceDiagram
      participant L as Loop
      participant T as Tool
      participant C as Compressor
      participant S as Session Store
      L->>T: tool call c9
      T-->>L: tool result c9
      L->>S: append complete turn
      L->>C: compact sealed range 1..18
      C-->>L: summary + source range + validation
      L->>S: append compression record
    

    后台任务不必阻塞整段压缩,但要以 taskId、状态和 artifact 句柄进入摘要。任务完成后再通过结构化通知补充,不能把 pending 写成 completed。

    摘要最好分“事实”和“待验证线索”

    压缩模型会把语气变得更肯定。例如原对话是“可能是缓存问题,还没验证”,摘要却写成“根因是缓存”。下一轮就会围绕错误结论继续。

    摘要结构可以把事实分层:用户明确约束、工具验证事实、团队决策、模型假设、待验证项。只有带工具证据或用户确认的内容进入 facts;推测留在 hypotheses,并附验证动作。

    facts:
      - value: "tests/unit/cache.test.ts 当前失败"
        evidence: "tool:c14 exit=1"
    constraints:
      - "不得修改生产数据库结构"
    hypotheses:
      - value: "失败可能与旧缓存有关"
        confidence: low
        verify: "清理测试 fixture 后重跑 c14"
    failed_attempts:
      - "仅提高 timeout 无效,trace:c11"
    

    这会多占一点摘要长度,却显著降低“推测升级成事实”的风险。

    增量压缩要避免摘要的摘要无限失真

    会话很长时,系统可能把旧摘要和新十轮再次总结。每压一次,都可能丢一些否定、数字和来源。若永远只总结上一次摘要,误差会积累。

    可以保留原始 transcript 与压缩记录的 source range。新摘要输入由“上一份结构化状态 + 最近原始轮次 + 仍有效的关键原文引用”组成;定期或发现冲突时,允许从原始范围重新压缩。原文不常驻模型,但仍可恢复。

    摘要记录带版本和 hash。恢复时发现摘要引用的原始范围不完整、任务 ID 缺失或 schema 旧,宁可回退到较早 checkpoint 并重新生成,也不要静默使用损坏状态。

    Offload 的句柄需要完整性检查

    artifactFile 指向的文件可能被清理、移动或外部修改。句柄带字符数、hash、创建时间和内容类型,读取时重新验证;不一致就告诉模型“原始证据已变化”,而不是继续引用旧摘要。

    对日志类 artifact,按行范围读取比一次全取更实用。预览可以包含错误上下各 20 行、总行数和查询建议;模型需要某个测试名时用 grep,再只读相关片段。句柄化不是把大内容藏起来,而是提供更精确的访问路径。

    清理策略也应考虑会话状态。仍被未完成任务或摘要引用的 artifact 不能按普通临时文件删除;会话归档后可以按保留期清理,manifest 留下“原文已过期”的明确记录。

    压缩前先做便宜的确定性整理

    不是所有缩减都要调用模型。重复的工具进度事件、已经被最终结果覆盖的临时 UI 状态、完整 Shell 输出的内联副本、相同文件的旧读取版本,可以按确定规则去重或 offload。

    之后再让模型总结自然语言历史,输入更干净,成本更低。确定性整理还不会把“禁止”总结成“提到过”,适合先保护协议结构和显式状态。

    顺序可以是:验证消息账本完整,移除不进历史的 transient 状态,长结果转句柄,按 taskId 汇总进度,最后压缩剩余对话。每一步都记录节省量,团队才知道真正的大头在哪里。

    压缩也可能失败,主任务不能因此消失

    摘要请求超时或模型返回空内容时,系统仍有原始会话。若下一次模型请求尚能在窗口内运行,可以继续并提示压缩未完成;若已经超预算,则阻止新模型调用,提供重试、导出或新会话交接选项。

    不能在压缩开始前就删除旧 messages,也不能把无效摘要写成成功 checkpoint。压缩结果先在临时结构通过 schema 和关键字段验证,再原子追加记录。

    用户取消压缩时也要回到稳定状态。界面可以显示“未压缩,原会话保持不变”,而不是留在永久 compacting。

    设计一次恢复演练

    准备一段 40 轮会话,其中有禁止条件、两次失败实验、三个未完成任务、一个 100KB 日志和一个仍在后台运行的 task。压缩后关闭进程,再从 session 与 artifact 恢复。

    验收不是摘要读起来流畅,而是恢复后:禁止条件仍是命令式约束;失败方法不会被再次建议;三个 taskId 状态正确;日志可按句柄读取;后台任务没有被误报完成;当前模型仍由新进程配置决定。一次这样的演练,能发现比“摘要字数降低了多少”更关键的问题。

    本文目录
    本文目录