[agent开发面试]上下文工程与压缩:Agent 的稀缺资源

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×),但必然有损------一个文件路径、一个精确报错、一个约束条件,都可能在摘要中消失,即使它后面仍然重要。

工程上必须做的三件事:

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

第 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 次 回退内置摘要
最大风险 摘要丢精确细节 约束可能被压缩掉 判定错误的删除不可逆

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

  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 可观测约束#

至少要能回答四个问题:

bash 复制代码
这一轮上下文里装了哪些分区?各占多少 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 的做法是在基础指令里常驻一段说明,教模型如何理解压缩:

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 的中间探索过程才是可以压缩的部分。 压缩没做好,最严重的后果不是变慢或变贵,而是静默地丢掉约束、重做已完成的工作。

相关推荐
Luhui_Dev1 小时前
数学问题在 Agent 实践中的难点
人工智能·数学·agent
LensonYuan1 小时前
Cursor状态栏一键切换OpenAI API Key
人工智能·python·cursor
用户2515916173401 小时前
Agent 教程看了不少,我决定拿三个旧例子补基础
人工智能
MacroZheng1 小时前
装上这款全能增强插件,DeepSeek Harness瞬间高大上了!
java·人工智能·后端
时间的拾荒人1 小时前
Qt 信号与槽机制详解(二):连接方式详解与面试指南
开发语言·qt·面试
u1301301 小时前
GitHub 热榜项目:日榜(2026-09-28)
人工智能·github
喜欢睡觉1 小时前
一个不会胡说八道的客服机器人:RAG 的原理与工程实现
人工智能
AI深栈1 小时前
第 19 章 · AI 工具循环与条件路由:管住 recursionLimit
java·人工智能
YOLO数据集集合1 小时前
小鸡计数检测数据集 |小鸡计数 密集目标检测 智慧畜牧 雏鸡管理9124期
人工智能·目标检测·目标跟踪·小鸡·小鸡计数