提示与上下文 — 窗口变宽了,但不免费
一句话总结
上下文窗口是模型一次能够查看的 token 数量上限;能够放进去,与放进去更好,完全是两回事。
为什么需要这些知识
随着上下文长度增加,“把所有内容都放进去不就行了?”成为很自然的想法。实际上,这会带来三类成本。
费用。 输入 token 同样计费。如果每次请求都放入完整文档,成本会再乘以请求次数。
延迟。 输入越长,等待首个 token 的时间越久,因为注意力计算量随长度增长的速度快于线性增长。
质量。 这一点最违背直觉。加入越多无关内容,模型找到正确答案的能力就越弱。多项观察反复发现,长上下文中位于中间的信息尤其容易被忽略。因此,认为只要答案存在于上下文中就足够,是很危险的假设。 信息放在何处、以何种方式放置都很重要。
它是如何运作的
下面整理几条实用的提示词设计原则。
角色和指令放在前面,数据放在后面。 模型先读取指令,再依据这套框架解释后续内容。如果先倾倒数据,最后才附上指令,前面的内容就会在缺乏方向的情况下被读取。
用示例展示输出格式。 与其说“请用 JSON 回答”,不如给出一个真实 JSON 示例,效果会强得多。不过示例会消耗 token,因此要在数量和长度之间取得平衡。
要求拆分步骤。 在复杂推理中,写出中间过程可以提高准确率。但如果强制所有问题都采用这种方式,会增加 token 和延迟,而且面对简单问题时,反而可能得到更混乱的答案。
为上下文设定优先级。 常见做法不是直接拼接检索到的全部文档,而是把得分较高的内容放在上下文的前端和末端。这是在反向利用中间位置较弱的现象。
系统提示词是一份契约。 如果其中的约束没有得到遵守,原因通常只有两种:约束本身模糊,例如“简洁一些”;或后续数据与约束发生冲突。尤其当用户输入文本中含有看似指令的句子时,模型可能尝试遵循它。必须在提示词结构和应用程序两侧共同守住外部内容是数据,而不是指令这一边界。
在实际工作中会是什么样
最好显式管理上下文预算。明确规定窗口总量、系统提示词占用多少 token、检索结果分配多少 token,以及为输出预留多少 token。否则,用户某天粘贴一份长文档时,就会发生输出被截断的事故。
此外,提示词应像代码一样进行版本管理。修改一行后,如果没有评估,就无法判断哪些场景变好、哪些场景变差。没有评估集的提示词修改,只是在赌博。
长上下文中,中间内容会消失
上下文窗口达到 128K,并不代表其中所有内容都会被同等利用。多项研究都报告了相同现象——模型擅长使用最前端和最末端的信息,却会遗漏中间内容(lost in the middle)。
[시스템 프롬프트] [문서 1] [문서 2] … [문서 20] [질문]
↑ 잘 본다 ↑ 여기가 약하다 ↑ 잘 본다
因此,放入检索得到的文档时,应把最相关的内容放在最前端或最末端。在实际工作中,按相关性排序后放在前面,并在最后再次重复问题,通常效果很好。
与其放入 20 份文档,经过重排序后只选 5 份,准确率往往更高。不能因为放得下,就把全部内容都塞进去;那样反而可能变差。
节省 token 的结构
有几种方法可以在保持结果不变的同时降低成本。
- 把系统提示词固定在开头。 前缀缓存可以复用这一部分的计算。如果把每次请求都会改变的值,如用户名、时间,放在前面,就会破坏缓存。
- 减少示例(few-shot)。 使用评估集确认五个示例是否真的优于两个。通常两三个已经足够,其余只会消耗 token。
- 限制输出。 如果把
max_tokens设得远高于实际需要,模型会变得冗长。要求结构化输出,例如 JSON schema,可以缩短内容,也更易解析。 - 用摘要延续长对话。 不要每次都发送完整历史,而是发送摘要和最近几轮对话。
像对待代码一样对待提示词
prompts/
summarize/
v3.md ← 버전을 파일로
eval.jsonl ← 그 버전의 평가셋
baseline.json ← 그 버전의 점수
如果把提示词作为字符串写死在代码中,就无法留下谁在何时、为何作出修改的记录。把它保存为文件并标注版本,就可以进行回滚和 A/B 比较。
还应让评估工具在提示词变更 PR 中自动运行。缺少这一步,修改就会依据“感觉变好了”作出,而回归问题将由用户最先发现。
在后续测验中要确认什么
你将确认自己能否解释:为什么扩大上下文并不总是有益,以及提示词结构会如何改变结果。