第 6 章 上下文工程

第一篇处理的是“怎么说”。从这一篇开始,问题换了一个:模型这一轮到底看见了什么。

很多被归咎于“模型不行”的故障,真正的原因在这里——不是它没听懂,而是它压根没看见该看的东西,或者看见了太多不该看的。

图 6-1 标出第二篇的位置:这一篇把视线从提示词文本移到上下文、检索、工具与外部结果。

全书六篇路线,第二篇被突出显示

图 6-1 全书六篇路线:第二篇


一、全都塞进去,效果反而变差

一个很常见的做法:既然不确定模型需要什么,那就都给它。

知识库检索到的十条片段,全放;对话历史,从第一轮开始全带;示例,攒了二十条,全塞;业务规则,写了三页,一起附上。反正现在的模型上下文窗口很大,装得下。

装得下,但结果往往是:

  • 回答开始变得笼统,抓不住重点;
  • 明明写在规则里的约束,某些轮次不被执行;
  • 偶尔会引用一条相关但不适用的资料,说得还挺像回事;
  • 成本和延迟明显上升;
  • 而且——你不知道这些内容里哪些真正起了作用。

最后一条是最麻烦的。当上下文里同时塞了好几类内容,某一轮出错时,你没有办法判断是哪一类干扰了判断。塞满上下文不只是浪费,它还会让你失去诊断能力——这和第 5 章讲过度工程时的那条教训是同一个道理。

这一章要处理的就是这个问题:这一轮,究竟应该让模型看见什么。

二、上下文不是仓库,是工作台

先做一个概念上的切换。

很多人把上下文当成仓库:把所有可能用得上的东西都放进去,需要的时候模型自己取。这个模型不对。上下文更像工作台——它的面积有限,上面摆的每一样东西都会影响你的注意力,而一张堆满杂物的工作台,会让人找不到真正要用的工具。

工作台上通常有四类东西,它们的性质完全不同:

表 6-1 上下文工作台的四类内容

类别 内容 特点
指令与规则 任务、约束、输出契约 相对稳定,每轮基本相同
本轮输入 用户这一轮的请求、要处理的对象 每轮必需,通常不大
参考材料 检索片段、工具回执、外部文档 每轮变化,质量参差,量最容易失控
历史与状态 对话历史、已提交的业务字段、流程阶段 随会话增长,是长会话中最大的膨胀源

后两类通常最容易随系统运行而膨胀,也最容易引入污染。指令写得再好,如果材料是错的或者状态是乱的,输出照样不对。

这里要先避开一个后面很容易出错的混淆:上下文说的是“这一轮模型看见什么”,而证据和状态说的是“它看见的东西在系统里是什么身份”。

用户说“我们是 A 公司”,这句原话是证据;经过归并写进字段的“单位=A 公司”,是状态。下一轮把这个字段注入上下文,它仍然是状态,不会因为被放进上下文就重新变回证据。这两个概念在第 11、12 章会正式展开,现在只需要记住一句:注入不改变身份。

三、“多”为什么会伤害

第一,竞争与稀释。 第 1 章那个项目观察到:规则增多之后,一部分约束的执行变得不稳定。更一般地说,上下文变长以后,模型拿到的不只是更多信息,还有更多需要区分、取舍和关联的候选内容——它们可能发生竞争。“装得下”不等于“总能被同样稳定地利用”。

第二,位置带来的差异。

研究支持 长上下文中信息所处的位置会影响它被利用的程度,这一现象在多项研究中被观察到。但不同模型、不同任务上的表现并不一致,而且随着模型演进也在变化,不宜当作固定规律套用。具体文献与实验条件见附录 E。

值得回想第 1 章那个哨兵实验:三个位置的口令都能被正确触发,说明内容确实进入了模型的视野。但“能被触发”和“在一次复杂判断中被充分利用”是两回事。可达不等于有效——哨兵能证明前者,证明不了后者。

第三,干扰。 这一条最容易被低估:真正危险的不是完全无关的内容,而是相关但不适用的内容。

生产观察 检索回来一条讲的是另一款相近型号的资料,模型很可能把它的参数说成当前产品的——因为它看起来处处都对得上。最危险的干扰项不是噪声,是长得像正确答案的东西。 第 7 章会展开这类故障。

第四,成本与延迟。 这条最直观,通常也最容易被接受,但它往往不是最大的代价。

四、把上下文当预算来管

既然工作台面积有限,就需要预算。

预算管理有三个动作:定总量、分配额度、定超额时的处置顺序。

总量不等于模型的上下文窗口——那只是物理上限。实际预算应该由成本、延迟和效果共同决定,不应默认等于窗口上限;很多生产任务的合理配置会明显低于它。

分配的原则是按“这一轮的必要性”排序,而不是按“可能有用”:

必须有        本轮用户输入、任务指令、输出契约
高价值        与本轮直接相关的证据(少而准)
有条件保留    示例(够用即可)、最近若干轮历史
可压缩        更早的历史(摘要化)
可外置或按需  确定性规则 → 代码;大段知识 → 检索;与本轮无关的背景 → 不注入

最后一行需要说清楚,因为“外置”要看规则的性质:手机号必须十一位、同一字段最多问两次,这类确定性规则交给代码执行更可靠(第 16 章);而“用户明确表示拒绝时应当视为拒绝而不是未知”这类需要语义判断的规则,仍然要留在模型看得见的地方——把它挪走,规则就没了。大段背景知识属于第三种:不必常驻,按需检索。

还有一条需要提前说清楚:历史摘要只是上下文的压缩产物,不能替代权威状态。 已确认的事实、用户的拒绝、前后冲突、明确撤回——这些会影响后续决策的信息,应该由状态机制单独保存。摘要负责帮模型理解背景,不负责充当事实来源。这两者的区别,是第 8 章的主题。

当预算超了,不要按“历史→检索→示例→指令”这种固定顺序机械裁剪。真正该保护的不是某个内容类别,而是“对当前这次决策不可缺少的信息”:用户上一轮刚明确拒绝过留电话,这条历史比三条检索片段重要得多;而某个问题的答案可能恰好只在检索结果的第三条里,把 top3 砍成 top2 就直接把答案砍掉了。

按必要性和边际贡献来处理更可靠:

  1. 先保住本轮不可缺少的任务约束、关键材料和权威状态;
  2. 删掉重复、失效、明显不相关的内容;
  3. 对可压缩的内容做摘要或结构化;
  4. 示例条数、检索条数、历史长度到底保留多少,交给下一节的实验来定。

适用边界 “模型支持很长的上下文”不等于“可以随便塞”。窗口大小决定的是能不能装下,不决定装满之后效果是否更好,也不改变成本和延迟随长度上升的事实。窗口是容量约束,预算是工程决策,两者不能互相替代。

五、怎么知道该放什么:做一次消融实验

上面这些原则能帮你排序,但没法告诉你具体答案:这个任务到底需要几条示例、几条检索片段、几轮历史?

这个答案依赖任务、依赖模型、依赖数据分布,没有通用值。它只能测出来。

方法叫消融实验,做法很朴素:从最小集合开始,逐项加入;或者从全集开始,逐项删除;每次都跑同一份回归集,记录效果、成本和延迟。

一份可以照着填的表:

表 6-2 上下文消融的实验记录模板

配置 上下文包含 回归通过率 平均 token 平均延迟
A 指令 + 本轮输入
B A + 4 条示例
C B + 检索 top3
D C + 最近 5 轮历史
E D + 全部历史 + 检索 top10

跑完之后要找的不是“效果最好的那一行”,而是在满足验收标准的前提下成本最低的那一行

做法是先把验收线定下来——比如关键用例必须全过、总体通过率不低于某个值、P95 延迟不超过某个值——然后在所有达标的配置里,挑上下文最短、成本最低的一组。优化目标不是“上下文越短越好”,而是在质量、成本和延迟之间,找到满足验收条件的最小配置。

跑表的时候,重点观察三件事:

  • 加入某一部分之后,效果有没有真的提升;
  • 这份提升,值不值它带来的 token 与延迟成本;
  • 继续加内容之后,是否出现停滞甚至下降——那正是本章第一节说的那个现象。

这件事的价值不只是省钱。当你知道每一部分带来了多少变化,某一天效果下降时,就多了一份可对照的排查依据。 消融表本身就是一份诊断资料。

适用边界 这种逐项增减适合工程诊断,但它测到的是“在当前加入顺序和当前配置下的边际变化”,不是各部分严格的独立贡献。示例、检索和历史之间可能存在交互,换一个加入顺序,结论也可能不同。如果需要更严谨的比较,应当测试多种组合或多种顺序。

适用边界 消融实验需要一份可靠的回归集才有意义——没有稳定的判分口径,跑出来的差异分不清是配置的作用还是随机波动。如果你还没有回归集,那件事的优先级高于优化上下文。第 19 章会讲怎么建。

六、排布:位置与结构

内容定下来之后,还有个怎么摆的问题。三条实用建议:

第一,按变动频率分区。 稳定不变的部分(角色、任务、输出契约、长期规则)放在一起且位置固定;每轮变化的部分(本轮输入、检索证据、状态)单独成区。这样做有两个好处:人容易维护,机器也容易复用——部分模型平台支持提示词缓存一类的机制,稳定前缀可能提高缓存命中、降低重复输入的成本。具体的触发条件、最小长度和计费规则属于平台实现细节,会随时间变化;本节只讲原理,参数以在线附录为准。

第二,材料要按角色分区隔离。 这是第 3 章那条分隔符纪律的延续,但需要说得更准:检索片段、工具回执、被分析的文档属于材料;而当用户消息本身就是要执行的请求时(“帮我查明天上海的天气”),其中的合法意图当然是指令,不能一并当成数据。

真正需要隔离的,是嵌在材料里的指令性文本——被分析的那封邮件里写着“忽略前面的要求”,它仍然只是邮件的内容。第 7 章会说明为什么这一条是本篇的核心。

第三,别指望靠声明来抢注意力。 把一条规则标上“最高优先级”“必须严格遵守”“重要!!!”,并不能可靠地让它在所有轮次都被执行——第 1 章那条被标注了三次的规则就是例子。位置、篇幅和结构是可以系统性调节的工程手段;单纯提高措辞强度,不是一种可靠的控制机制。 能做的第一步是减少竞争内容,并把规则改成边界清楚的结构;具备应用层条件时,再把能够确定执行的约束交给代码或其他确定性节点保证(第 16 章、第 18 章)。

七、和提示词工程的分界

这两件事经常被混着说,但它们的失败长得不一样,修法也不同。

表 6-3 提示词工程与上下文工程的分工

提示词工程 上下文工程
回答的问题 这次任务该怎么做、遵循什么规则 这一轮该看见什么信息
典型失败 说不清楚、边界没定义、输出不可解析 该看的没看见、看见了错的、看见了太多
修法 改契约、补定义、加示例 改检索、改注入、改预算、改排布

区分它们有一个很实用的自查方法:

在受控的调试环境里,重建这一轮模型实际收到的完整上下文(必要时先做脱敏),然后问自己:如果我是模型,只看到这些,我能做对吗?

这个动作看起来简单,但在真实系统里经常会发现意外——某个变量没渲染进去、历史被截断在半句话、检索片段其实是空的、同一条规则被拼进去了两次。这类问题改一百遍提示词也不会好,因为问题根本不在提示词里。

第 1 章那次探针实验,本质上就是这个自查动作的一种工具化形式。

八、看得见,不代表看的是对的

假设你现在做完了这一章的全部工作:内容选对了、预算合理、排布清晰、各部分加入后的效果变化都测过。

还有一个问题没有解决:送进去的东西,本身是不是对的。

检索片段可能命中了错误的文档;注入的业务字段可能是空值或者占位符;同名的两个产品可能被混在一起;更麻烦的是,材料里可能带着一句“请忽略前面的要求”。

这些内容会以完全合法的形式进入上下文——格式正确、位置正确、预算之内,但内容是错的,甚至是敌意的。

上下文编排解决的是“什么进入这一轮的模型输入”;它并不保证这些内容本身真实、可信、适用于当前这个对象。