增加检索 chunk 数量线性增加成本,但 recall 收益很快边际递减;同时大模型对长上下文的注意力呈 U 形分布,越晚加入的 chunk 被阅读概率越低,50 个 chunk 的边际收益实际最差。
如果检索不完善而窗口又很大,为什么不检索五十个 chunk 而非五个,然后让模型自己判断?因为召回率和成本的增速并不一致,而且 chunk 在上下文中的位置其实很重要。
这是一个合理的直觉。每一次因为文档缺失而导致的 RAG 失败都支持更大的 k,而拥有大上下文窗口的模型让这个限制感觉已经消失了。检索五十个,全部传入,召回问题就这样被暴力破解了。
两件事会出问题。账单随 k 线性增长,而召回率却趋于平坦;而且模型并不会均匀地关注长上下文——所以边际的那个 chunk 既是最昂贵的一个,也是最不可能被读到的一个。
已发表研究发现了什么
基准结果是 Liu 等人的论文 Lost in the Middle: How Language Models Use Long Contexts(arXiv:2307.03172,后发表于 TACL)。他们在多文档问答任务中控制了含答案文档在一堆干扰项中的位置,并让这个位置产生变化。
报告的模式呈 U 形:当相关文档位于输入的最开始或最末尾时,准确率最高;当位于中间时,准确率大幅下降。他们还报告说,当干扰项足够多时,金标准文档位于中间位置时的表现可能低于模型在完全没有检索到任何文档时的表现——在这种配置下,检索反而比不做更糟。
把这当作对一类行为的发现,而非一个常数。这是针对那个时期的模型测量的,长上下文训练自那以后已有改进;一些更新的模型显示出更平坦的曲线。但没有改变的是:这个属性是模型特定的,值得去验证而不是假设;而且效果的方向——末端优于中间——在后续工作中足够稳健,可以作为设计依据。
为什么针测试无法定论
一个厂商在针haystack评估中展示了 100% 的成绩,并没有推翻上述结论,理解其中的原因是关键的有用之处。针任务把一个独特的、不在上下文的句子插入到填充文本中,然后让模型找到它。这是一个词汇上 trivial 的搜索:针与周围环境不相似。
检索到的 chunk 则相反。五十个都在同一主题上——它们就是因为这个被选出来的——而且其中几个是近似的候选,讨论了正确的主题但细节错误。任务不是找到一个独特的字符串,而是在合理的候选之间做区分,并在其中几个上做聚合。在更难的任务上表现出真实的降级,与完美的针分数是一致的,所以这不是你在决定 k 时需要的证据。
两条曲线分叉
在你自己的检索评估上跑一下数字。Recall@k 有一个特征形状——从 1 到约 10 快速上升,之后趋于平坦——因为大多数问题已经被排序器高度排名的一个 chunk 回答了,而那些没有被回答的问题通常需要不同的分块策略,而不是更长的列表。成本是线性的。
Assume 500-token chunks and an input rate of Gin per million tokens.
k = 5 -> 2,500 context tokens 1.0x cost
k = 10 -> 5,000 2.0x
k = 25 -> 12,500 5.0x
k = 50 -> 25,000 10.0x
Now put your own recall@k beside it. If recall@50 exceeds recall@5 by
a handful of points, you are paying ten times the input cost for that
handful — and adding 45 chunks of distractor to every request that
did not need them.
Latency moves too: prefill is roughly linear in prompt length, so
time to first token grows with k on every single request.
重排序器打破了这个权衡,这是支持它最有力的论据。检索 50 个,让 recall@50 成为你的上限,重排序到 5 个,让成本、延迟和干扰都处于 k = 5 的水平。你买到了长列表的召回率,却不需要为其 token 付账。
顺序作为一个自由变量
考虑到位置效应,你连接检索到的 chunk 的顺序是一个大多数 pipeline 从未设置的真实参数。大致按值得尝试的程度排序的选项:
最佳优先。默认选项,也是一个合理的选项。把最强的证据放在模型最可靠关注的区域。
三明治。最佳 chunk 在前, 次佳在最后,其余放在中间。直接利用了 U 形,而且实现起来零成本。
最佳最后。紧挨着问题之前。值得测试,因为上下文中的近因效应通常有很大影响,这样可以把最强证据放在离生成最近的位置。
来源顺序。如果你的 chunk 是一个文档的顺序部分,按位置而非分数排序会给模型连贯的 prose 而不是打乱的顺序,这可能比分数排序更重要。
无论你选择哪一个,给来源编号并指示引用,并且去测量这个选择而不是靠推理——这是一个在固定检索集上的廉价 A/B,没有索引变化也没有模型变化,这使得它成为 RAG pipeline 中少数真正干净的实验之一。
还有一件事会降低大上下文的效果,与注意力无关。重叠的 chunk 意味着在 k = 50 时你经常把同一个句子发送两到三次,而来自不同文档的近似重复段落——一个在四处被引用的政策——会进一步倍增。模型把重复读作证实,但当重复是你的 splitter 造成的人工产物时,这是错误的推论。在组装 prompt 之前对检索集做去重:合并在来源范围上重叠的 chunk,并用廉价的相似度检查去掉几乎相同的文本。在一个有大量重叠的语料上,这免费为上下文预算恢复了真实的一部分。
如果你想知道你的模型的行为而不是 2023 年模型的行为,这个实验小到可以在一个下午运行完成,而且比任何发表数字都更有价值。从你的评估集中取二十个问题,构建一个包含正确答案的 k 个 chunk 的固定上下文,然后只改变它的位置——最前、中间、最后——保持其他所有条件相同。按位置记录准确率。这是对你实际服务的语料的一个干净、诚实、单变量的测量,它直接告诉你排序是否值得工程化。
位置效应是模型的属性,不是 pipeline 的属性,所以同一个上下文在两个模型中可能表现不同。广告的上下文长度在目录里,但模型用得好的长度是另一个问题,你的评估集可以回答。
Reranking: The Cheapest Accuracy Win in RAG
Is RAG Dead Now That Context Windows Are Huge?
Why Your RAG Returns the Right Chunk and the Wrong Answer