上下文工程:把信息放进窗口的顺序与取舍
奶油甜饼
8 分钟阅读
上下文不是装得越多越好。把用户问题、检索内容、示例、指令塞进同一个窗口时,顺序、取舍和截断策略直接决定输出质量。经验上,模型对窗口首尾的注意力显著高于中间,因此"放什么"和"放哪里"是两项独立的设计,混为一谈就会得到看似丰富、实则失效的上下文。
窗口不是桶,是带权重的栈
大模型处理输入时存在位置偏差:开头的系统指令和结尾的用户问题更容易被遵守,中间的"长文本"常被当作背景弱化处理。这一现象在长上下文场景尤为明显——把关键约束埋在 20 页文档的第 10 页,模型大概率会忽略它。所以上下文工程的第一条原则是:按"被遵守的优先级"而非"信息产生的先后"来排布内容。
典型排布顺序(从高优先级到低):
- 系统指令:角色、输出格式、硬性约束。
- 当前用户问题:放在结尾,紧挨着生成位置。
- 检索到的证据:靠近用户问题,但避免越过指令。
- 少样本示例:夹在指令和问题之间。
这套次序背后的逻辑是:模型在生成第一个字之前,已经对整个窗口做了一次注意力扫描,越靠近两端、越容易被反复回看的位置,权重越高。把"必须照做"的指令放在最前,把"此刻要回答的问题"置于结尾,中间再填充可弱化的背景,符合模型的处理习惯。
检索内容放开头还是结尾
有两种流派。把检索片段放在用户问题之前,能让模型在"读问题"之前先建立知识背景;放在问题之后,则让问题先行锚定注意力,证据作为补充。实测里没有绝对胜者,但有一个稳定结论:检索片段之间要保留结构化分隔(标题、来源标记),且每条证据都要能回溯到原始出处。
下面这张对照表概括了两种位置的取舍:
| 放置位置 |
优势 |
风险 |
适用场景 |
| 问题之前 |
先建背景,回答有依托 |
证据可能被问题冲淡 |
证据强、问题开放 |
| 问题之后 |
问题锚定,证据补强 |
模型可能没读完长证据 |
问题明确、需精准引用 |
| 首尾各放摘要 |
兼顾两端注意力 |
占用双倍预算 |
证据极长且关键 |
经验上,对"需要引用原文"的任务,把证据紧挨问题效果更好;对"需要综合理解"的任务,前置证据更稳。
用预算把排布显式化
下面这段组装逻辑把预算分配显式化,而不是依赖"拼接凑够为止":
def build_messages(system_prompt, query, retrieved, examples,
max_tokens=120000, reserve=2000):
"""按优先级组装上下文,超预算时从低优先级开始裁剪。"""
budget = max_tokens - reserve - _estimate(system_prompt) - _estimate(query)
# 1. 示例占用预算,超出则截断数量
kept_examples = []
for ex in examples:
cost = _estimate(ex)
if cost <= budget:
kept_examples.append(ex)
budget -= cost
else:
break
# 2. 检索内容贪心放入,单条过长则内部截断
kept_docs = []
for doc in sorted(retrieved, key=lambda d: d.score, reverse=True):
cost = _estimate(doc.text)
if cost <= budget:
kept_docs.append(doc)
budget -= cost
else:
head = _truncate(doc.text, budget)
if head:
kept_docs.append(doc._replace(text=head))
break
return [
{"role": "system", "content": system_prompt},
*[e.to_message() for e in kept_examples],
*[{"role": "user", "content": d.to_block()} for d in kept_docs],
{"role": "user", "content": query},
]
_estimate 用字符数或 tokenizer 近似即可,工程上不必追求精确计数,留出 reserve 给输出空间更重要。真正容易出错的不是计数误差,而是忘了给输出留地盘,导致收尾一轮被生硬截断。
取舍:哪些信息该被牺牲
预算永远不够。裁剪顺序要和业务风险挂钩:
- 重复说明:多轮对话里把历史整段塞回,是最大浪费。改为保留"上一轮的决策结论"而非完整对话。
- 低分检索:向量召回的前 N 条里,尾部相关性常断崖下跌,截断它们几乎无损。
- 完整文档:给模型整篇 PDF,不如给"定位到的章节 + 页码"。模型要的是"在哪",不是"全部"。
一个常见的误判是"宁可多给不可少给"。实测里过量的弱相关证据会稀释关键证据,模型在大量相似信息中反而抓不住重点。给得准,比给得多管用。
边界条件与踩坑
- 截断会静默丢信息。一定要在返回里带上"被丢弃了什么",否则下游无法判断回答是否基于完整证据。
- 系统指令被长上下文稀释时,用"重复约束"补充:在用户问题末尾再写一遍关键格式要求,成本极低,收益稳定。
- 多轮对话的上下文会随轮次膨胀,建议每轮只回灌"增量结论",而不是把整棵对话树重发。
- 不同模型对长输入的敏感度差异很大,换模型时必须重新跑一遍"中间内容是否被忽略"的探针测试。
- 少样本示例若和问题领域不一致,反而带偏输出,示例要和目标任务的分布对齐,而不是随手找几个。
- 把用户原话和检索证据用明确分隔符隔开,避免模型把证据误读成用户指令,造成"被证据劫持"的幻觉式回答。
指令与证据的冲突:谁压过谁
检索到的证据有时会和系统指令直接矛盾。比如指令要求"只基于给定资料回答,不得编造",但某条高相关片段本身包含过时结论。模型在这种冲突下的行为不稳定:有时信指令,有时信证据。工程上的解法不是赌模型选对边,而是在指令里显式写明优先级——"当证据与问题无关或来源不明时,明确说不知道,不要硬凑答案"。把优先级写死,比指望模型自己权衡可靠得多。
一个常见的隐性冲突,是"少样本示例"和"检索证据"抢话语权。示例告诉模型"这类问题该这样答",证据却指向另一个事实,两者方向相反时,输出会左右摇摆。经验上,当任务依赖实时或私有证据时,应降低示例权重,少放或不放,让证据主导;当任务依赖固定格式输出时,示例优先,证据只作事实填充。这条取舍没写在提示里,就只能靠试错发现。
还要注意"指令过长被自身稀释"的现象:当系统提示超过一定长度,结尾的约束可能被模型弱化。对策是在用户问题末尾再重复一次关键约束,用极低成本把最重要的一条重新推到高注意力位置。这不是冗余,是针对模型位置偏差的补偿手段。
一个实操提醒
排布顺序不是写死一次就完事。同一套上下文结构,在不同模型、不同任务上表现会有差异。建议把"顺序"和"预算分配"做成可配置项,配合前面说的探针测试,定期验证当前排布是否仍最优。上下文工程是持续调参,不是一次性设计,把它当成需要监控的指标,而非写定的模板。
上下文工程本质是预算分配问题:在有限的 token 里,把最该被遵守的内容放在最该被注意的位置,并诚实记录每一次牺牲。
浏览 0
点赞 0
评论 0