• 欢迎关注我的微信公众号“ Falost ” 右边扫描关注 --->>

Lost in the Middle:为什么上下文越长,模型越容易“迷路”?我的三层解法(位置管理+检索精减+路由分流)

AI / 大模型 神棍 260℃ 0评论

最近在写上下文工程这个系列,前两篇我把两本账算完了:KV Cache 那篇讲的是显存账——长上下文的显存到底烧在哪;提示缓存 + 上下文压缩那篇讲的是钱的账——让同一段前缀只算一次钱。今天算第三笔,也是最容易被忽略的一笔:准确率的账。

起因是有读者在后台问我:”我把公司 50 页资料全塞给模型,为什么它反而答得乱七八糟?”这个问题我太有共鸣了——我一开始也是这么干的:既然窗口够大,那就全塞进去呗。结果模型经常抓不住重点,像个在图书馆里迷路的人,书都在架子上,就是找不到答案在哪一本。

后来我才搞清楚,这不是模型笨,是我踩中了一个著名陷阱:Lost in the Middle。这篇我用大白话拆一下这个现象,再给出现在我实际在用的三层解法。读完你会明白:模型读长上下文不是”平均用力”的,关键信息放在哪,直接决定它答不答得对。

一、现象:模型用长上下文,是一条 U 型曲线

先上结论:答案放在上下文开头或结尾,模型找得到;放在中间,性能明显下滑——哪怕是对外宣称支持超长上下文的模型也一样。

这个结论来自斯坦福团队的论文 Lost in the Middle(Liu et al.,TACL 2023),实验任务是多文档问答和键值检索。他们发现,模型对上下文开头(首因偏差)和结尾(近因偏差)的信息利用最好,中间则明显变差,性能曲线呈 U 型。具体数字更扎心:把答案从开头挪到中间,GPT-3.5-Turbo 在多文档问答上的准确率能掉 20% 以上;干扰文档加到 20、30 个时,最差情况甚至比”一个文档都不给”还低——闭卷准确率是 56.1%。

论文里还有一组对照很说明问题:GPT-3.5-Turbo 闭卷 56.1%,把答案固定放在开头能到 88.3%;Claude-1.3 是 48.3% 对 76.1%。这说明模型不是没能力答对,它只是”找不到”。所以我的第一个教训是:别指望模型通读全文,它更像一个只认真读前言和后记的读者。

想自己复现也不难:拿任意 OpenAI 兼容接口(比如 DeepSeek,官方文档在文末),把含答案的文档分别插在开头、中间、结尾,同一批问题各问一遍,对比正确率,你也能画出那条 U 型曲线。

二、我的三层解法

总原则一句话:别想着让模型看完所有内容,而是让它只看该看的,并且把该看的放在它看得见的位置。

第 1 层:位置管理,把关键内容钉在开头和结尾

既然模型有首因和近因偏好,我就顺着它来:最重要的指令和文档放最前面(紧跟 system prompt),第二重要的放到最后收尾强调,其余才放中间。我写了个小工具 smart_order:检索结果按相关度排序后不直接顺排,而是把第 1 名钉在开头、第 2 名挪到结尾,避免关键文档被埋进中间地带(这段代码我本地跑过):

def smart_order(docs, k=5):
    """docs: [(score, title)],按分数降序传入;第1名放开头,第2名挪到结尾"""
    docs = sorted(docs, key=lambda x: -x[0])[:k]
    if len(docs) < 2:
        return list(docs)
    return [docs[0]] + docs[2:] + [docs[1]]

跑出来的效果:原始排序 [退市新规全文 0.91, 退市标准细则 0.87, 年报格式 0.55, ST公司梳理 0.42, 上市规则 0.31] 经过 smart_order 变成 [退市新规全文, 年报格式, ST公司梳理, 上市规则, 退市标准细则]——0.87 那篇被特意挪到了结尾的近因位。别小看这个调整,论文里 20% 的准确率差距,就是开头、中间、结尾这几个位置造成的。

第 2 层:检索精减,宁可少给

论文里 retriever-reader 的实验还发现:塞更多"不含答案"的文档会拖低准确率。也就是说,上下文越长越"全",模型反而越容易在无关内容里走丢。我的做法是:向量召回先拉 20 条候选,再用 cross-encoder 重排,最后只留 3~5 条进上下文。重排我用的是 FlagEmbedding 里的 bge-reranker-v2-m3,官方示例用法是:

from FlagEmbedding import FlagReranker
reranker = FlagReranker("BAAI/bge-reranker-v2-m3", use_fp16=True)
scores = reranker.compute_score([[query, doc] for doc in candidates])

召回 20 条是为了召回率,重排后只留 5 条是为了不迷路,两步都不能省。Anthropic 在讲 context engineering 时也是类似思路:按需动态检索(just-in-time),而不是把整库预塞进上下文。

第 3 层:路由分流,能不问"长"的就不问

最后一层最简单也最容易被忽略:不是所有请求都配用长上下文。闲聊、常识问答硬塞 50 页资料,又贵又容易答歪。我在服务前面加了一个轻量路由,先判断请求属于哪一类:

def route(need_external_doc=False, need_long_synthesis=False):
    if not need_external_doc and not need_long_synthesis:
        return "short-direct"   # 常识/闲聊:短上下文
    if need_long_synthesis:
        return "long-context"   # 多文档综合/全文总结:才上长上下文
    return "rag-topk"           # 需要外部资料:检索+重排+位置拼装

"把这 50 页招股书总结成要点"才走 long-context,"今年退市新规有哪些变化"走 rag-topk,"你好"直接 short-direct。判断逻辑可以是简单规则,也可以是小模型分类,关键是别让低价值请求白白占用长上下文——这跟前面讲的缓存、压缩是同一本账。

三、我的组合拳顺序

三层串起来的顺序我固定是:先路由分流(这请求配不配用长上下文)→ 需要外部资料就检索 + 重排(只留 3~5 条)→ 拼装时用 smart_order 排位置。排查顺序反过来:模型答错,先看答案文档在上下文哪个位置——在中间就重排或精减,还错就再减文档数。这比我以前"全塞进去再反复调 prompt"管用得多。

总结

一句话:Lost in the Middle 告诉我们,模型对长上下文的利用是条 U 型曲线,中间是盲区。我的解法是把"给模型看什么"从体力活变成技术活——路由先分流,检索只留精华,拼装把关键钉头尾。上下文工程的三本账(显存、钱、准确率)到这里就都算完了。

下期预告:下一篇我想给这个系列收个尾,把这几篇串成一个完整的"上下文预算"系统:一次请求花多少 token、哪些前缀能缓存、哪些内容要压缩、哪些该检索,用一张预算表管起来。想一起折腾的,下篇见。

参考文献

转载请注明:Falost的小窝 » Lost in the Middle:为什么上下文越长,模型越容易“迷路”?我的三层解法(位置管理+检索精减+路由分流)

如果你觉得这篇文章不错或者对你有帮助,想请我喝一杯咖啡,可以打赏
喜欢 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址