Agent 面试

上下文工程与压缩:Agent 的稀缺资源

上下文工程与压缩:Agent 的稀缺资源

Context Window 是模型一次能看的 Token 上限,但它不是「可用的工作记忆」。注意力是二次代价、KV Cache 线性占用显存、位置偏差让中间内容系统性失效——上下文既贵又不可靠。上下文工程就是在这三重要约束下做预算管理:什么放进去、放哪个位置、什么时候换掉、换掉的信息去哪里。

前四期已经建立了「模型只提出决策、运行时负责执行」这条主线:

  • 第 01 期说明了 LLM 是概率生成模型,Attention 是 O(n²)、KV Cache 是必备优化;
  • 第 02 期把 Agent 拆成 Model / Prompt / State / Memory / Tools / Guardrails;
  • 第 03 期讲清了 Tool Call 从意图到执行的完整链路;
  • 第 04 期讲清了工具能力如何被发现、接入和治理。

这一期回到那个所有 Agent 最终都会撞上的瓶颈:模型每一轮决策时能看到的那一小块 Token 空间。工具越多、任务越长、检索越频繁,这块空间就越紧张。而「把它塞满」从来不是解法——它是问题的开始。

本篇回答两个问题:

#面试问题指向哪个工程面
Q1Context Engineering 和 Prompt Engineering 有什么区别?为什么不能把信息全塞进上下文?上下文的物理成本模型与预算分配
Q2上下文满了怎么压?Tool Result 太大、搜索返回 100 条、多轮膨胀分别怎么办?压缩手段分类与工程实现

两题之间有明确的因果链:Q1 说明「为什么有限」,Q2 说明「在有限下怎么办」。能答出 Q1 的成本模型,Q2 的方案才有选择依据;否则压缩就退化成「随机砍一刀」。


一、Context Engineering 与 Prompt Engineering 是什么关系

1.1 推荐回答

Prompt Engineering 关注单个提示的表述方式——指令怎么写、示例放几条、格式怎么约束。它优化的是「同一份输入里,措辞如何影响输出」。

Context Engineering 关注整个上下文空间的生命周期管理——哪些信息该进来、占多少预算、放在什么位置、什么时候被替换或压缩、替换后信息去哪里。它优化的是「在有限预算内,模型每一轮能看到什么」。

两者是包含关系:

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 计算相关性,序列长度翻倍,计算量约翻四倍。

长度 n 的输入 → 注意力计算量 ∝ n²

工程含义非常直接:

上下文长度相对注意力计算量相对成本(示意)
4K1×1×
32K64×~16×(受 KV Cache 与并行度影响,非线性)
128K1024×~40–60×

实际成本曲线受实现优化、批处理、缓存命中影响,不会严格等于 n²,但「长上下文显著更贵、且贵得超线性」这个结论是稳定的。这也解释了为什么厂商的长上下文通常要单独定价。

2.2 理由二:KV Cache 线性占用显存,且随轮次累积

第 01 期还讲过:生成每个 Token 都要复用之前所有 Token 的 K、V。Agent 运行时每轮都要重发完整上下文,KV Cache 从第 1 轮持续涨到第 N 轮。

第 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 四个理由合起来

塞满上下文
   ├── 成本超线性上升(注意力 O(n²) + KV Cache 累积)
   ├── 延迟上升(prefill 变长)
   ├── 关键信息被淹没(lost in the middle)
   └── 整体质量下降(context rot)

这不是四个独立缺点,而是同一个事实的四个侧面:Context Window 是容量,不是工作记忆。 有效上下文远小于标称窗口。


三、上下文分区与预算设计

3.1 信息漏斗

从「模型能看到的最大范围」到「真正参与本轮决策的信息」,中间有四层筛选:

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 优先级设计:按「可恢复性」排序,不是按「重要性」

一个常见的错误是「按重要性排序」。更可靠的标准是这条信息一旦丢失,能否恢复:

不可逆(丢了就没了,必须保)
  ├── 用户已确认的决策与结论
  ├── 关键 ID(订单号、任务号、会话号)
  ├── 用户明确给出的约束("不要动 migrations/")
  └── 已执行的副作用记录(改过哪些文件、发过哪些请求)
        ↓
当前任务状态(可重建,但重建有成本)
        ↓
近期对话原文(可从存储回查)
        ↓
历史工具结果(可重新调用,代价是时间与钱)
        ↓
早期寒暄与重复的中间推理(无保留价值)

判据一句话:「后面还会不会被追问?」 会 → 保;不会 → 可裁。

注意前两项的特殊地位:已确认的结论和用户约束是「不可再生资源」。它们一旦被压缩掉,模型无法通过任何手段重新推导出来——用户不会再重复说一遍,任务也不会自动回滚。这也是下一节 Codex 案例中「governance decay」问题的根源。

3.4 前缀稳定性决定 KV Cache 命中率

第 01 期提到 KV Cache 的三种状态:prefill(首次计算)、decode(逐 Token)、cache hit(前缀复用)。只有前缀完全一致,缓存才能命中。

这意味着:

✅ 稳定前缀(可命中缓存)
[System: 身份 + 约束] [工具定义] [稳定示例] ... [动态内容在最后]

❌ 前缀被污染(缓存频繁失效)
[System: 身份 + 约束 + 当前时间戳] [用户画像] [工具定义] ...
   ← 时间戳每轮都变,导致它之后的全部内容缓存失效

把动态内容塞进 System 区,是 Agent 成本失控最常见的单一原因。 正确做法是把所有会变的东西(时间、用户状态、检索结果、工具结果)一律推到前缀末尾。

这不是微优化:一次缓存失效意味着整个前缀重新 prefill,成本和延迟都会跳一档。

3.5 System 与 User 的分界是「生命周期」,不是「重要性」

面试中常被问「什么该放 System,什么该放 User」。判据不是「哪个更重要」,而是哪个活得更久:

  • System = 跨轮、跨任务稳定的契约(角色、能力边界、输出规范、安全约束);
  • User = 本次任务特有的输入(目标、参数、本轮问题)。

工具定义放哪取决于它是否频繁变化:工具集稳定 → 放 System 享受缓存;工具集按任务动态组装 → 动态注入,但要注意这样一来 System 之后的缓存会受影响,需要权衡。


四、上下文压缩:四种手段(本篇骨架)

这是 Q2 的主体。市面上的「压缩技巧」很多,但按信息去哪里了分类,只有四种。这个分类的好处是:选型时有明确的判断依据,而不是罗列技巧。

4.1 总览

手段信息去向压缩比可逆性代价适用信息
截断直接丢弃最高不可逆零成本不会再被追问的内容
摘要压缩成语义摘要高(10–50×)有损,细节丢失一次模型调用长文本、长对话
外部化移出上下文,留句柄高可回查(原样)存储 + 一次读回大文件、长日志、中间产物
子 Agent 隔离换到另一个上下文执行最高结论可回传一次额外调用探索型脏活

选择依据一句话:这段信息后面还会不会被追问?

会被追问,且需要精确原文  →  外部化(留句柄 + 摘要,需要时读回)
会被追问,但只需语义       →  摘要
不会被追问                →  截断
整段工作本身就把上下文搞脏  →  子 Agent 隔离

4.2 截断:最便宜,也最容易切错

截断是把内容直接从上下文中丢弃。它的优势是零成本、零延迟;风险是不可逆。

适用的典型场景:

  • 开头的寒暄与确认(“你好” “好的” “帮我看看”);
  • 已经得出结论的中间推理过程(保留结论,丢掉推演);
  • 重复的工具结果(同一个文件被读了三次,只留最后一次);
  • 已过期的中间状态(计划已经迭代了三版,只留最新版)。

不适合:任何可能被追问的内容——用户给过的约束、已确认的决策、关键 ID、副作用记录。

max_tokens / max_steps 这类硬性预算,本质也是截断的一种兜底形式。

4.3 摘要:有损压缩,必须保住关键实体

摘要是让模型把长内容换成语义摘要。压缩比通常很高(10–50×),但必然有损——一个文件路径、一个精确报错、一个约束条件,都可能在摘要中消失,即使它后面仍然重要。

工程上必须做的三件事:

  1. 结构化摘要,而不是自由发挥。给定固定小标题(目标 / 约束 / 已完成 / 待办 / 关键决策 / 当前状态),强制模型填格子;
  2. 保留关键实体:路径、ID、报错原文、命令、用户原话,要求逐字保留而不是转述;
  3. 原文可回查:摘要旁边保留原文的存储位置,需要时能读回原样。

第 4.7 节的三个真实方案(Claude Code、Codex、JEV)在这三点上的选择差异很大,正是本节最有价值的对照素材。

4.4 外部化:把状态移出上下文,只留句柄

外部化是把大块内容写进文件 / 数据库 / 状态存储,上下文里只留引用句柄 + 一句话摘要:

❌ 直接注入
[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 只收到结论:

主 Agent 上下文
   │
   ├── 派发:分析这 30 个文件的依赖关系
   │        ↓
   │   子 Agent(独立上下文,可读 30 个文件、跑 20 次搜索)
   │        ↓
   └── 回传:结论 + 证据引用(约 500 token)

原来需要 60K Token 的探索过程,主上下文只增加 500 Token。这是压缩比最高的手段。

代价有两处:一是多一次调用(成本与延迟);二是信息经过了一次有损传递——子 Agent 可能漏掉主 Agent 需要但当时不知道需要的细节。缓解办法是要求子 Agent 回传结构化结论 + 证据句柄,让主 Agent 需要时能按句柄深挖。

多 Agent 的完整讨论见第 11 期;这里只把它作为一种压缩手段。

4.6 三个高频场景对症

场景一:Tool Result 太大(比如一次数据库查询返回 800 行)

组合方案:
  1. 结构化抽取——只回传决策需要的字段,丢掉其余列
  2. 外部化——全量写文件,上下文保留「路径 + 行数 + 关键统计」
  3. 分页取用——让 Agent 决定要不要读更多,而不是一次全给它

关键是不要把「原始返回」当成「必须注入的内容」。工具返回是数据源,不是上下文条目——中间应该有一层「投影」。

场景二:搜索返回 100 条结果

❌ 100 条全部注入              → 噪声淹没信噪比
❌ rerank 后注入 top-10 却不说明  → Agent 以为只有 10 条

✅ rerank 取 top-5 注入
   + 明确提示「共 100 条,已按相关性展示前 5 条,可继续检索」
   → 由 Agent 决定是否扩大范围

这里的关键是保留「还有更多」这个元信息。压缩掉了内容,但不能压缩掉「存在更多内容」这一事实——否则模型会误判信息完备性并过早收敛。

场景三:多轮对话膨胀

四种手段组合:

最近 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 不是单一机制,而是三层递进,从最便宜的手段开始,只有在不够时才升级:

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 窗口为例):

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 段固定结构的,这比「请总结一下」可靠得多:

#段落作用
1Primary Request and Intent用户到底要做什么
2Key Technical Concepts涉及的技术概念
3Files and Code Sections保留完整代码片段
4Errors and Fixes报错与修复方式
5Problem Solving试过什么、做了什么决策
6All User Messages非工具结果的用户消息逐字保留
7Pending Tasks还没做完的事
8Current Work摘要前正在做的具体工作
9Optional 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)。

流程:

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 token3,368 token
大会话(保留 13 项)226,661 token19,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 去总结旧轮次。摘要是有损的——一个文件路径、一处精确报错、一个约束、一条命令,都可能在后面仍然重要的时候消失。这个库从不重写任何内容。

它做的事:

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 CodeCodex(v2)fast-jev-compaction
摘要还是删除摘要(Tier 3)+ 删除(Tier 1)摘要(服务端)只删除 / 截断,绝不摘要
压缩在哪做客户端服务端(加密 blob)客户端(经 Jev 判定)
保留用户原文✅ 第 6 段逐字✅ 64K 预算内原文✅ 文本消息永不动
格式约束9 段固定结构服务端自由笔记不适用(不生成新文本)
触发时机167K(200K 窗口)90% × 解析窗口挂到 /compact 钩子
成本策略三层渐进,尽量走零成本层复用缓存的 forked 调用一次并发判定请求
失败处理连续 3 次熔断重试上限 2 次回退内置摘要
最大风险摘要丢精确细节约束可能被压缩掉判定错误的删除不可逆

这三种方案的共同点,比差异更有价值:

  1. 都不让压缩后的内容脱离「按轮次分组」的结构——绝不允许出现「有 tool_result 没有 tool_use」;
  2. 都保护用户原始输入——用户说过什么必须留在明文里;
  3. 都在远未填满时触发;
  4. 都有明确的失败回退路径——压缩不能成为单点故障。

差异则集中在一个问题上:「模型生成的摘要」和「原始记录的裁剪」哪个更安全? 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 可观测约束

至少要能回答四个问题:

这一轮上下文里装了哪些分区?各占多少 Token?
缓存命中率是多少?前缀失效率是多少?
本轮触发了哪一层压缩?压缩比是多少?
压缩后丢了哪些内容?关键实体还在不在?

前三个是成本与性能问题,第四个是正确性问题——它需要一个回归评测集来回答(与第 12 期的评估体系打通)。


六、深入讨论

6.1 摘要会丢信息,怎么保证不丢关键的

三层保险:

  1. 结构化摘要,强制填固定小标题(Claude Code 的 9 段就是这个思路);
  2. 关键实体白名单:路径、ID、报错原文、用户原话、命令——要求逐字复制而不是转述;
  3. 原文可回查:摘要旁保留原文句柄,需要时读回。

追加一层验证:把压缩前后的上下文分别喂给模型回答同一组问题,答案不一致即说明压缩丢了信息。这就是压缩的回归测试。

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 的做法是在基础指令里常驻一段说明,教模型如何理解压缩:

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 核心代码节选(一):分区预算与优先级裁剪

关键不在「装了什么」,而在超预算时裁谁:

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 核心代码节选(二):前缀稳定性检查

把「动态内容污染前缀」这件事变成可检测的断言:

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 区的对照实验):

[前缀稳定]   系统提示含时间戳 = False
             前缀一致 → cache hit,重算 0 token
[前缀不稳定] 系统提示含时间戳 = True
             在第 18 个 token 处分歧("当前时间:2026-09-28 16:29")
             → 其后 1,742 个 token 全部失效,需重新 prefill

1 个动态字段让 1742 个 Token 的缓存作废。 这就是「动态内容必须后置」的量化依据。

9.3 核心代码节选(三):四种压缩手段的压缩比对比

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 条命中关键实体):

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   ← 只回传结论

结论很清晰:截断的压缩比最高,也是唯一丢关键实体的方案。这就是为什么「先砍再从尾部截断」在实践中会出正确性问题。

9.4 脚本与结论对应表

脚本验证的结论
01_budget.py分区预算与优先级裁剪:system / goal 永不裁
02_prefix_cache.py动态内容进 System 会让整个前缀缓存失效
03_compression.py四种手段的压缩比 vs 关键实体保留率
04_auto_compact.py三层压缩模拟:阈值触发、分层降级、熔断
05_pinning.py约束钉住:压缩后约束违反率的对照

9.5 三个「改一改再跑」的练习

  1. 把 system 也放进裁剪列表,观察约束丢失后任务成功率与「重复询问用户」次数的变化;
  2. 把触发阈值从 90% 降到 60%,观察压缩次数、总成本与最终任务成功率的三角关系;
  3. 把 summarize 换成 truncate,观察「关键实体丢失」如何传导为后续轮次的返工。

十、延伸阅读

站内

  • LLM 基础与模型边界:Attention 的 O(n²) 代价与 KV Cache 的三种状态,是本文成本模型的机制来源。
  • 第 02 期:什么是 AI Agent——State 与 Context 的区别、组件职责边界。
  • 第 06 期:记忆系统——记忆与对话历史的分界、召回与注入策略。
  • 第 08 期:Planning 与状态管理——计划外化与断点续跑,与本文的外部化手段同源。
  • 第 09 期:Harness——Context Manager 挂在运行时的哪一层、暴露什么接口。
  • Agent 记忆专题:召回的注入时机与分层压缩实践。

站外

  • Anthropic:Compaction(Messages API 文档)——服务端按阈值压缩、compaction block、pause_after_compaction 与自定义摘要指令。
  • OpenAI:Codex CLI 上下文管理文档——model_auto_compact_token_limit、tool_output_token_limit、compact_prompt 与 AGENTS.md 的「压缩边界外」定位。
  • Liu et al.:Lost in the Middle: How Language Models Use Long Contexts——位置偏差的原始实验。
  • tamaratran/fast-jev-compaction——不摘要、只做保留/截断/删除判定的压缩实现,附 Claude Code 插件适配。

十一、一句话总结

上下文是 Agent 唯一同时被三重约束的稀缺资源:算力上超线性、显存上线性累积、可用性上还随长度衰减。上下文工程就是在这些约束下做预算管理——分区、按可恢复性排序、保持前缀稳定、并对压缩做有依据的选型。

而三种真实产品方案给出的最一致的一条经验是:用户说过的话和不可再生的约束必须原文保留,agent 的中间探索过程才是可以压缩的部分。 压缩没做好,最严重的后果不是变慢或变贵,而是静默地丢掉约束、重做已完成的工作。

AI Agent面试上下文工程上下文压缩

← 返回面试题