大模型上下文工程的实践:什么该放进上下文,什么不该
Prompt 工程这两年聊得很多,但真正决定模型表现的,往往不是"指令写得漂不漂亮",而是整个上下文窗口里放了什么、放了多少、怎么组织的。这件事有个专门的名字,叫上下文工程(Context Engineering)。
窗口是稀缺资源,放什么、不放什么,直接影响输出质量。这篇讲清楚。
一、上下文工程是什么,和 Prompt 工程什么关系
两者的关系可以这样理解:
- Prompt 工程:研究"怎么吩咐"——指令怎么写、给不给示例、要不要让它一步步想。
- 上下文工程:研究"桌上摆什么"——窗口里除了指令,还要放哪些料、放多少、按什么顺序摆。
模型每一次响应,看的都是整个窗口:系统提示、对话历史、检索到的内容、工具返回结果、用户当前输入,全混在一起。指令写得再好,窗口里塞满噪声,表现一样拉胯。
哦,原来是这样:窗口像模型的工作台桌面。Prompt 工程是"怎么吩咐它干活",上下文工程是"把桌面收拾干净、只摆它需要的东西"。后者常常被忽略,却更影响成败。
二、窗口里的几类内容,各有各的角色
先盘一下,一个典型应用的上下文里都有什么:
- 系统提示:角色设定、规则、边界。模型行为的"总纲"。
- 对话历史:之前聊了什么,让模型能接上上下文。
- 检索内容:RAG 召回的知识片段,模型"不知道的外部信息"。
- 工具结果:模型调工具后拿回来的数据。
- 用户当前输入:这一轮用户到底说了什么。
每一类的地位不一样。系统提示定边界,历史给连续性,检索给外部知识,工具给实时数据,用户输入定本次任务。缺了哪类,模型就"瞎"一角;塞多了哪类,窗口就被占掉。
系统提示值得单独说一下,因为它是窗口里"权重最高"的内容——规则、边界、角色都在这里。写系统提示有几个要点:一是精炼,别写成论文,模型要的是清晰的规则,不是散文;二是具体,"回答要专业"这种话没用,"只回答 XX 领域问题,其他领域明确说不知道"才有用;三是别塞敏感信息,系统提示可能被诱导泄露,密钥、内部地址这类东西别放。
检索内容也一样,放进来之前要加工:截断到相关片段、去掉格式噪声、必要时标明来源。别把检索引擎吐出来的原始片段直接怼进窗口,那里面经常混着乱七八糟的东西。
三、该放进来的:相关、必要、模型不知道的
判断一个东西该不该放进窗口,标准很朴素:
- 和当前任务相关的:用户问退款,就把退款相关的资料放进来;问库存,就放库存的。
- 模型本来不知道的:你的业务规则、私有数据、实时状态,这些模型训练时没见过,必须给。
- 必要的约束:输出格式、安全红线,这些不写清楚,模型就自由发挥。
哦,原来是这样:往窗口里放东西的标准,不是"可能有用的都塞",是"模型缺这个才能答对"。有它没它一个样的,就别占地方。
四、不该放进来的:过时、无关、重复
反过来,这些是典型该清理的:
- 过时的历史:几十轮之前聊过的事,和当前任务无关,还占地方。
- 无关的检索结果:RAG 召回了一堆八竿子打不着的片段,模型会被带偏。
- 重复内容:同一段话在系统提示、历史、检索里出现三遍,纯浪费。
- 工具返回的原始大块数据:查个库返回几百行 JSON,全塞进去,窗口瞬间爆掉。
哦,原来是这样:上下文不是越多越好。塞进去的无关内容,会稀释模型的注意力——关键信息被淹没在噪声中间,模型反而"看不见"了。
五、组织顺序也有讲究:重要的放头尾
窗口里的内容,摆放位置会影响效果。一个被反复观察到的现象是:模型对窗口开头和结尾的内容更敏感,中间部分容易被忽略——这就是"中间遗忘"。
所以组织顺序上有个朴素原则:
- 系统提示放开头:规则、边界,模型时刻要看。
- 最重要的检索内容放靠前或靠后:别埋在中间。
- 用户当前输入放最后:模型"看着"最新问题作答,衔接最自然。
哦,原来是这样:同样的内容,摆的位置不同,效果就不一样。把最重要的东西放两头,中间放次要的,是对"中间遗忘"最直接的对抗。
六、历史管理:压缩、摘要、滑动窗口
对话历史是最容易失控的部分。聊得越久,历史越长,窗口越满。管理方式有几种:
- 滑动窗口:只保留最近 N 轮,更早的直接丢。简单粗暴,适合多数场景。
- 摘要压缩:把更早的历史总结成一段摘要,替代原文。保留了"聊过什么"的脉络,省了 token。
- 状态外置:把关键状态(用户偏好、已确认的事项)存到外部数据库,需要时再取,不占窗口。
哦,原来这样也可以:历史管理的目标不是"记住所有",是"刚刚够"。模型能接上话、不丢关键约定,就够了。全量塞历史,是在用 token 买一种"假的安全感"。
七、工具结果的取舍:不是原样塞
工具返回的结果,经常被直接原样塞进窗口,这是个大坑。
比如查个订单库,返回 500 行 JSON,全塞进去,窗口瞬间被吃掉一大半,而且大部分行和当前问题无关。正确做法是加工一下:只取相关字段、截取需要的行、必要时让模型自己再调一次更精确的查询。
哦,原来是这样:工具结果是"喂给模型的料",也要加工,不是从锅里直接端。原样塞等于把洗菜、切菜的活全丢给了模型,还占了它的台面。
八、不同应用形态,上下文策略不一样
上下文工程没有统一模板,不同应用形态,策略差异很大。
客服/问答类:重点是检索内容的相关度和历史压缩。用户问题分散,检索质量决定答案,历史只要能接上就行。
Agent 类:重点是工具结果的管理。Agent 会连续调多次工具,每次返回一堆数据,窗口膨胀极快。要严格筛选工具结果、及时清掉过时的中间结果。
代码助手类:重点是相关代码的选取。整个项目几十万行,不可能都塞进去。要根据当前编辑位置,把相关的文件、函数、依赖关系挑出来放进去。
哦,原来是这样:上下文工程的策略,跟着应用形态走。没有"通用最优解",只有"你这个场景的取舍"。
九、窗口也是钱:上下文的成本账
上下文工程不只是效果问题,也是成本问题。窗口里的每个 token 都要计费,塞得越多,每次调用越贵。
而且这个成本是乘出来的:一个应用每天调用十万次,每次上下文多塞 2000 token,一天就是两亿的额外 token。这笔账,比换个便宜模型的收益大得多。
所以"该不该放进窗口"这个判断,除了效果,还要算钱:这段内容放进去,带来的质量提升,值不值这点 token 钱。多数时候你会发现,砍掉冗余,效果不掉,成本先降。
哦,原来是这样:上下文工程是效果和成本的双重优化。塞得少不等于效果差,塞得多反而两头亏。
十、上下文和"记忆"的关系
很多人把上下文和"记忆"混为一谈,其实要分清。
上下文是这一次调用里模型能看到的东西,调用结束就没了。记忆是跨调用的、持久的信息——用户偏好、历史结论、长期状态。
记忆的实现,靠的是"把重要的东西持久化,下次调用再放回上下文"。也就是说:记忆是存在外面的,上下文是每次临时搬运进去的。
这个区分很关键:想让模型"记住"用户偏好,不是把所有历史都塞进上下文,而是把关键信息提取出来存好,下次按需放进窗口。
哦,原来这样也可以:上下文是"每次的桌面",记忆是"抽屉"。把该进抽屉的存好,每次只把需要的搬上桌面。
十一、一个上下文组装的骨架
把前面串起来,一个应用组装上下文的流程长这样:
def build_context(user_input, history, retriever, tools_state):
parts = []
parts.append(SYSTEM_PROMPT) # 1. 系统提示,放开头
hits = retriever.search(user_input, top_k=3) # 2. 检索,只取最相关几条
parts += rank_by_relevance(hits) # 3. 相关度排序,重要的靠前
parts.append(compress(history)) # 4. 压缩后的历史
parts.append(format_tool_results(tools_state)) # 5. 工具结果,只留相关字段
parts.append(user_input) # 6. 当前输入,放最后
return "\n".join(parts)
每一类内容,都按"取舍 → 加工 → 排序"三步处理过,再拼进窗口。
十二、常见错误,对照自查
最后列几个最常见的错,对照自查:
- 什么都往里塞:不分相关与否,检索结果全量放,历史全量放,窗口被噪声填满。
- 历史从不压缩:聊到几百轮,窗口全被旧对话占掉。
- 工具结果原样塞:几百行 JSON 直接进窗口,吃掉一半上下文。
- 重要内容埋在中间:关键信息放在窗口正中间,被"中间遗忘"吃掉。
- 把 Prompt 工程当全部:指令写得再精致,窗口里是垃圾,输出也是垃圾。
十三、怎么评估你的上下文策略有没有效
最后说评估。改了上下文策略,怎么知道有没有用?
和评测篇的思路一致:用一批带标准答案的评测题,改前改后各跑一遍,对比答案质量。注意控制变量:这次只改上下文策略,其他(模型、prompt 写法)不动,否则说不清是谁的功劳。
另外可以观察两个间接信号:一是 token 消耗量,砍掉冗余后消耗应该下降;二是"模型答非所问"的比例,上下文干净后应该减少。
上下文工程不是一次性的,业务在变、任务在变,策略也要跟着调。定期跑评测,才是长期的做法。
收尾
上下文工程,本质是"给模型配一张干净的工作台"。该放的放、不该扔的别扔、重要的摆两头、料要加工过再端上去。
窗口就那么大,怎么用,决定了模型能发挥几成。这一层做好,往往比换更贵的模型,提升更明显。