LLM 对上下文头尾记忆强、中间弱,top_k 从 3 升至 15 时准确率反而下降,是可复现的模型固有失败模式。
你搭了一个 RAG 流程。用 3 篇检索文档测试,正确答案就在第 2 篇里——模型每次都能准确命中。你信心满满,把 top_k 从 3 调到 15,觉得"保险起见多拿点上下文"。
准确率反而下降了。不是崩溃,不是超时,日志里也没有任何错误——模型只是开始自信满满地给出错误答案,或者干脆漏掉那些明明就白纸黑字写在你发送的 prompt 里的事实。你手工把上下文窗口从头到尾读了一遍,答案就在里面。模型看到了。只是没用上。
如果你的系统也这样,问题不在于你搭的检索器不行。你遇到的是一个被充分文档化、不挑模型的失败模式:LLM 对上下文窗口中不同位置的信息,回溯能力并不均匀。它们对开头和结尾记得牢,中间那段,信息进去就石沉大海。
这不是道听途说——而是直接被测量过的。Liu 等人的"Lost in the Middle"研究(2023 年)在受控环境下对不同上下文长度进行了检索测试,发现了始终一致的 U 形准确率曲线:当相关事实位于上下文的最开头或最末尾时,性能最高;埋在中间时性能下降——有时甚至掉到接近随机猜测的区域。这在多个模型家族中都成立,不是某家厂商的个例。
这个机制是架构层面的,不是一个能打补丁修复的 bug。自注意力不会在 token 位置上均匀分配回溯能力。模型在训练过程中大量接触短距离依赖(下一个词通常依赖附近的词),而对从长而不结构化的段落中间精确检索这个行为,得到的训练信号相对较少。位置编码进一步加剧了这个问题——大多数方案都会让注意力偏向强烈的局部近因偏向,以及偏向序列开头(一个天然的锚点),导致中间部分在结构上处于低注意力状态。
在生产环境中真正坑人的是这一条:对外宣称的上下文窗口和实际可用的上下文窗口是两码事,厂商给出的都是前者。拥有"100 万 token 上下文"的模型,并不意味着它能在 100 万 token 范围内可靠检索——只意味着它在 100 万 token 之前不会报错。这两个根本不是一回事,而它们之间的差距,正是"演示环境好好的,生产环境就开始 flaky"的 bug 产生的温床。
而且它天生静默,无声无息。没有什么异常可以捕获。检索器完成了本职工作,prompt 组装完成了本职工作,token 明确存在于上下文中——失败只发生在模型的注意力选择上,除非你专门去测,否则从外部根本看不出问题。
别再把 top_k 当成安全阀用了。更多检索出的 chunk 不会单调提升召回率——超过某个点后,它反而会把那个唯一正确的 chunk 往死区更深处推。把 top_k 往低调,而不是往高调,并且做好测量。
重排序,而不只是检索。排名完成后,把置信度最高的 chunk 放在上下文的头部和尾部,而不是按排名顺序从上往下塞。一篇被检索器排第 1 名的 chunk 如果被放在 prompt 正中央,效果反而不如排在第 4 名但放在边缘的 chunk。
加上重排序阶段。在初次检索后加一个廉价的 cross-encoder 重排步骤,只把前几名以位置感知的方式送入 prompt,效果始终优于"宽泛检索、全塞进去、让模型自己整理"的做法。
搭一套自己的大海捞针评估。别拿厂商的长上下文基准测试当回事去跑你的工作负载。位置敏感度因模型而异、因模型版本而异(一次静默升级就可能导致回退)、也因你具体文档的结构而异。从日志里拿一条真实查询,把已知正确的 fact 分别放在典型上下文长度的 10%、50% 和 90% 位置,然后测量每个位置上的准确率。每次换模型都重新跑一遍。
考虑先收窄再考虑拓宽。多步检索循环、逐步过滤到一个小而高置信度的上下文,往往优于一次性塞满"所有可能相关的东西"的单个巨大上下文。少而精、位置好的 token,几乎在任何时候都胜过多而散的 token。
上下文窗口大小和有效检索范围是两码事——不要混为一谈。
"Lost in the Middle"效应是架构层面的,在多个模型家族中都被测量验证过,不是偶发 bug。
这种失败模式是静默的:没有报错,只有那些技术上存在于上下文中的事实,悄悄地给出了错误答案。
修复方法:缩小 top_k、重排序加位置感知重排、搭建自己的位置评估、优先选择收窄检索而不是拓宽检索。