上下文工程
决定什么内容进入上下文窗口、以什么顺序进入。系统变复杂之后,取代 prompt 工程的那门手艺。
prompt 工程问的是怎么把一个请求说清楚。上下文工程问的问题更大:在 token 预算有限的前提下,哪些内容该放进去,又该按什么顺序放?
只问一个问题时,措辞决定一切。但一个带工具、检索文档、对话历史和记忆的 agent,措辞只是影响输出质量的一小部分。所有内容都在争抢同一份预算。
谁在争抢
每次请求都会从下面这些内容里挑出一部分:
- System prompt:角色、约束、输出约定
- 工具定义:每个可用工具一份 schema
- 少样本示例
- 检索到的文档
- 对话历史
- 本次会话的工具返回结果
- 存储的记忆
- 当前的这次请求
- 留给答案的空间
每一项都有用,合起来却超出任何窗口的容量。上下文工程做的就是这道分配题。
位置和内容一样重要
最值得付诸实践的一条结论:注意力在上下文中并不是均匀分布的。 开头和结尾的内容,比中间的内容更容易被可靠地用到。
原因有两个,彼此独立。有些架构用的是近似注意力,不会查看所有位置。而且就算注意力是精确的,模型也得学会使用远处的位置——极端距离在训练中出现得太少。
由此得到的排布方式:
[system prompt] ← 稳定、已缓存、注意力高
[工具定义] ← 稳定、已缓存
[少样本示例] ← 稳定、已缓存
[检索到的上下文] ← 可变、最强条目放两端
[对话历史] ← 可变、按需截断
[当前请求] ← 最后、注意力最高稳定内容放在前面一举两得:既占据了高注意力位置,又启用了前缀缓存,几乎不花力气就能降成本。可变内容放在后面。
在检索到的内容内部,按相关性排序,并把最强的条目放在两端,而不是照排名顺序一路铺开——那样会把第三条埋进注意力偏低的中间区域。
少往往就是多
一个反直觉、但值得内化的结论:加入勉强相关的内容,常常会让输出变差。
够用五个文本块,却检索了二十个,成本翻两番,答案还更差。多出来的十五个稀释了注意力,偶尔还会有一个被用上——于是产出一个自信的错误答案,而来源只是一段看着相关的文字。
这就是为什么重排序提升的是质量,而不只是效率;也是为什么「我们有一百万 token 的窗口,全都塞进去」是个错误。精确胜过堆量。
工具定义也是上下文
容易忽略:每份工具定义在每次请求里都占 token。二十份啰嗦的定义,等于每次调用都背着一笔可观的开销,而选择准确率会随工具集变大而下降。
按需路由到相关的子集,别把所有工具都摊开。成本和准确率会一起受益。
控制增长
agent 的上下文每迭代一轮就长一截。按价值从高到低排列的手段:
压缩工具返回结果。 回报最高的一招。只返回标识符和摘要,另开一个工具在需要时取完整内容。
截断旧历史,但一定要显式保留任务陈述。因截断而丢掉目标,是常见又让人摸不着头脑的失效方式。
每轮重述一遍目标。 成本很低,而且能在对话越来越长时对抗目标漂移。
把状态外置。 把发现写进文件,让 agent 可以回读。这是唯一能撑住真正长任务的办法。
隔离上下文。 让子任务各自拥有简短的记录——这也是支持多 agent 系统最有力的理由。
要点回顾
- 上下文工程是在互相竞争的内容之间分配有限的 token 预算;系统变复杂之后,它比措辞更重要。
- 位置很重要:稳定内容放前面(注意力高,还能吃到前缀缓存),当前请求放最后。
- 检索到的内容里,最强的条目放两端,不要照排名顺序排。
- 加入边际内容会让输出变差——精确胜过堆量,这也是重排序能提升质量的原因。
- 工具定义会占用上下文并降低选择准确率;按需路由到子集。
- 控制增长:压缩工具返回结果、截断时保住目标、重述目标、外置状态。
下一篇: 编程 agent