这篇学完你能回答什么
- 多轮历史越来越长,你怎么压缩?压缩时什么必须保真?
- 频繁小压和少次大压,哪个更好?为什么?
- prompt 缓存是什么原理?压缩会不会把它击穿?
从一个真实故障讲起
一个编码 Agent。团队自认为把上下文管理做得很勤快:每 10 轮自动做一次摘要压缩,把历史压到 2000 token 以内。逻辑上无懈可击——窗口永远宽裕,成本永远可控。
上线后两个反直觉的结果:
- 账单不降反升 40%;
- 用户抱怨「它老忘事」——明明第 3 轮说过的偏好,到第 40 轮又要重问一遍。
两个原因都藏在「勤快」这两个字里:
账单升,是因为每次压缩都重写了前缀 → prompt 缓存整段作废 → 后面每一轮都要重新写一次缓存;而且压缩本身是一次额外的模型调用,单独计费。压得越勤,这两笔固定开销付得越多。
老忘事,是因为信息每被转述一次就丢一点。压 4 次,就是丢 4 次,是复利式的失真,不是线性的。
正确做法恰恰和直觉相反:少次大压。
每 10 轮自动压一次
历史压到 2000 token 以内,窗口永远宽裕
前缀被重写断点
prompt 缓存整段作废
后面每轮重写缓存
压缩本身还是一次额外采样,单独计费
账单不降反升 40%
压得越勤,这两笔固定开销付得越多
用户抱怨它老忘事
第 3 轮说过的偏好,第 40 轮又要重问
核心概念:先打比方,再给定义
Compaction(压缩)。把接近窗口上限的对话摘要成一段高保真摘要,然后从这段摘要继续往下跑。比喻:不是每写完一页就重抄一遍笔记,而是写到本子快满时,认真整理一次交接文档。
Tool result clearing(工具结果清理)。把老的工具返回从历史里移除。工具结果一旦进上下文就是「永久居民」,成本按剩余轮次复利——清理就是给它办退租。
Prompt caching(提示缓存)。服务端把某段前缀的中间计算结果留住,下次同样前缀直接复用,命中时按远低于原价计费。
三者的关系是一场博弈:压缩管「历史必然增长」,清理管「工具结果膨胀」,缓存管「重复前缀的成本」——而前两个都要改写前缀,天然和第三个打架。 这一篇讲的就是怎么让它们和解。
认真整理一次交接文档
整理一次就丢一点 —— 所以别每写完一页就重抄一遍
把接近上限的对话摘成高保真摘要
从这段摘要继续往下跑;它管的是「历史必然增长」这件事
老住户腾房,房租不必一直付
退了租就再也看不到 —— 后面还要回看那些结果就麻烦了
把老的工具返回从历史里移除
工具结果一进上下文就是永久居民,成本按剩余轮次复利
原理拆解
tools → system → messages,改上层废下层;两把刀都动最下层| 项 | 数值 | 读法 |
|---|---|---|
| 写入溢价 | 5 分钟约 1.25 倍原价;1 小时约 2 倍 | 写进去要先贵一次 |
| 命中 | 约 0.1 倍原价,并免费续期 | 续期从请求开始时刻算起 |
| 最小可缓存长度 | 512 / 1024 / 2048 / 4096 token,按模型不同 | 不到门槛不缓存,且不报错 |
| 回溯窗口 | 20 个 block | 一轮加太多 block,会把上次写入推出去 |
压缩流程:输入 token 数达阈值触发 → 生成一段摘要块 → 摘要块之前的内容全部作废 → 从摘要继续。阈值默认 15 万,最低只能设到 5 万 —— 这个下限本身就是官方在说:不要频繁小压。用量要跨所有迭代汇总,只看顶层字段会算漏账。
和解只需要一个断点:在系统提示词末尾单独打一个缓存断点,压缩发生时那段缓存仍然有效、照常读取,只有新生成的摘要需要作为新内容写入一次 —— 长系统提示词因此可以跨多次压缩一直活着。
tool_choice 变化 —— 内容看着一模一样,缓存就是不命中。 ┌──────────── 上下文窗口 ────────────┐
│ tools │ system │ 历史 + 工具结果 │
└───┬───────┬────────────┬───────────┘
│ │ │
改这里 │ 改这里 │ 改这里 │
▼ ▼ ▼
┌───────────────────────────────────┐
│ 缓存前缀层级:tools → system → messages │
│ 改上层 → 下层全部失效 │
└───────────────────────────────────┘
▲ ▲
│ │
清理会动这里 压缩会动这里压缩的机制(截至 2026-08 已经是一等公民 API)
流程是:按输入 token 数达到阈值触发 → 生成一段摘要块 → 摘要块之前的内容全部作废 → 从摘要继续。几个必须知道的细节:
- 阈值默认 15 万 token,最低只能设到 5 万。 这个下限本身就是官方在告诉你:不要频繁小压。
- 压缩是一次额外采样,单独计费。 用量要跨所有迭代汇总,只看顶层的输入输出 token 字段会算漏账。
- 自定义摘要指令是「完全替换」,不是追加。 这是最容易踩的坑——你只写了「重点保留代码片段」,就等于把默认提示里「保留状态、下一步、已有结论」全删了。
- 可以在压缩后暂停,让你把最近几条消息原样接在摘要后面,避免刚说完的关键信息立刻被摘要走样。
- 带工具时压缩可能失败:模型在摘要那一步跑去调工具了,返回一个空摘要。解法是在指令里明确写「这一步只写文本,不要调用任何工具」。
- 摘要用的是同一个模型,不能换个便宜的来做。
缓存的机制
- 层级
tools → system → messages,改上层废下层; - 写入只发生在断点处,读取时向前回溯,回溯窗口是 20 个 block。这意味着一轮加太多 block,会把上一次的写入推出窗口,明明内容没变也命中不了;
- 有最小可缓存长度门槛,按模型不同为 512 / 1024 / 2048 / 4096 token 不等。不到门槛不缓存,且不报错——两个缓存字段都是 0 就是这种情况;
- 计费:5 分钟写入约 1.25 倍原价、1 小时写入约 2 倍,命中约 0.1 倍;命中会免费续期,续期从请求开始时刻算起(生成耗时也算在寿命里);
- 常见的隐形失效项:工具定义顺序变了、某些语言序列化时 JSON 键顺序不稳定、图片增删、思考或 effort 配置变动、
tool_choice变化。
压缩与缓存怎么和解(这是本篇最值钱的一段)
「压缩会击穿缓存」这个说法要说得更精确:击穿的是会话前缀,不是全部。
官方给的解法是:在系统提示词末尾单独打一个缓存断点。这样压缩发生时,系统提示词那段缓存仍然有效、照常读取,只有新生成的摘要需要作为新内容写入一次。长系统提示词因此可以跨多次压缩一直活着。
再配合「少次大压」,整笔账就清楚了:
一次压缩的固定开销 = 额外采样(读入约 T 个 token)
+ 摘要写入缓存
+ 会话前缀重建
压缩次数 ↑ → 固定开销 × 次数 ↑,同时信息经多次转述失真 ↑
所以阈值应尽量贴近窗口上限,而不是"勤快地小压"。工程实践(截至 2026-08)
表 1:三种手段的选型
| 手段 | 解决什么 | 代价 | 什么时候不该用 |
|---|---|---|---|
| 工具结果清理 | 工具返回膨胀 | 改写前缀影响缓存;被清掉的内容模型再也看不到 | 后续步骤还要回看那些结果时 |
| 压缩 | 历史必然增长 | 额外采样计费 + 信息损耗 + 前缀重建 | 会话离窗口上限还远时——频繁小压更贵也更糊 |
| prompt 缓存 | 重复前缀的成本 | 写入有溢价;要求前缀严格稳定 | 前缀本来就每次都变时(只会一直付写入费) |
压缩时的五类必须保真信息(这一条在 Agent 章已经定过,这里是同一套口径):
- 已确认的关键决定与硬性约束;
- 未完成的待办与下一步;
- 已经踩过的失败路径——不保留就会重复踩,这是最容易被摘要丢掉、代价又最高的一类;
- 外部世界已发生的不可逆动作(已发的邮件、已建的工单、已扣的款);
- 用户明确表达的偏好与口径。
但更重要的是一条架构判断:关键约束应该活在结构化状态里,而不是对话历史里。 历史是会被有损转述的介质,把不能丢的东西放在会被转述的地方,本身就是设计错误。正确做法是把约束提到一个每轮原样重新注入的状态块里,让它根本不参与压缩。
缓存友好设计清单
- 稳定的在前、变化的在后——这是唯一一条不需要记细节的总原则;
- 断点打在跨请求完全一致的最后一个 block 上,绝不打在带时间戳或本轮用户输入的 block 上;
- 系统提示词末尾单独一个断点,让它能在压缩中幸存;
- 长会话要考虑加第二个断点,防止增长把上一次写入推出 20 block 的回溯窗口;
- 检查 JSON 序列化的键顺序是否稳定,工具定义顺序是否固定;
- 一个会话内 effort 与思考配置保持恒定;
- 检查是否够到最小长度门槛——不够是静默失败,只能靠看返回字段发现。
压缩配置清单
- 阈值尽量贴近窗口上限,别为了「保险」调低;
- 自定义摘要指令时,记得把默认提示里那些通用要点(状态、下一步、已有结论)一起写进去;
- 指令里明确禁止在摘要步骤调用工具;
- 用「压缩后暂停」把最近若干条原样保留;
- 成本核算要跨所有迭代汇总,别只看顶层 token 字段。
| 手段 | 解决什么 · 代价 | 什么时候不该用 |
|---|---|---|
| 工具结果清理 | 治工具返回膨胀;代价是改写前缀影响缓存,被清掉的内容模型再也看不到 | 后续步骤还要回看那些结果时 |
| 压缩 compaction | 治历史必然增长;代价是额外采样计费 + 信息损耗 + 前缀重建 | 会话离窗口上限还远时 —— 频繁小压更贵也更糊 |
| prompt 缓存 | 治重复前缀的成本;代价是写入有溢价,且要求前缀严格稳定 | 前缀本来就每次都变时,只会一直付写入费 |
- 五类必须保真
- 已确认的关键决定与硬性约束;未完成的待办与下一步;已踩过的失败路径;不可逆动作;用户的偏好与口径
- 最容易丢的一类
- 已经踩过的失败路径 —— 不保留就会重复踩,是代价最高的那一类
- 架构判断
- 关键约束应该活在结构化状态里,每轮原样重新注入,根本不参与压缩
- 断点打在哪
- 打在跨请求完全一致的最后一个 block 上,绝不打在带时间戳或本轮用户输入的 block 上
- 长会话
- 加第二个断点,防止增长把上一次写入推出 20 block 的回溯窗口;再查够不够最小长度门槛
- 压缩配置
- 自定义摘要指令是完全替换,默认的状态、下一步、已有结论要自己写回去;禁止摘要步骤调工具;摘要用的还是同一个模型
面试视角
- 先讲博弈关系压缩和清理都改前缀,天然和缓存打架
- 给算术依据固定开销 × 次数,失真还是复利式的
- 给保真清单再升级成架构判断:关键约束不该放在历史里
- 给和解方案系统提示词末尾单独打一个缓存断点
- 认为压缩就是「让模型总结一下历史」
- 说不出压缩本身要花钱,账只算了省下的那一半
- 以为开了缓存自然就省钱
- 把「压缩击穿缓存」当成一个无解的问题
- 觉得压得越勤,上下文就越干净
- 知道自定义摘要指令是完全替换,不是追加
- 知道压缩是一次额外采样,单独计费,用量要跨迭代汇总
- 知道最小可缓存长度不够时是静默失败,两个字段都是 0
- 说得出至少两个隐形的缓存失效项
- 能把「关键约束提到结构化状态」讲成架构判断,而不是技巧
面试官怎么问。 Q1-13 是大厂二三面的压力题,Agent 团队必问;Q1-14 是平台组和网关组的必问题。两题经常连着出——因为它们本来就是同一笔账的两面。
答题结构建议
- 先讲三者的博弈关系(压缩和清理都改前缀,和缓存打架),这一句就能拉开档次;
- 给出「少次大压」的算术依据,而不是直觉;
- 给出保真清单,并升级成架构判断(关键约束不该放在历史里);
- 给出和解方案(系统提示词末尾单独打断点)。
分水岭信号
- 只读过:认为压缩就是「让模型总结一下历史」;觉得压得越勤上下文越干净;说不出压缩本身要花钱;以为开了缓存自然就省钱;把「压缩击穿缓存」当成无解。
- 真做过:知道压缩是额外采样并单独计费;知道自定义摘要指令是完全替换;知道最小可缓存长度不够时是静默失败;说得出至少两个隐形的缓存失效项;能把「关键约束提到结构化状态」讲成架构判断而不是技巧。
小结与延伸
继续深入
本篇归属第 1 章「LLM 与 Prompt 基础」,去做这一章的题。