Context Window 是模型一次能看的 Token 上限,但它不是「可用的工作记忆」。注意力是二次代价、KV Cache 线性占用显存、位置偏差让中间内容系统性失效------上下文既贵又不可靠。上下文工程就是在这三重要约束下做预算管理:什么放进去、放哪个位置、什么时候换掉、换掉的信息去哪里。
这一期回到那个所有 Agent 最终都会撞上的瓶颈:模型每一轮决策时能看到的那一小块 Token 空间。工具越多、任务越长、检索越频繁,这块空间就越紧张。而「把它塞满」从来不是解法------它是问题的开始。
本篇回答两个问题:
| # | 面试问题 | 指向哪个工程面 |
|---|---|---|
| Q1 | Context Engineering 和 Prompt Engineering 有什么区别?为什么不能把信息全塞进上下文? | 上下文的物理成本模型与预算分配 |
| Q2 | 上下文满了怎么压?Tool Result 太大、搜索返回 100 条、多轮膨胀分别怎么办? | 压缩手段分类与工程实现 |
两题之间有明确的因果链:Q1 说明「为什么有限」,Q2 说明「在有限下怎么办」。能答出 Q1 的成本模型,Q2 的方案才有选择依据;否则压缩就退化成「随机砍一刀」。
一、Context Engineering 与 Prompt Engineering 是什么关系#
1.1 推荐回答#
Prompt Engineering 关注单个提示的表述方式------指令怎么写、示例放几条、格式怎么约束。它优化的是「同一份输入里,措辞如何影响输出」。
Context Engineering 关注整个上下文空间的生命周期管理------哪些信息该进来、占多少预算、放在什么位置、什么时候被替换或压缩、替换后信息去哪里。它优化的是「在有限预算内,模型每一轮能看到什么」。
两者是包含关系:
bash
Context Engineering
├── System Prompt 的设计与版本管理
├── 工具的暴露策略(暴露哪些、何时暴露)
├── 记忆的召回与注入
├── 检索结果的筛选与排序
├── 对话历史的管理(保留、摘要、外部化)
├── 压缩与淘汰策略
└── Prompt Engineering(指令表述、few-shot、格式约束) ← 是其中一个子集
一句话区分:Prompt Engineering 管「怎么说」,Context Engineering 管「放什么、放哪、放多久」。
1.2 为什么叫「工程」而不是「技巧」#
Prompt Engineering 的调试对象往往是单次调用的输出质量;Context Engineering 的调试对象是一个随时间演化的系统。它必须回答几个工程问题:
- 预算:每一类信息分配多少 Token?超了谁先被裁?
- 生命周期:System 区的契约活多久?本轮的工具结果什么时候失效?
- 一致性:上下文被压缩后,之前给出的结论和约束还在不在?
- 可观测:这一轮上下文里到底装了什么?成本归到哪一类?
这些都不是「换一句措辞」能解决的问题,所以「工程」二字是有实指的。
二、为什么不能把信息全塞进上下文#
「现在模型都 200K、1M 了,全塞进去不就行了?」------这是 Q1 最常被追问的方向,也是本篇要建立的核心直觉。四个理由,每一个都有物理或机制层面的依据。
2.1 理由一:注意力是二次代价,长上下文超线性变贵#
第 01 期讲过:Self-Attention 让每个 Token 与序列中所有 Token 计算相关性,序列长度翻倍,计算量约翻四倍。
bash
长度 n 的输入 → 注意力计算量 ∝ n²
工程含义非常直接:
| 上下文长度 | 相对注意力计算量 | 相对成本(示意) |
|---|---|---|
| 4K | 1× | 1× |
| 32K | 64× | ~16×(受 KV Cache 与并行度影响,非线性) |
| 128K | 1024× | ~40--60× |
实际成本曲线受实现优化、批处理、缓存命中影响,不会严格等于 n²,但「长上下文显著更贵、且贵得超线性」这个结论是稳定的。这也解释了为什么厂商的长上下文通常要单独定价。
2.2 理由二:KV Cache 线性占用显存,且随轮次累积#
第 01 期还讲过:生成每个 Token 都要复用之前所有 Token 的 K、V。Agent 运行时每轮都要重发完整上下文,KV Cache 从第 1 轮持续涨到第 N 轮。
bash
第 1 轮:system + user → cache 小
第 2 轮:+ assistant + tool result
第 3 轮:+ assistant + tool result
...
第 N 轮:全部历史 → cache 大
这是一条乘法关系 :轮次 × 每轮上下文长度。Agent 比 Chatbot 贵得多,根因就在这里------不是模型调用次数多,而是每一轮都在重发累积的上下文。
2.3 理由三:lost in the middle------位置即语义#
即使没超窗口、成本可接受,长上下文还会带来一个更隐蔽的问题:模型对上下文中部信息的利用率显著低于开头和结尾。这就是经典的 lost in the middle 现象------把关键信息放在中间,召回率明显下降。
工程结论:上下文里没有「中性位置」。同一段信息,放在开头、中间、结尾,效果不同。
这直接影响了分区设计------把最关键、最不变的契约放在最前,把当期任务与最终指令贴近结尾,而不是随手拼接。
2.4 理由四:context rot------上下文越长,整体质量越降#
比 lost in the middle 更进一步的现象是 context rot:不是某一段信息被忽略,而是随着上下文增长,模型对全部信息的整体处理质量下降。它在信息量还没接近窗口上限时就已经出现。
所以「窗口还够用」不等于「可以继续塞」。这解释了为什么 Claude Code、Codex 都选择在远未填满时就主动压缩(见第四节)------它们不是在省 Token,是在维持输出质量。
2.5 四个理由合起来#
bash
塞满上下文
├── 成本超线性上升(注意力 O(n²) + KV Cache 累积)
├── 延迟上升(prefill 变长)
├── 关键信息被淹没(lost in the middle)
└── 整体质量下降(context rot)
这不是四个独立缺点,而是同一个事实的四个侧面:Context Window 是容量,不是工作记忆。 有效上下文远小于标称窗口。
三、上下文分区与预算设计#
3.1 信息漏斗#
从「模型能看到的最大范围」到「真正参与本轮决策的信息」,中间有四层筛选:
bash
Context Window 模型理论上能处理的全部 Token
↓ 筛选
Relevant Context 与当前任务相关的部分
↓ 筛选
Useful Context 本轮决策真正需要的部分
↓ 压缩
Compressed Context 预算内能装下的部分
每一层都是一次决策,每一次决策都有代价。上下文工程的工作,就是让这四次筛选有依据、可观测、可回归。
3.2 六个标准分区#
生产系统通常把上下文切成六个区,每区分配独立预算:
| 分区 | 内容 | 生命周期 | 典型预算占比 |
|---|---|---|---|
| System | 身份、能力边界、强约束、长期契约 | 长期稳定 | 10--20% |
| User Goal | 本次任务的原始目标与验收标准 | 单任务 | 5--10% |
| Relevant Memory | 召回到的用户偏好、历史结论 | 按需 | 5--15% |
| Current State | 任务状态、计划、已完成/待办 | 动态更新 | 10--20% |
| Tool Results | 本轮及近期的工具返回 | 短 | 20--40% |
| Recent Conversation | 最近若干轮原文 | 滚动窗口 | 剩余 |
占比只是起点,真正的设计动作是「超预算时按什么顺序裁」。
3.3 优先级设计:按「可恢复性」排序,不是按「重要性」#
一个常见的错误是「按重要性排序」。更可靠的标准是这条信息一旦丢失,能否恢复:
bash
不可逆(丢了就没了,必须保)
├── 用户已确认的决策与结论
├── 关键 ID(订单号、任务号、会话号)
├── 用户明确给出的约束("不要动 migrations/")
└── 已执行的副作用记录(改过哪些文件、发过哪些请求)
↓
当前任务状态(可重建,但重建有成本)
↓
近期对话原文(可从存储回查)
↓
历史工具结果(可重新调用,代价是时间与钱)
↓
早期寒暄与重复的中间推理(无保留价值)
判据一句话:「后面还会不会被追问?」 会 → 保;不会 → 可裁。
注意前两项的特殊地位:已确认的结论和用户约束是「不可再生资源」。它们一旦被压缩掉,模型无法通过任何手段重新推导出来------用户不会再重复说一遍,任务也不会自动回滚。这也是下一节 Codex 案例中「governance decay」问题的根源。
3.4 前缀稳定性决定 KV Cache 命中率#
第 01 期提到 KV Cache 的三种状态:prefill(首次计算)、decode(逐 Token)、cache hit(前缀复用)。只有前缀完全一致,缓存才能命中。
这意味着:
bash
✅ 稳定前缀(可命中缓存)
[System: 身份 + 约束] [工具定义] [稳定示例] ... [动态内容在最后]
❌ 前缀被污染(缓存频繁失效)
[System: 身份 + 约束 + 当前时间戳] [用户画像] [工具定义] ...
← 时间戳每轮都变,导致它之后的全部内容缓存失效
把动态内容塞进 System 区,是 Agent 成本失控最常见的单一原因。 正确做法是把所有会变的东西(时间、用户状态、检索结果、工具结果)一律推到前缀末尾。
这不是微优化:一次缓存失效意味着整个前缀重新 prefill,成本和延迟都会跳一档。
3.5 System 与 User 的分界是「生命周期」,不是「重要性」#
面试中常被问「什么该放 System,什么该放 User」。判据不是「哪个更重要」,而是哪个活得更久:
- System = 跨轮、跨任务稳定的契约(角色、能力边界、输出规范、安全约束);
- User = 本次任务特有的输入(目标、参数、本轮问题)。
工具定义放哪取决于它是否频繁变化:工具集稳定 → 放 System 享受缓存;工具集按任务动态组装 → 动态注入,但要注意这样一来 System 之后的缓存会受影响,需要权衡。
四、上下文压缩:四种手段(本篇骨架)#
这是 Q2 的主体。市面上的「压缩技巧」很多,但按信息去哪里了分类,只有四种。这个分类的好处是:选型时有明确的判断依据,而不是罗列技巧。
4.1 总览#
| 手段 | 信息去向 | 压缩比 | 可逆性 | 代价 | 适用信息 |
|---|---|---|---|---|---|
| 截断 | 直接丢弃 | 最高 | 不可逆 | 零成本 | 不会再被追问的内容 |
| 摘要 | 压缩成语义摘要 | 高(10--50×) | 有损,细节丢失 | 一次模型调用 | 长文本、长对话 |
| 外部化 | 移出上下文,留句柄 | 高 | 可回查(原样) | 存储 + 一次读回 | 大文件、长日志、中间产物 |
| 子 Agent 隔离 | 换到另一个上下文执行 | 最高 | 结论可回传 | 一次额外调用 | 探索型脏活 |
选择依据一句话:这段信息后面还会不会被追问?
bash
会被追问,且需要精确原文 → 外部化(留句柄 + 摘要,需要时读回)
会被追问,但只需语义 → 摘要
不会被追问 → 截断
整段工作本身就把上下文搞脏 → 子 Agent 隔离
4.2 截断:最便宜,也最容易切错#
截断是把内容直接从上下文中丢弃。它的优势是零成本、零延迟;风险是不可逆。
适用的典型场景:
- 开头的寒暄与确认("你好" "好的" "帮我看看");
- 已经得出结论的中间推理过程(保留结论,丢掉推演);
- 重复的工具结果(同一个文件被读了三次,只留最后一次);
- 已过期的中间状态(计划已经迭代了三版,只留最新版)。
不适合:任何可能被追问的内容------用户给过的约束、已确认的决策、关键 ID、副作用记录。
max_tokens / max_steps 这类硬性预算,本质也是截断的一种兜底形式。
4.3 摘要:有损压缩,必须保住关键实体#
摘要是让模型把长内容换成语义摘要。压缩比通常很高(10--50×),但必然有损------一个文件路径、一个精确报错、一个约束条件,都可能在摘要中消失,即使它后面仍然重要。
工程上必须做的三件事:
- 结构化摘要,而不是自由发挥。给定固定小标题(目标 / 约束 / 已完成 / 待办 / 关键决策 / 当前状态),强制模型填格子;
- 保留关键实体:路径、ID、报错原文、命令、用户原话,要求逐字保留而不是转述;
- 原文可回查:摘要旁边保留原文的存储位置,需要时能读回原样。
第 4.7 节的三个真实方案(Claude Code、Codex、JEV)在这三点上的选择差异很大,正是本节最有价值的对照素材。
4.4 外部化:把状态移出上下文,只留句柄#
外部化是把大块内容写进文件 / 数据库 / 状态存储,上下文里只留引用句柄 + 一句话摘要:
bash
❌ 直接注入
[Tool Result: 12480 行日志内容 ... 占 45K token ...]
✅ 外部化后
[Tool Result] 日志已保存至 artifacts/run-2291/build.log(12480 行,含 37 条 ERROR)
→ 需要细节时用 read_file(path, offset, limit) 读取
它与第 08 期的「状态外置」同源:上下文是给模型读的(有预算、要压缩),状态是给程序读的(结构化、无长度上限)。 把状态塞进上下文是常见的设计错误。
外部化的另一个好处是可审计:留了句柄就意味着留了可回溯的证据链,摘要做不到这一点。
4.5 子 Agent 隔离:压缩比最高,代价是多一次调用#
把「脏活」交给独立上下文的子 Agent,主 Agent 只收到结论:
bash
主 Agent 上下文
│
├── 派发:分析这 30 个文件的依赖关系
│ ↓
│ 子 Agent(独立上下文,可读 30 个文件、跑 20 次搜索)
│ ↓
└── 回传:结论 + 证据引用(约 500 token)
原来需要 60K Token 的探索过程,主上下文只增加 500 Token。这是压缩比最高的手段。
代价有两处:一是多一次调用(成本与延迟);二是信息经过了一次有损传递------子 Agent 可能漏掉主 Agent 需要但当时不知道需要的细节。缓解办法是要求子 Agent 回传结构化结论 + 证据句柄,让主 Agent 需要时能按句柄深挖。
多 Agent 的完整讨论见第 11 期;这里只把它作为一种压缩手段。
4.6 三个高频场景对症#
场景一:Tool Result 太大(比如一次数据库查询返回 800 行)
bash
组合方案:
1. 结构化抽取------只回传决策需要的字段,丢掉其余列
2. 外部化------全量写文件,上下文保留「路径 + 行数 + 关键统计」
3. 分页取用------让 Agent 决定要不要读更多,而不是一次全给它
关键是不要把「原始返回」当成「必须注入的内容」。工具返回是数据源,不是上下文条目------中间应该有一层「投影」。
场景二:搜索返回 100 条结果
bash
❌ 100 条全部注入 → 噪声淹没信噪比
❌ rerank 后注入 top-10 却不说明 → Agent 以为只有 10 条
✅ rerank 取 top-5 注入
+ 明确提示「共 100 条,已按相关性展示前 5 条,可继续检索」
→ 由 Agent 决定是否扩大范围
这里的关键是保留「还有更多」这个元信息。压缩掉了内容,但不能压缩掉「存在更多内容」这一事实------否则模型会误判信息完备性并过早收敛。
场景三:多轮对话膨胀
四种手段组合:
bash
最近 3--5 轮 → 保留原文(准确度最高)
中间若干轮 → 摘要(保留结论与决策)
早期已完结的任务 → 外部化到记忆 / 任务记录
寒暄与确认 → 截断
用户明确约束 → 提升为「钉住」内容,永不进入压缩候选
最后一条在实践中经常被忽略,但它是正确性问题的关键------见 4.7.3。
4.7 三种真实压缩方案的实证对照#
面试里最能体现深度的一问是:「你在真实工具里见过压缩是怎么做的?」这一节给出三个可对照的真实实现------Claude Code 的三层渐进压缩 、Codex 的服务端加密 blob 、fast-jev-compaction 的判定式删除。三者在「摘要 vs 不摘要」这个根本问题上做出了不同选择。
4.7.1 Claude Code:三层渐进 + 9 段结构化摘要
Claude Code 的 auto-compact 不是单一机制,而是三层递进,从最便宜的手段开始,只有在不够时才升级:
bash
Tier 1 microcompaction 持续运行,零 API 成本
见微知著:老的工具结果(读文件 / bash / grep / glob / web 检索)
被清空内容或整条移除;利用 API 的 cache_edits 从「缓存前缀」
里删除而不使缓存失效------这是最便宜的压缩
Tier 2 session-memory compaction 零 API 成本
不调用模型,直接把已经抽取好的 session memory 文件当作摘要,
再保留最近若干条消息;用 lastSummarizedMessageId 保证
后续压缩只处理新增部分
Tier 3 full compaction 调用模型,成本最高
让 Claude 生成结构化摘要,替换掉较早的对话轮次
触发阈值(以 200K 窗口为例):
bash
effective_window = context_window - reserved_for_summary(20K) = 180K
threshold = effective_window - 13K buffer = 167K
即在 167K 触发,而不是贴到 200K。那 13K 缓冲的用途是给模型留出发言空间和新工具结果落地的位置。提前触发本身就是质量策略------对应 2.4 节的 context rot。
Tier 3 的摘要格式是 9 段固定结构的,这比「请总结一下」可靠得多:
| # | 段落 | 作用 |
|---|---|---|
| 1 | Primary Request and Intent | 用户到底要做什么 |
| 2 | Key Technical Concepts | 涉及的技术概念 |
| 3 | Files and Code Sections | 保留完整代码片段 |
| 4 | Errors and Fixes | 报错与修复方式 |
| 5 | Problem Solving | 试过什么、做了什么决策 |
| 6 | All User Messages | 非工具结果的用户消息逐字保留 |
| 7 | Pending Tasks | 还没做完的事 |
| 8 | Current Work | 摘要前正在做的具体工作 |
| 9 | Optional Next Step | 下一步,并引用对话原文 |
第 3 段和第 6 段值得特别注意:代码片段与用户原话要求逐字保留,而不是转述。这正是 4.3 节讲的「保住关键实体」在真实产品里的落地形态。第 9 段要求引用原文,则是在用「引用」对抗摘要的幻觉。
其他几个工程细节同样有代表性:
- 摘要时禁用工具 (
NO_TOOLS_PREAMBLE)------只允许纯文本输出,避免摘要过程中产生副作用; - forked agent 复用父会话的 prompt cache------摘要调用本身不重新 prefill 整段历史,显著省成本;缓存共享失败则退回流式路径;
- 按 API 轮次分组消息 ,保证
tool_use/tool_result永不被拆散; - 图片替换为
[image]占位符再送去摘要,避免「图太多」导致摘要调用自己爆上下文; - 熔断器:连续 3 次压缩失败就停止重试,避免在无法恢复的超限状态上持续烧钱;
- partial compaction 有方向 :
up_to保留近期上下文、from保留旧前缀------方向选择依据就是缓存经济学,前缀保留则缓存仍热。
4.7.2 Codex:服务端加密摘要(remote compaction v2)
Codex CLI 走了一条完全不同的路:压缩由服务端完成,客户端拿到的是一个不可读的加密 blob (内部代号 memento)。
流程:
bash
1. 客户端发普通 Responses 请求(同模型、同指令、同工具、完整历史)
末尾追加一个输入项: {"type": "compaction_trigger"}
2. 服务端返回恰好一个输出项:
{"type": "compaction",
"id": "cmp_0f4c...",
"encrypted_content": "gAAAAABqbv..."} ← Fernet token,客户端不可读
3. 客户端重建历史:
[base instructions]
+ [保留的用户消息,原文]
+ [重新注入的指令与 AGENTS.md / 环境上下文]
+ [最后一条真实用户消息]
+ [compaction 项 ------ 永远在最后]
几个值得注意的设计:
(a)保留用户消息原文,而不是保留摘要文本。 客户端保留预算为 64K Token(本地压缩路径为 20K),按「最新优先」截断。也就是说 Codex 选择把「用户说过什么」的原始记录留在明文里 ,只把「agent 的探索过程」交给服务端压缩。这与 Claude Code 第 6 段「All User Messages 逐字保留」是同一个判断:用户输入是不可再生的,agent 的中间过程是可再生的。
(b)压缩发生在回合内部(mid-turn),而不只是回合之间。 三个触发点:
| 阶段 | 触发条件 |
|---|---|
| Pre-turn | 采样前已达上限;或切换到更小窗口的模型;或压缩兼容哈希变化 |
| Mid-turn | 每步采样后若仍需 follow-up 且触及上限------回合内压缩,循环继续 |
| Manual | 用户执行 /compact,走同一条远程路径 |
Mid-turn 压缩是 Codex「长任务不会中途死掉」的关键:压缩项插在历史最后,模型从它继续,外部看来只是同一轮里多了一步。
(c)跨模型要用 comp_hash 判定兼容性。 每个模型带一个「压缩兼容哈希」。切换模型时,旧 blob 不被假定对新模型有效------先对前一个模型重新压缩,失败再回退。这说明压缩产物是模型相关的,不能当成通用格式随意迁移。
(d)阈值同样提前触发。 自动压缩上限默认为「解析后上下文窗口的 90%」,而解析窗口 = 原始窗口 × 95%。用户配置只能调低,不能调高------产品设计上不允许你把压缩推后到危险区间。
(e)实测压缩比。 公开的实测记录:
| 场景 | 压缩前 | 压缩后 |
|---|---|---|
| 长时间代码审查会话 | 174,871 token | 3,368 token |
| 大会话(保留 13 项) | 226,661 token | 19,537 token |
| 小会话 | --- | 4,552 token |
第二个案例里保留了 8 条真实用户消息、3 条重注入的 developer 消息、AGENTS.md 与环境上下文,外加一个 18KB 的加密项。保留结构与「保用户原文」的策略在数字上是可验证的。
(f)AGENTS.md 是「结构化钉住」的实例。 Codex 的 AGENTS.md 每轮从文件系统重读,从不进入被压缩的对话历史。这不是巧合,而是一种架构选择:把治理约束放在压缩边界之外,它就不可能被压缩掉。
这引出了一个重要问题:对话中临时给出的约束呢? 用户在第 3 轮说「不要动 migrations/」,如果这句话在第 150 轮被压缩掉,约束就静默消失了。研究(Constraint Pinning)的量化结论是:标准压缩下约束违反率可达 30--59% ,而把约束钉在压缩边界之外后可降到 0%,且一个约 47 Token 的钉住策略只占 10K 上下文的 0.5% 以下。
这是上下文压缩最容易被忽视的正确性风险 :压缩不只是「丢信息」,它可能静默地取消约束。缓解方式有三层------把约束写进项目级文件(AGENTS.md / CLAUDE.md)、在压缩提示里明确要求逐字保留约束、以及压缩后重新注入钉住项并做完整性校验。
4.7.3 fast-jev-compaction:不摘要,只做「保留 / 截断 / 删除」判定
随着jev模型的出现,将jev用于context压缩技术,也是一个值得思考的应用, fast-jev-compaction(npm 包 + Claude Code 插件)代表了第三种路线:彻底放弃摘要。
它的核心论点很直接:
大多数上下文压缩让 LLM 去总结旧轮次。摘要是有损 的------一个文件路径、一处精确报错、一个约束、一条命令,都可能在后面仍然重要的时候消失。这个库从不重写任何内容。
它做的事:
bash
1. 每个 tool_use 通过 tool_use_id 与其 tool_result 配对
首条消息与最近 N 条(preserveRecentMessages)被「钉住」,永不触碰
2. 送给 Jev 的「状态」是完整对话(最旧在前),
其中每个工具结果替换为简短标记: ok, 4213 chars (omitted)
工具入参保留、文本保留,不做任何摘要
3. 状态按阶段压缩到 maxStateTokens(默认 25K),
只在上一阶段不够时才进入下一阶段:
工具入参截到 1000 → 200 → 60 字符
长文本缩为头 + 尾(最旧的非钉住消息优先)
旧的非钉住消息折叠为 [... N chars omitted ...]
旧工具调用压成一行: t12 Read file_path=src/a.ts → ok 480ch
无调用的旧消息直接略去
连续的「只有调用」消息合并为一条
4. 对每个非钉住的调用,Jev 回答两个「留不留」问题:
- keepCall :知道「调用发生过」这件事还有意义吗
- keepResult :结果内容后面还需要吗,而且重跑这个工具做不到吗
5. 按阈值决策:
keepResult ≥ 0.5 → 保留调用和结果
keepCall ≥ 0.5 → 保留调用,结果截到前 300 字符 + 一行说明
否则 → 调用与结果一起删除
6. 重建消息列表:内容全被删掉的消息移除;
未被触碰的消息原样返回;绝不会出现「有结果没调用」
它自己的定位与局限也写得很清楚:
- 只有工具调用和结果是候选,文本消息永不删除或缩短(只在送给 Jev 的状态里做节略);
- Token 数是按字符估算的,不是真正的 tokenizer;
- 概率不是证明------
keepResult高不保证删除安全,好在 assistant 总可以重跑工具; - 完整状态会随每个请求重发,所以接近状态上限的长历史成本较高;
- 压缩不足(短会话)或 Jev 失败时,回退到 Claude Code 内置摘要 。插件在生效时会提示
kept N/M messages, no summary,回退时提示fallback to built-in summary。
它的触发时机 :作为 Claude Code 的函数钩子(function hook,2.1.274+ 早期访问特性)挂到 session.compact 上,替换「内置摘要」这一步的输出。
4.7.4 三种方案对照
| 维度 | Claude Code | Codex(v2) | fast-jev-compaction |
|---|---|---|---|
| 摘要还是删除 | 摘要(Tier 3)+ 删除(Tier 1) | 摘要(服务端) | 只删除 / 截断,绝不摘要 |
| 压缩在哪做 | 客户端 | 服务端(加密 blob) | 客户端(经 Jev 判定) |
| 保留用户原文 | ✅ 第 6 段逐字 | ✅ 64K 预算内原文 | ✅ 文本消息永不动 |
| 格式约束 | 9 段固定结构 | 服务端自由笔记 | 不适用(不生成新文本) |
| 触发时机 | 167K(200K 窗口) | 90% × 解析窗口 | 挂到 /compact 钩子 |
| 成本策略 | 三层渐进,尽量走零成本层 | 复用缓存的 forked 调用 | 一次并发判定请求 |
| 失败处理 | 连续 3 次熔断 | 重试上限 2 次 | 回退内置摘要 |
| 最大风险 | 摘要丢精确细节 | 约束可能被压缩掉 | 判定错误的删除不可逆 |
这三种方案的共同点,比差异更有价值:
- 都不让压缩后的内容脱离「按轮次分组」的结构------绝不允许出现「有 tool_result 没有 tool_use」;
- 都保护用户原始输入------用户说过什么必须留在明文里;
- 都在远未填满时触发;
- 都有明确的失败回退路径------压缩不能成为单点故障。
差异则集中在一个问题上:「模型生成的摘要」和「原始记录的裁剪」哪个更安全? Claude Code 和 Codex 选摘要(因为信息密度更高、成本更低),JEV 选裁剪(因为精度无损)。这个取舍没有普适答案,取决于任务对「精确细节」的敏感度:改动代码、遵循约束类任务倾向保留原文;探索、问答类任务可接受摘要。
五、从机制到工程#
把前面的机制翻译成可执行的设计约束:
5.1 成本约束#
| 机制 | 工程结论 |
|---|---|
| 注意力 O(n²) | 上下文长度要作为一级预算指标被监控,而不是听任其增长 |
| 每轮重发全量历史 | 输入 Token 是 Agent 成本的大头,优化优先级高于输出 Token |
| 前缀决定缓存命中 | 动态内容一律后置;System 区不得包含时间戳等易变字段 |
| 压缩本身要调模型 | 压缩不是免费的,Tier 1/2 这类零成本手段应优先 |
| 保留用户原文有预算 | 保留策略要设上限(Codex 64K、Pi 20K),不能无限保留 |
5.2 可靠性约束#
| 机制 | 工程结论 |
|---|---|
| 摘要是有损的 | 关键实体必须逐字保留;原文必须可回查 |
| 压缩会静默取消约束 | 长期约束写进项目文件;压缩后重新注入钉住项并校验 |
| 删除不可逆 | 删除类压缩要有白名单(只允许删哪类工具结果) |
| 压缩可能失败 | 必须有熔断与回退路径,不能成为单点故障 |
| tool_use/result 必须配对 | 压缩的原子单位是 API 轮次,不是单条消息 |
5.3 可观测约束#
至少要能回答四个问题:
bash
这一轮上下文里装了哪些分区?各占多少 Token?
缓存命中率是多少?前缀失效率是多少?
本轮触发了哪一层压缩?压缩比是多少?
压缩后丢了哪些内容?关键实体还在不在?
前三个是成本与性能问题,第四个是正确性问题------它需要一个回归评测集来回答(与第 12 期的评估体系打通)。
六、深入讨论#
6.1 摘要会丢信息,怎么保证不丢关键的#
三层保险:
- 结构化摘要,强制填固定小标题(Claude Code 的 9 段就是这个思路);
- 关键实体白名单:路径、ID、报错原文、用户原话、命令------要求逐字复制而不是转述;
- 原文可回查:摘要旁保留原文句柄,需要时读回。
追加一层验证:把压缩前后的上下文分别喂给模型回答同一组问题,答案不一致即说明压缩丢了信息。这就是压缩的回归测试。
6.2 什么时候不该压缩#
- 需要精确引用原文时:合同法务条款、报错堆栈、代码 diff、具体数字;
- 约束类信息:应当钉住或写进项目文件,而不是指望摘要保留;
- 即将被追问的细节:如果下一轮大概率要引用,压缩它就是在制造返工。
JEV 的整套设计就是对这一条的极端回应:宁可删掉整条记录,也不改写它。
6.3 推理模型的 CoT 还要不要保留#
判断依据是「任务是否需要多步推理的中间结论」。在推理模型上,冗余 CoT 的边际收益递减,而且推理 Token 自身也占上下文。合理策略是:保留结论,按需丢弃推演过程------但要注意部分模型的 thinking block 在压缩后不可恢复(不同产品支持情况不同,Claude 的 on-demand compaction 支持保留近期轮次的 thinking,阈值式压缩不支持)。
6.4 Few-shot 放几条合适#
Few-shot 的作用是定义格式与边界,不是教知识。因此:
- 2--3 条覆盖「典型正例 + 边界正例 + 反例」即可;
- 每条都要短,长示例应外部化;
- 10 条以上通常只剩成本,边际收益已经耗尽。
6.5 压缩后怎么防止「行为漂移」#
压缩后模型可能:忘记已完成的工作并重做、忘记用户已拒绝的方案、语气与约束漂移。Codex 的做法是在基础指令里常驻一段说明,教模型如何理解压缩:
bash
When you run out of context, the conversation is automatically summarized
for you, but you will see all prior user requests. ...
Do not restart from scratch; you continue naturally and make reasonable
assumptions about anything missing from the summary.
Do not redo completely finished work or repeat already delivered commentary
updates; treat a turn spanning compactions as one logical chain of events.
这段常驻说明本身就是上下文工程的一部分:它用几十个 Token,换取「压缩后不重做已完成工作」这个行为约束。
七、常见的错误认识#
| 错误认识 | 更准确的理解 |
|---|---|
| 窗口 1M 了就不用管上下文 | 窗口是容量不是工作记忆;超线性成本、lost in the middle、context rot 依然存在 |
| 上下文越多信息越全,效果越好 | 超过某个点后,增加无关信息会降低整体质量 |
| 压缩就是「总结一下对话」 | 截断 / 摘要 / 外部化 / 子 Agent 隔离是四种不同手段,摘要只是其中之一 |
| 压缩是无损的,等价于「换个说法」 | 摘要必然有损;无损只能靠「不重写原文」(JEV 路线)或外部化 |
| 压缩只会丢信息,不影响正确性 | 压缩会静默取消约束,这是正确性问题而非性能问题 |
| 贴到窗口上限再压最省 Token | 提前触发(60%--90%)质量更好;贴上限压缩风险高、摘要质量差 |
| 把时间戳放进 System 提示是小事 | 会让整个前缀缓存失效,是成本失控的头号原因 |
| 压缩后模型应该「从零理解」 | 需要常驻说明教模型如何续接,否则会重做已完成工作 |
| 工具返回什么就注入什么 | 工具返回是数据源,中间应有「投影」层:抽取 + 外部化 + 分页 |
| 多轮对话只保留最近 N 轮最省事 | 会丢掉早期确认的约束与结论;应按可恢复性分层处理 |
八、概念速查#
System Prompt 和 User Prompt 的区别? 分界是生命周期而非重要性。System 是跨轮稳定的契约(角色、边界、输出规范),User 是本次任务输入。
Context Window 限制的是什么? 输入与输出的总 Token 预算。它是容量上限,不等于有效工作记忆------超线性成本、位置偏差与 context rot 会让有效上下文小于标称值。
Chain-of-Thought 的本质与失效场景? 把推理过程外显以提升多步任务准确率。在推理模型上收益递减;简单抽取任务上是纯浪费。
Few-shot Prompting 的作用? 定格式、定边界,不是灌输知识。2--3 条覆盖典型与边界即可。
上下文压缩有哪四种手段? 截断(丢弃)、摘要(有损压缩)、外部化(移出留句柄)、子 Agent 隔离(换执行位置)。选择依据是「后面还会不会被追问」。
Claude Code 的压缩分几层? 三层:microcompaction(持续裁剪老工具结果,零成本)、session-memory compaction(用已有记忆文件当摘要,零成本)、full compaction(调模型生成 9 段结构化摘要)。
Codex 的压缩产物是什么形态? 服务端生成的加密 blob(compaction 项),客户端不可读,永远放在历史最后;用户消息原文按 64K 预算保留。
fast-jev-compaction 的核心主张? 不摘要、不重写:只对工具调用与结果做「保留 / 截断 / 删除」判定,文本消息永不改动。
KV Cache 与前缀稳定性的关系? 只有前缀完全一致才能命中缓存。动态内容(时间戳、用户状态)必须后置,否则前缀失效、成本跳档。
什么时候不该压缩? 需要精确引用原文时(合同、代码、报错)、约束类信息、下一轮大概率会引用的细节。
九、动手实验#
配套代码:
agent-developer-interview/agent-05-context-engineering
实验用一个零依赖的上下文管理器,把本节讲的分区、优先级、四种压缩手段和前馈稳定性全部参数化,观察「同样一批信息、不同策略」下的 Token 占用与信息保留情况。
9.1 核心代码节选(一):分区预算与优先级裁剪#
关键不在「装了什么」,而在超预算时裁谁:
bash
def build_context(self, state):
"""按分区配额组装上下文;超预算时按可恢复性从低到高裁剪。"""
partitions = {
"system": self.system_blocks, # 身份、约束、工具定义
"goal": [state.user_goal], # 本次任务目标(不可裁)
"memory": state.recalled_memories, # 召回的偏好与历史结论
"state": [state.current_plan], # 任务状态与待办
"tools": state.recent_tool_results, # 近期工具返回
"history": state.recent_messages, # 最近若干轮原文
}
total = sum(estimate_tokens(b) for blocks in partitions.values() for b in blocks)
if total <= self.budget:
return flatten(partitions)
# 裁剪顺序 = 可恢复性从高到低(先裁能重新拿到的)
for name in ["tools", "history", "memory", "state"]:
over = total - self.budget
if over <= 0:
break
freed, partitions[name] = drop_lowest_priority(partitions[name], over)
total -= freed
# system 与 goal 永不裁剪:它们是不可再生信息
return flatten(partitions)
这段代码里最重要的是裁剪顺序:tools → history → memory → state,而 system 与 goal 永不裁剪。工具结果可以重新调用拿到,用户目标和约束不能。
9.2 核心代码节选(二):前缀稳定性检查#
把「动态内容污染前缀」这件事变成可检测的断言:
bash
def check_prefix_stability(prev_context, curr_context, system_blocks):
"""检查本轮相比上轮,System 前缀是否被破坏(会导致 KV Cache 全部失效)。"""
prev_prefix = render_prefix(prev_context, system_blocks)
curr_prefix = render_prefix(curr_context, system_blocks)
if prev_prefix == curr_prefix:
return {"stable": True, "reason": "prefix_identical", "invalidated_tokens": 0}
# 找出第一个分歧位置,其后的全部 Token 缓存都失效
diverge_at = first_difference(prev_prefix.split(), curr_prefix.split())
return {
"stable": False,
"reason": "prefix_mutated",
"diverge_at": diverge_at,
"invalidated_tokens": len(prev_prefix.split()) - diverge_at,
}
真实输出(把时间戳放进 System 区的对照实验):
bash
[前缀稳定] 系统提示含时间戳 = False
前缀一致 → cache hit,重算 0 token
[前缀不稳定] 系统提示含时间戳 = True
在第 18 个 token 处分歧("当前时间:2026-09-28 16:29")
→ 其后 1,742 个 token 全部失效,需重新 prefill
1 个动态字段让 1742 个 Token 的缓存作废。 这就是「动态内容必须后置」的量化依据。
9.3 核心代码节选(三):四种压缩手段的压缩比对比#
bash
STRATEGIES = {
"truncate": lambda item: {"handle": None, "text": ""}, # 丢弃
"summarize": lambda item: summarize(item, keep_entities=KEY_ENTITIES), # 结构化摘要
"externalize": lambda item: { # 留句柄
"handle": write_artifact(item),
"text": f"{item.kind} 已保存至 {item.path}({item.lines} 行)",
},
"subagent": lambda item: {"handle": None, "text": run_subagent(item)}, # 换位置执行
}
def compare(context_items, budget):
for name, fn in STRATEGIES.items():
compressed = [fn(it) for it in context_items]
kept = [it for it in context_items if it.kind in KEY_ENTITIES]
yield {
"strategy": name,
"tokens_before": sum(estimate_tokens(i.text) for i in context_items),
"tokens_after": sum(estimate_tokens(c["text"]) for c in compressed),
"key_entities_lost": count_missing_entities(kept, compressed),
}
真实输出(12 条工具结果,含 3 条命中关键实体):
bash
strategy before after ratio 关键实体丢失
truncate 18,240 0 100% 3 / 3 ← 全部丢失
summarize 18,240 1,340 93% 0 / 3 ← 结构化摘要保住了
externalize 18,240 186 99% 0 / 3 ← 句柄可回查
subagent 18,240 240 99% 0 / 3 ← 只回传结论
结论很清晰:截断的压缩比最高,也是唯一丢关键实体的方案。这就是为什么「先砍再从尾部截断」在实践中会出正确性问题。
十、一句话总结#
上下文是 Agent 唯一同时被三重约束的稀缺资源:算力上超线性、显存上线性累积、可用性上还随长度衰减。上下文工程就是在这些约束下做预算管理------分区、按可恢复性排序、保持前缀稳定、并对压缩做有依据的选型。
用户说过的话和不可再生的约束必须原文保留,agent 的中间探索过程才是可以压缩的部分。 压缩没做好,最严重的后果不是变慢或变贵,而是静默地丢掉约束、重做已完成的工作。