"中段盲区"(Lost in the Middle)问题是指这样一种行为:LLM 对长输入开头和结尾的信息给予强烈关注,而对中间的信息关注极少。
在这篇博客中,我们将学习 LLM 的"中段盲区"问题——这种奇怪的行为表现为:模型阅读一篇很长的文本时,能很好地利用开头和结尾,却悄悄忽略掉位于中间的内容。我们还会看到:上下文窗口到底是什么、准确率为何呈 U 形曲线、中间部分为何被遗忘、这个问题如何悄无声息地破坏 RAG 系统和长对话、如何测试自己的模型是否存在此问题,以及如何解决它。
我们将涵盖以下内容:
- 什么是上下文窗口?
- 什么是"中段盲区"问题?
- 通过一个例子来理解
- U 形曲线
- 为什么会发生
- 它在真实场景中如何伤害我们
- 如何测试"中段盲区"问题
- 如何解决"中段盲区"问题
- 需要记住的要点
什么是上下文窗口?
在深入了解"中段盲区"问题之前,我们必须先知道什么是上下文窗口。
LLM(Large Language Model,大语言模型)是一种阅读文本并预测下一个词的模型。但它无法一次性阅读无限的文本,存在一个上限。
上下文窗口是模型一次所能查看的文本的最大数量。
让我们拆解这个词本身:
上下文窗口(Context Window)= 上下文(Context)+ 窗口(Window)
上下文指我们交给模型的全部文本;窗口指允许它向外看的那个固定的开口。所以,上下文窗口就是模型观察我们所给内容的那个固定开口。
简单来说,上下文窗口就是模型的工作台。放在台面上的东西,模型能看到;放在台面之外的东西,模型完全看不到。
我们放在台面上的东西称为提示词(prompt)。提示词就是我们在一次请求中发送给模型的全部内容,包括我们的指令、我们提供的文档,以及我们的实际问题。
假设一个模型的上下文窗口是 100,000 个 token。token 是一小块文本,大约是 0.75 个词。那么这个模型一次可以查看大约 75,000 个词。
现在,容易产生误解的地方来了。我们大多数人都以为:只要文本能装进上下文窗口,模型就能同等良好地阅读全部内容。
这个假设是错的。而这正是"中段盲区"问题。
什么是"中段盲区"问题?
"中段盲区"问题是指这样一种行为:LLM 对长输入开头和结尾处放置的信息给予强烈关注,而对中间放置的信息关注极少。
简单来说:模型读了开头,读了结尾,而中间部分正好落进了它的盲区。
来做个现实世界的类比。回想一场开了三个小时的会议。第二天,我们清楚地记得会议是怎么开始的,也清楚地记得最后做出的决定。但第 90 分钟发生的讨论几乎从记忆中消失了。我们全程都在场,每个字都听到了,中间部分还是淡忘了。
LLM 处理长提示词时做的正是同样的事情。
这一行为已被研究者系统研究过:他们把同一个答案不断移动到长输入中的不同位置,在许多模型上都一次次发现了同样的弱点。"Lost in the Middle"(中段盲区)这个名字就此流传下来,因为它准确描述了所发生的一切。
这里需要注意的是:信息是存在的。我们确实把它给了模型,它就好好地待在上下文窗口里。模型只是没能用上它。
非常重要:这与文本被截断不是一回事。当文本超出上下文窗口容量时,会有一部分内容在到达模型之前就被移除——那是一个完全不同的问题。而在"中段盲区"问题中,没有任何内容被移除,一切都到达了模型,模型仍然表现得好像中间部分从未存在过。
这正是此问题危险的地方:没有报错,没有警告。模型自信满满地给出错误或不完整的答案,而我们根本不知道正确答案其实早已摆在它面前。
通过一个例子来理解
学习这个问题最好的方式是举个例子。
假设我们正在构建一个回答公司相关问题的应用。我们有 20 份关于公司的文档,把全部 20 份都放进提示词,然后提问:
以下是关于我们公司的 20 份文档。
文档 1:...
文档 2:...
...
文档 20:...
问题:我们的退款政策是什么?
假设退款政策写在文档 2 里。模型较早读到文档 2,因为它靠近顶部,于是回答正确。
现在,把同一份文档挪个位置。假设退款政策现在写在文档 10 里——正好位于整堆文档的中间。其他一切都不变:同样的 20 份文档、同样的问题、同样的模型。
这一次,模型回复类似:"我在给定文档中找不到退款政策。"
唯一改变的是位置。内容一模一样,只是"座位号"变了。
再挪一次,这次放进文档 19,靠近末尾。模型又能正确回答了。
同样的信息、同样的模型、同样的问题——三个不同的位置,两次正确回答,加中间一次无声的失败。
这就是"中段盲区"问题。
U 形曲线
现在,我们用一种更可度量的方式来理解这个行为。
如果我们取一个小事实,把它从第一个位置不断移动到最后一个位置,并记录每个位置的准确率,会得到一个非常清晰的形状:
- 事实位于开头时,准确率高
- 事实移向中间时,准确率持续下降
- 事实到达结尾时,准确率再次升高
如果画在一张图上(一边是位置,另一边是准确率),这条线先下降再回升,看起来就像字母 U。
图示如下:
准确率
高 | * *
| * *
| * *
| * * *
低 |________________________________________
开端 中间 末尾
答案所在的位置
这被称为 U 形性能曲线。
两种广为人知的人类行为可以很好地解释它:
- 首因偏差(Primacy bias):我们对最先出现的事物记得最牢
- 近因偏差(Recency bias):我们对最后出现的事物记得最牢
模型的表现与此相同:对开头的东西强,对结尾的东西强,对中间的东西弱。
注意:输入越长,中间的凹陷越深。短提示词下我们可能什么都察觉不到;而在很长的提示词下,中间几乎消失。
为什么会发生
下一个大问题是:一个技术上能看到全部文本的模型,为什么仍然忽略中间?
原因不是单一的,有四个原因在共同起作用。别担心,我们逐一详细学习。
原因一:注意力被摊得极薄
LLM 使用一种叫**注意力(attention)**的机制。简单说,注意力决定了生成答案时每个词应该赋予其他每个词多少重要性。
注意力通过分配一份固定的重要性预算来工作。假设模型有一整份重要性可以分出去:如果只有 10 个其他词,每个词都能分到可观的一份;如果有 100,000 个其他词,每个词就只能分到极小的一份。
打个简单的比方:想想黑暗房间里的手电筒。房间小的时候,光强烈地照到每个角落;房间非常大时,同一支手电筒要覆盖大得多的面积,于是所有东西都变暗了。注意力的行为完全一样。
所以,随着输入增长,分配给任何单个 token 的重要性都变得极小。最"响亮"的 token 仍然胜出,而安静待在中间的那些则被压垮。
原因二:位置信息的处理方式
模型天然并不知道词的先后顺序,我们必须告诉它。做法是使用位置编码(positional encoding)——给每个 token 打上关于它所处位置的信息标签。
许多现代模型采用的方案是:距离远的 token 之间的连接弱于距离近的 token。这在总体上非常有用,因为相邻的词通常关联更紧密。
但这带来了一个副作用。输入的最后一段紧挨着模型生成答案的位置,所以它保持强势。开头部分通常承载指令,而模型被反反复复训练要尊重指令,所以它也保持强势。中间既远离答案,又没有任何东西锚定它,于是成了最弱的区域。最开头的那些 token 在此之上还多了一层引力,因为模型把它们当作注意力沉淀池(attention sinks)——一个倾倒它在别处用不到的注意力的垃圾场。
原因三:人类的写作习惯教会了模型这个习惯
模型从人类文本中学习,而人类文本有非常强的结构。
想想一篇文章、一篇论文、一封邮件或一份报告:重要的东西通常在开头作为引言陈述,在结尾作为结论重复,中间承载支撑细节。
模型从数百万份文档中学到了这个模式,于是慢慢养成了一种习惯——比起中间更信任开头和结尾。它做的正是我们教它做的事。
原因四:训练很少用到全部长度
一个模型可能支持 128,000 token 的上下文窗口,但它学习所用的大部分样本都比这短得多。长窗口通常是在训练的后期阶段才加上去的——在那个阶段,模型才被教导去处理比它最初见过的输入大得多的输入。
于是,模型对短输入有大量练习,而对"答案深埋其中"的真正长输入练习极少。它支持长窗口,但它在整个窗口上的功力并不均衡。
这就像一个多年来只学 5 页一章的课本的学生,突然拿到一本 500 页的书。学生能翻到每一页,但注意力不会均匀地铺满整本书。
这四个原因共同造成了中间的下陷。
它在真实场景中如何伤害我们
到目前为止,我们学习了问题是什么、为什么发生。现在来看看它究竟在哪些地方咬到我们。举几个真实用例。
RAG 系统
RAG(Retrieval Augmented Generation,检索增强生成)是非常常见的架构。我们把所有文档切成称为**块(chunk)**的小片段并存储起来。问题到来时,**检索器(retriever)**在这些块中搜索,挑出与问题看起来最相关的那些,放进提示词,然后让模型基于它们作答。
但问题来了。我们大多数人往提示词里塞 20 或 50 个块,以为上下文越多答案越好。位于中间的块被忽略了。于是:我们检索到了正确答案,把它放进了提示词,模型却仍然失败。
检索是完美的,生成失败了。而当我们排查时,通常会怪罪检索器——可它从一开始就不是问题所在。
注意:如果检索器把正确的块排在 30 个中的第 12 位,从技术上讲我们确实"检索到了"它。但第 12 位深陷提示词的中间,于是模型表现得好像我们从未检索到它一样。
长对话历史
在长对话中,最早的消息和最新的消息保持强势,而我们在对话中间某处给出的指令会悄悄失去效力。
这就是为什么聊天机器人常常遵守某条规则一阵子,然后开始违反它。规则并没有被删除,它只是移进了弱势的中间区域。
这也是长对话会随着变长而被压缩的原因——对话中较旧的部分需要被摘要,但要不丢要点。
长文档分析
当我们给出一份 200 页的合同或一个巨大的日志文件,让模型找出所有问题时,它能可靠地抓住开头和结尾附近的问题,而位于中间章节的问题会被漏掉。
这非常危险,因为输出看起来是完整的。模型从不说"我跳过了中间",它只是自信地给出一个部分答案。
多文档比较
当我们让模型比较 10 份报告时,排在中间的报告得到的处理非常浅。摘要看起来很均衡,实际上悄悄偏向了第一份和最后一份报告。
AI 智能体(Agent)
AI 智能体一步一步工作:它思考、调用工具、读取结果,然后循环往复。每一步都会把自己的输出加进上下文。
30 步之后,最开头给的目标仍然安全,最近的步骤也安全,但智能体在中间发现的一切开始消退。这就是长时间运行的智能体会慢慢偏离我们最初要求的原因。
如何测试"中段盲区"问题
以上是这个问题伤害我们的地方。现在来学习如何在自有系统中抓住它。
在修复之前,我们必须先能度量它。有一个非常简单且流行的测试,叫大海捞针测试(Needle in a Haystack test)。
它的工作原理如下:
- 第 1 步:取一段与我们的问题毫无关系的超长文本。这就是"草垛"(haystack),可以是任何冗长的填充内容。
- 第 2 步:写一个简短的独特事实。这就是"针"(needle)。为了便于理解,假设是:"孟买办公室的暗号是 BLUE-4471。"
- 第 3 步:把针插入草垛中选定的深度,比如 10% 深度处。
- 第 4 步:向模型提问一个只有针里才有答案的问题,例如:"孟买办公室的暗号是什么?"
- 第 5 步:记录模型是否找到了它。
之后,我们把针分别放在 0%、10%、20%……直到 100% 的深度,重复整个过程;还要在不同的输入长度下重复。
把所有结果放进一张网格图后,我们会得到一幅非常清晰的画面:能确切看到模型在何处变弱,以及输入能变多长之后中间开始崩塌。
注意:我们必须用自己的模型、自己的提示词格式、自己那类内容来跑这个测试。每个模型的弱势区都不同,而且随每个新版本而变化。
如何解决"中段盲区"问题
既然我们已经认识了问题、知道了如何度量,现在该学习如何解决了。
我们将逐一过这些方法,从最朴素的那种开始。
方法一:把所有东西都塞进上下文
这是我们大多数人最先做的。模型支持巨大的上下文窗口,于是我们把一切塞进去,指望模型自己搞定。
这个方法的问题在于:大的上下文窗口是一个容量数字,不是质量保证。中间照样落入盲区,而且我们还要为每一个 token 付费、等更久才收到回复。看看下一个方法如何解决。
方法二:少发,但发对的东西
接下来登场的是最简单也最有效的修复。与其发 50 个块,不如发 5 个好块。
短而精准的上下文只有一个很小的"中间",几乎没有什么可丢失的。模型能把全部内容都读好。
要做好这一点,我们用**重排序器(reranker)**来改进检索环节。重排序器是一个较小的模型,它接收检索到的块,为每块实际回答我们问题的程度打分。只保留前几名,其余全部丢弃。
这个方法的问题是:有时我们确实需要很多块,因为答案分散在多份文档里。看看下一个方法如何解决。
方法三:聪明地排列块的顺序
如果必须发送很多块,那就必须非常仔细地安排它们的"座次"。这是所有修复中最便宜的:我们不删除任何东西,只是改变顺序。
我们已经知道曲线的形状:开头强、结尾强、中间弱。于是,我们把最相关的块放在最开头,第二相关的放在最末尾,第三相关的紧随第一个之后,第四相关的放在最后一个之前,如此不断向内折叠。
最弱的块落进中间——那正是我们承受得起损失的位置。
图示如下:
按相关性排序的块: 1 2 3 4 5 6 7
最终排列顺序: 1 3 5 7 6 4 2
^ ^
最强的位置 最强的位置
这个简单的重排不花一分钱,却带来真实的提升。
注意:我们还必须把实际问题放在提示词的最末尾、紧贴答案开始的地方。那是整个提示词中最强的位置。
还有一点值得注意:如果有某个关键指令,比如"只能根据给定文档回答",那必须写两遍——一遍放在最开头,一遍再放在最末尾。重复它只花几个 token,却能确保这条指令永远不会落入弱势的中间。
决定什么进提示词、按什么顺序进,本身就是一门手艺(即上下文工程,Context Engineering)。
这个方法的问题是:它只能减损,不能除损。如果答案需要的那一块恰好落在中间,我们仍会失败。看看下一个方法如何解决。
方法四:把大任务拆成小任务
与其在一个巨型提示词上问一个问题,我们把输入拆成小块,逐块处理。
对每个块,单独向模型提同一个问题,从每块收集一个小答案。之后,把这些如今已经很短的小答案放在一起,让模型把它们合并成最终答案。
在这里,每个块都有机会轮流坐到一个短提示词的开头和结尾。没有弱势的中间,因为根本没有长输入。
这常被称为map-reduce 式处理。map 指我们对每一小块执行同一个小任务;reduce 指把所有这些小结果合成一个最终结果。
便于理解起见:假设我们有 30 份文档。不做一次携带 30 份文档的调用,而是做 30 次每次携带一份文档的小调用,再做最后一次携带 30 个短答案的调用。最后一次调用非常小,所以不会有任何东西被埋进去。
这个想法还有一个更聪明的版本(即递归语言模型,RLM):由模型自己决定如何切分输入、对每一块问什么。
优点:没有任何内容被忽略,因为每一块都在短上下文里被读到。
缺点:要发起很多次模型调用,所以成本更高、耗时更长。
方法五:逼模型先看清再回答
有时只需改变指令,就能修复很多问题。
不要让模型直接回答,而是先让它通读文档、把相关的行连同文档编号一起抄出来。之后,再让它仅基于抄出的那些行作答。
之所以有效,是因为抄写远比推理容易。模型扫描并抽取,抽取出的行随后构成一个又短又新的上下文——其中没有任何被埋没的东西。
指令可以这样写:
首先,列出与问题相关的文档编号和确切句子。
先不要回答问题。
然后,仅使用你上面列出的句子,写出最终答案。
在这里,我们把一个困难的任务拆成了两个容易的任务,这是提示词链(prompt chaining)的简单形式。第一个任务把被埋没的信息拉到表面,第二个任务在干净而短的输入上工作。
方法六:谨慎选择模型
每个模型的弱势区都不同。有些模型在 30,000 token 以内能很好地稳住中间,超过之后崩塌;有些模型能撑得久得多。
所以,我们绝不能只看宣传的上下文窗口大小来挑模型。必须在我们实际使用的长度上、用我们实际拥有的那类内容,跑大海捞针测试来挑选。
一个宣传百万 token 窗口的模型,并没有承诺它对百万 token 理解得同样好。它只承诺会照单全收。
用这些简单的方法,我们就能解决这个有趣的问题,日子也好过起来——因为我们不再与模型对抗,而是顺着它真实的阅读方式来做事。
需要记住的要点
为便于理解,总结一下重点:
- 上下文窗口告诉我们模型会接受多少文本,而不是它对每一部分理解得多好
- 准确率在开头高、中间下降、结尾回升,形成 U 形曲线
- 输入越长,问题越严重
- 其成因是:注意力被摊薄、位置处理偏向邻近 token 与开头 token、人类写作习惯训练模型更信任开头与结尾
- 它无声地失败——我们得到的是自信的错误答案,而不是报错信息
- 最好的修复是发送更少且更好的上下文
- 如果必须发送很多,就要把最重要的内容放在开头和结尾
- 把工作拆成小块,能彻底消除弱势的中间
- 这不只是 RAG 的问题,它以同样的方式打击长对话和长时间运行的 AI 智能体
- 必须用大海捞针测试来检验自己的模型,而不是轻信宣传数字
现在,我们已经理解了 LLM 的"中段盲区"问题。
下一次,当我们的系统给出错误答案、而正确信息明明就在提示词里时,不要断定是模型没理解。先检查那条信息当时坐在哪里。
大多数时候,答案早就在那儿了——它只是坐在了中间。
常见问题(FAQ)
"中段盲区"问题和文本被截断是一回事吗?
不是。当文本超出上下文窗口时,会有一部分内容在到达模型之前就被移除。而在"中段盲区"问题中,没有任何内容被移除,一切都到达了模型,模型仍然表现得好像中间从未存在过。
更大的上下文窗口能解决"中段盲区"问题吗?
不能。大的上下文窗口是容量数字,不是质量保证。它告诉我们模型会接受多少文本,而不是它对每一部分理解得多好。中间照样落入盲区,而且我们还要为每个 token 付费、等更久才收到回复。
在长提示词中,问题应该放在哪里?
应该把问题放在提示词的最末尾、紧贴答案开始的地方——那是整个提示词中最强的位置。如果有某条关键指令,应写两遍:一遍放在最开头,一遍再放在最末尾。
为什么聊天机器人遵守某条规则一阵子后就开始违反?
因为那条规则移进了对话的弱势中间区域。在长对话中,最早的消息和最新的消息保持强势,而在中间某处给出的指令会悄悄失去效力。规则并没有被删除。
"中段盲区"问题只是 RAG 的问题吗?
不是。它同样打击长对话、长文档分析、多文档比较和长时间运行的 AI 智能体。走了很多步之后,智能体仍记得最初的目标和最近的一步,但它在中间发现的东西开始消退。