Liu et al.「Lost in the Middle」研究证实 LLM 对输入窗口不同位置的信息利用率呈 U 型曲线,开头和结尾最可靠,中间最容易被忽略。提示词设计应将关键信息放首尾,干扰项放中间。
两个完全相同的词以不同顺序组成的请求,实际上是两个不同的请求,有着不同的成本和不同的答案。这一点令人不适,但这就是整篇文章所建构的基础。
Same tokens, different request
顺序不是一种展示层面的选择。它同时改变三件独立的事情,而且值得把它们分开来讨论,因为它们指向截然相反的设计方向。
模型实际使用的部分。长文本中,材料所处的位置会影响它被使用的可靠性。这正是已发表论文所测量的效果。
可以被缓存的部分。Prompt 缓存作用于前缀。前一个请求中第一个出现差异的字节会结束可复用区域,因此任何早期放置的易变内容都会破坏其后面所有内容的可缓存性。
在截断中存活下来的部分。当输入过长时,你的分配器或提供商从一端丢弃内容。顺序决定了哪一块内容会处于那个被丢弃的端。
重量级的引用是 Liu 等人的《Lost in the Middle: How Language Models Use Long Contexts》(TACL 2024)。实验将包含答案的文档插入到干扰项中的不同位置,然后测量准确率随位置变化的函数。报告的曲线呈 U 形:当相关材料位于输入的最开头或最末尾时性能最高,位于中间时性能最低。
有两个注意事项是这一发现本身的组成部分,但在复述时通常被省略了。这一效应是在作者测试的模型、他们测试的长度、以及检索风格任务上报告的——它不是自然法则,后来的模型不能保证以相同的形态复现它。而且它研究的是单一输入,不是对话:论文中没有任何内容涉及第四十轮对话。请把它当作一种先验知识来指导排序策略,而不是一个具体数字。
实际的翻译很短。不要把你需要可靠使用的任何内容放在长输入的中间。中间是填充物待的地方。
以下是通常的建议所回避的张力。以下两者都是正确的,但它们互不兼容:
relevance order : most relevant material near the edges,
ideally adjacent to the question at the end
cache order : least volatile material first, so the longest
possible prefix is byte-identical across requests
Retrieved documents are HIGHLY relevant and HIGHLY volatile.
Relevance wants them last. The cache does not care where they go,
but anything placed BEFORE them cannot be cached if they move.
解决方案不是妥协,而是分层。先按易变性排序以定义可缓存前缀,然后在易变后缀中应用相关性排序。这样静态块形成稳定的前缀,永远不会移动,而真正影响质量排序决策都是在那些反正也无法缓存的块上做出的。前缀的经济性是单独计算的;在这里它只是排序函数必须尊重的一个约束。
一个让很多人意外的结果是:对话历史通常应该放在检索文档之前,而不是之后。历史是追加写入的,因此它在每次请求间的部分是稳定的;检索结果在每次请求中都会完全改变。把检索放在历史之前意味着每一轮都会使整个历史块的缓存失效。把历史放在前面可以保持这个增长但稳定部分的可缓存性,把变化剧烈的部分放在末尾——而这也正是相关性排序希望好材料所处的位置。
type Rank = { volatility: 0 | 1 | 2 | 3; relevance: number };
// volatility: 0 = never changes, 1 = changes rarely, 2 = per session,
// 3 = per request. Ascending order defines the prefix.
const RANK: Record<string, number> = {
system: 0, // persona, policy, output contract
tools: 0, // schemas, serialised with STABLE key order
reference: 1, // static corpus carried on every request
record: 2, // compacted session state
history: 2, // append-only turns
retrieved: 3, // changes every request
toolresult:3,
question: 3, // the current turn, always last
};
function order(blocks: Block[]) {
return blocks.sort((a, b) => {
if (RANK[a.kind] !== RANK[b.kind]) return RANK[a.kind] - RANK[b.kind];
// Within the volatile tail, relevance ascending: weakest material in
// the middle of the tail, strongest adjacent to the question.
return a.relevance - b.relevance;
});
}
// Retrieved documents, ordered within their own block:
// [3rd best, 5th, 6th, 4th, 2nd, BEST] <- best nearest the question
function orderDocs(docs: Doc[]) {
const s = [...docs].sort((a, b) => b.score - a.score);
const out: Doc[] = [];
s.forEach((d, i) => (i % 2 === 0 ? out.push(d) : out.unshift(d)));
return out; // strongest at the ends, weakest mid
}
orderDocs 是把"lost in the middle"的研究结果转化为四行代码:它通过交叉排列,使得分最高的文档落在块的的两端,得分最低的坐在中间——这恰恰是论文中报告的使用可靠性最低的区域。如果你只从排序文献中选取一条编码实践,就选这个。
注意这个函数里没有涉及什么:对话轮次的重排。在对话中,时间顺序就是语义,颠换它会产生不连贯而不是更好的检索。排序策略应用于块之间和检索集合内部,而不是转录本内部。
只有当模型能够区分一个块在哪里结束、下一个块从哪里开始时,排序才有帮助。两个没有分隔符拼接在一起的文档是一个中间部分令人困惑的文档,一段紧贴着用户问题的检索片段可能看起来像是问题的一部分。每个块都需要一个明确的边界,至少携带其种类信息,对于检索材料还要携带其来源——编码选择是一个独立的决策,有其自身的 token 成本。
检索块上的来源信息还做了件值得一说的事:它使引用变得可检验,它也让你能在会话出问题时回答"模型到底有没有看到这条?"一个携带 id 的块可以在上下文转储中被搜索到;一段匿名的文本墙则无法做到。
最后一条不花任何成本的排序规则:把实际问题放在最后,永远。它是唯一一个位置不存在权衡的块——它既高度相关又高度易变,在两种排序方式下都属于末尾。一个令人意外的事实是,相当多的组装器把问题放在检索材料之前,因为那是用户体验中的顺序——这就把模型必须满足的请求放在了最不利的位置。
关于在这里投入多少精力的最后提醒。排序效应是真实存在的,但与正确处理 eligibility 和 allocation 的效果相比,它们也很小。一个完美排序但缺少相关文档的上下文,差过一个包含文档但排序糟糕的上下文,而这两种失败看起来是一样的。如果这个系列中有另一页值得你花一个下午,那应该是决定什么内容能进入窗口的过滤器。一旦内容正确了,再做排序,而且只做一次。
Context Caching Strategy: Ordering for Cache Hits
Instruction Placement: Top, Bottom or Both?
Context Rot: Why Long Sessions Get Worse