系列第一篇聊了执行模型:Agent 不是一次生成,而是以 Run/Turn/Attempt 为节拍的持续运行系统。但执行模型只回答了"Agent 怎么动",没回答"Agent 每动一步时看到什么"。这篇讲后者------上下文工程。它是 Agent 系统里看起来最平常、做错最致命的部分。
Context 不是聊天记录
先破一个幻觉:塞得越多,Agent 越聪明。
很多"Agent 项目"的上下文策略就是 append-only:系统提示、历史消息、检索结果、工具输出,统统往里堆,直到撞窗口上限,然后抱怨模型变笨。变笨不是幻觉------注意力是会稀释的。相关研究(lost-in-the-middle、context rot)反复验证同一件事:输入越长,模型对中段信息的利用率越差,无关信息越多,关键信号越容易被淹。
所以先立本文的第一性原理:
模型没有世界,没有记忆,没有"之前"------它只有这一次调用看到的全部输入。Context 是模型唯一的现实。
上下文工程不是"信息管理",而是为模型构造它此刻的全部现实,且这个现实的唯一目的是让下一步决策更好。由此得到全文唯一的设计宪法:
这段信息会改变当前决策吗?答不上来的,不该进场。
"全"不是目标,"信噪比"才是。
Context 由什么组成:八类成分,三种性质
一个高性能 Agent 系统的上下文,拆开看有八类成分:
| 成分 | 例子 | 生命周期 |
|---|---|---|
| 1. 身份与原则 | system prompt、人格、能力边界 | 版本级慢变 |
| 2. 工具 Schema | 工具定义与参数协议 | 注册级半静态 |
| 3. 会话事实投影 | 用户/assistant 消息、工具调用与结果 | 随 Turn 增长 |
| 4. 压缩摘要 | 被折叠历史的降级形态 | 压缩时重算 |
| 5. 运行环境 | 时间、当前附件、注入的提醒 | 每轮现算 |
| 6. 检索召回 | 长期记忆、知识、跨会话信息 | 按需注入 |
| 7. 输出协议 | 格式约束、结构化输出要求 | 任务级 |
| 8. 惰性内容实体化 | 被折叠内容的按需回读 | 模型主动取回 |
这张表的要害不在列全,在于八类成分分属三种性质:事实(3)、事实的降级(4、8)、环境(1、2、5、6、7)。工程上的大事故几乎都来自性质错置:
- 把环境写进事实:system prompt、每轮提醒混进会话历史------历史从"发生了什么"堕落成"某次渲染的缓存",无法更新、无法重算;
- 把检索当事实保存:召回内容持久化进历史,底层知识更新后历史里还是旧版本;
- 把降级当原文:摘要覆盖原文且不可追溯,模型把摘要里的信息损失当成"没发生过"。
(行业实现各有归属习惯------比如输出协议,有人放在工具侧,有人放在上下文侧,不冲突,关键是想清楚它是什么性质。)
三层架构:Fact / Projection / Render
把三种性质落成架构,就是三层:
Fact 层(发生了什么) 。append-only 的权威存储:会话 transcript、记忆库、产物元数据。它的工程问题是一致性------工具调用与结果必须配对、提交必须原子、恢复只能从它出发。系列第一篇讲的"提交点不变量",落点就在这一层:连续性不来自"模型想过什么",来自已经提交的事实。
Projection 层(模型看到什么)。为当前 Turn 的决策、从 Fact 层和环境临时组装的视图。它可以修复(供应商协议要求的残链修补)、可以降级(压缩、折叠)、可以按需取回(pointer 展开),但遵守一条铁律:
投影永不反写事实。 一次兼容性修复如果写回历史,永久历史就被一次临时处理污染了。
Render 层(最终字节) 。投影序列化成供应商协议报文的最后一刻。它的核心指标是字节确定性------因为 prompt 缓存是前缀字节匹配,字节确定性 = 命中率 = 钱 + 速度(详见杠杆四)。
三层各有各的不变量,跨层泄漏是新手最容易踩的坑:在 Fact 层做渲染优化(污染历史)、在 Render 层做信息选择(抖动缓存)、在 Projection 层持久化(层次崩塌)。
杠杆一:选择------什么有资格进场
选择是宪法"它会改变哪个决策"的落地。一个成熟系统的选择策略通常长这样:
- 近期原文逐字保留:最近的上下文含最多未消解的指代和进行中的任务状态,压缩它等于制造失忆;
- 远期历史摘要化:已完成阶段的细节可以降级为结论;
- 超大工具结果 pointer 化:一个 50KB 的文件内容不该全量进场,进场的是"它存在、是什么、怎么取回";
- 记忆与知识按需检索:不常驻,按当前任务相关性注入。
实战示范(如一):压缩由统一入口自闭环------投影微压缩、主动压缩(折较早前缀为摘要、最近原文逐字保留)、反应式压缩(prompt 超长时的急救全折)单一路径;超大工具结果折叠成 pointer,模型用专门的回读工具按需取回。字节本体不进报文,引用才进。
杠杆二:位置------注意力不是均匀分布的
这个杠杆常被忽视,但非常重要。实证结论(不同模型程度有差异,方向一致):输入的开头与结尾获得最高的注意力权重,中段是洼地。
三条直接推论:
- 身份与硬约束放最前 (system 区),当前任务与最新进展放最后(消息尾部)------这是注意力免费送给你的两个黄金位;
- 长摘要、长篇检索结果天然处于洼地------重要信息若埋在中段,要么前移,要么在尾部用一句话重申;
- 工具 Schema 钉死在第 0 位------它最稳定、最适合当前缀,既占住开头的高权重位,又顺便服务缓存(见杠杆四)。
不要把它当成物理定律(模型间差异很大),但作为工程启发式它几乎从未出错:当你纠结"这段信息放哪"时,答案是"放两端"。
杠杆三:降级------信息的生命周期管理
上下文里的信息不是二元的(在/不在),而是有生命周期的:
text
原文 → 摘要 → pointer → 丢弃
(全量在场) (结论保留) (引用在场) (彻底离场)
压缩的本质不是省 token,是维持决策信噪比。一次好的压缩应该回答:"丢掉这些信息后,模型继续做当前决策会错在哪?"答得上来的信息不能压。
两个工程要点:
- 留 tail:主动压缩只折较早前缀,最近一段原文逐字保留------因为指代("上面那个文件")和进行中的状态都活在尾部;
- 可回读:降级到 pointer 的信息必须给模型留取回通道,否则折叠等于删除------模型会在需要时幻觉出内容补上。
杠杆四:缓存------字节确定性 = 命中率 = 钱 + 速度
主流供应商的 prompt 缓存是前缀字节匹配:请求字节从第 0 位开始与历史请求比对,匹配到哪儿缓存命中到哪儿。这让渲染顺序从"实现细节"升级为"架构决策":
- 稳定前缀 (tools + system)与变动后缀(messages)严格分层,先稳定后变动;
- 前缀里任何抖动------哪怕是一个 map 的迭代序变化------都会让它后面的所有内容缓存全失效;
- 缓存在成本与首 token 延迟上是数量级差异,缓存命中率是被渲染顺序直接决定的架构指标,不是供应商福利。
实战示范(如一) :渲染序钉死为 tools → system → messages;工具 schema 只经单一路径构造并在注册期深冻结;所有多键 JSON 对象走保序构造。曾经踩过的坑:Java 的
Map.of迭代序由 JVM 启动随机盐值决定,进程内稳定、跨实例不同------多实例部署时每个实例的"稳定前缀"都不一样,缓存命中率莫名腰斩,最后靠构造期拒收无序 map(启动即失败)根治。
多 Agent:上下文隔离是排洪区
顺带一提(第三篇展开):SubAgent 常被说成分工,但它更本质的价值是上下文隔离------子任务在自己的上下文里爆炸(几十轮搜索、读写、试错),只把结论回灌主上下文。没有这层排洪区,主上下文在任何一个重任务面前都必死。
度量与收尾
上下文工程和其他工程一样,没有度量就没有优化:
- 缓存命中率 → Render 层健康度;
- 每 Turn token 构成分布(固定开销 / 事实 / 检索占比)→ 组成合理性,固定开销占比失控是最常见的慢性病;
- 压缩触发率、恢复重试率、重复工具率 → Projection 层压力与决策质量的代理指标。
收个尾。第一篇说 Agent 的中心是"从消息到事实、再从事实到行动的闭环";这篇补上它的另一半:
模型住在这个闭环里,但它看不见闭环------它只看得见你为这次调用构造的那一小块现实。上下文工程,就是替模型经营这块现实的工程。
系列预告:下一篇《多 Agent 委派》------子 Agent 与后台 Worker 的生命周期、血缘与上下文隔离的完整设计。