跳到主要内容

RAG 流水线中被你忽略的查询重写层

· 阅读需 12 分钟
Tian Pan
Software Engineer

当 RAG 系统回答错误时,大多数团队的第一反应是归咎于编码器(encoder)。更换更大的嵌入模型(embedding model)。尝试针对特定领域微调过的模型。增加维度。三个迭代周期(sprint)后,召回率曲线只提升了几个百分点,而用户的投诉看起来还是老样子。

诊断错了。大多数检索失败并非嵌入失败。它们是查询形状(query-shape)失败——在编码器运行之前就存在的词汇不匹配,无论如何调整向量都无法修复。

用户输入“如何取消”。相关的文档标题却是“订阅生命周期管理”,并使用了“终止”、“计费周期结束”和“服务停用”等词汇。世界上没有任何编码器能靠词汇运气将这两个字符串拉入同一个邻域。余弦相似度(cosine similarity)的差距是真实存在的,它存在于输入中,而非模型中。位于检索之前的查询重写层是大多数流水线跳过的步骤,随后他们却要花一个季度的时间试图在下游进行补偿。

你一直误诊的词汇缺口

标准的 RAG 检索将用户的提问视为一个现成的查询。它对其进行编码,在索引中进行点积运算,并返回 top-k 结果。这在有人同时编写了问题和文档的演示数据集上有效。但在生产环境中会失效,因为真实用户是以向人类提问的方式书写的:简短、描述不足、充满代词和隐含语境,且通常被表述为一个问题而非一个主题。

另一方面,文档是由那些自认为在解释某事的人编写的。它们冗长、结构化、充满章节标题和抽象名词,且几乎从不以“如何”开头。嵌入空间的几何形状反映了这种不对称性。简短的用户查询与其它简短的用户查询聚在一起。冗长的说明性段落与其它冗长的说明性段落聚在一起。真实查询与实际回答它的文档之间的平均距离,比两个无关文档之间的平均距离还要大。

这就是为什么微调嵌入模型通常令人失望的原因。如果你有标记过的配对数据并且有耐心,你可以从领域调优的编码器中挤出 5–10% 的召回率,但你优化的是流水线中错误的一端。更廉价的做法是重塑查询,使其与文档处于相同的邻域。这就是查询重写的作用。

HyDE:嵌入你希望拥有的答案

假设性文档嵌入(Hypothetical Document Embeddings,简称 HyDE)彻底改变了检索问题。与其对问题进行嵌入,不如要求 LLM 为该问题写一个虚假的答案,然后对 那个答案 进行嵌入并据此进行检索。

直觉是几何学上的。真实的答案文档和虚构的答案文档是以相同的语调编写的:相同的长度、相同的词汇、相同的名词短语密度、相同的说明性语气。它们位于嵌入空间的同一区域。简短的用户提问则不然。因此,虚构的答案充当了翻译器,在触达索引之前就将查询带入了文档所在的邻域。

HyDE 不需要假设内容在事实上的正确性。它只需要在风格和词汇上具有代表性。LLM 可以自信地编造一个错误的答案,而检索依然会得到改进,因为你匹配的是形状,而非事实。根据语料库的不同,报告的增益从几个百分点到几十分点的精确率和召回率不等,而在用户词汇与文档词汇差异最大的领域提升最为明显——这正是微调编码器成本最高昂的情况。

成本是在检索之前增加了一次额外的 LLM 调用。在小型模型上,这需要 200–500 毫秒。这是一项真实的权衡,在对延迟敏感的路径上,你可能希望在 HyDE 之前进行置信度检查(先运行普通检索;如果 top-1 相似度得分低于阈值,则回退到 HyDE 并重新检索)。对于批量摄取或分析查询,你几乎总是可以毫不犹豫地支付这项延迟成本。

分解:一个问题隐藏了三个子问题

多跳(Multi-hop)问题以另一种方式破坏了检索。“向 CTO 汇报的工程经理中,谁是在 B 轮融资前加入的?”无法通过单一文档回答。答案需要三次检索——谁是 CTO,谁向他们汇报,以及每个人何时加入——原始问题的嵌入并不指向其中任何一个。它指向一个包含三者碎片的模糊中间地带,对于其中任何一个点来说都不是正确的文档。

分解(Decomposition)将多跳查询重写为一系列单跳子查询,对每个子查询进行检索,然后进行合并。一次性完成(“将此拆分为独立的子问题”)已经优于单查询检索。迭代完成——检索,查看发现的内容,根据部分答案生成下一个子查询——可以匹配更繁重的多跳基准测试的召回率,同时每步提取的块更少。

需要注意的失败模式是过度分解。被要求分解的 LLM 会乐于将一个单跳问题拆分为三个冗余的子查询,从而在没有任何收益的情况下使你的检索成本翻倍。廉价的缓解措施是使用路由器:在调用分解器之前,使用一个小型分类器来决定问题是单跳还是多跳。这种成本意识版本将简单问题通过普通检索路由,仅对复杂问题进行升级,从而保持 p50 延迟持平,并确保 p99 延迟处于合理范围内。

多查询扇出与排名融合

即使是单跳问题,用户的表述也只是各种合理解法分布中的一个样本。“我该如何取消”有许多同义表达:“结束我的订阅”、“停止计费”、“移除我的计划”。每个同义表达嵌入到向量空间的位置都略有不同,每个表达检索到的前 k 个结果(top-k)也略有差异。不同表述之间的交集比任何单一检索结果都更可靠。

多查询扇出(Multi-query fan-out)会生成用户查询的 N 个同义改写,对每个改写进行检索,并融合结果。标准的融合算法是倒数排名融合(Reciprocal Rank Fusion, RRF),它根据文档在各个列表中出现的排名,按照 1/(k + rank) 的总和为每个文档评分(k 通常设置为 60)。在多个列表中排名靠前的文档会胜出;仅在某一个列表中排名靠前但在其他列表中未出现的文档则会被视为主题偏移而被过滤。

RRF 基于排名而非分数,这也是为什么它在融合产生不可比分数的检索器(例如稠密向量相似度和 BM25)时表现良好。它也不需要训练数据,只需要一个常量和一次排序。RAG-Fusion 是一种结合了多查询生成与 RRF 的架构。据报道,与原生 RAG 相比,它的回答质量在准确率上提升了约 8–10%,在全面性上提升了 30–40%,这比大多数更换嵌入模型带来的提升都要大。

架构层面的教训是:检索召回率的提升并不依赖于一个完美的查询,而是通过一组合理查询的集成(Ensemble)并通过其交集来过滤噪声实现的。而查询重写器正是创建这个集成工具。

延迟与成本的代价——以及为什么它是值得的

以上每种技术都会在检索前增加一次 LLM 调用,工程师们理所当然地会对此持怀疑态度。为了增加 20% 的召回率而使 p50 延迟翻倍,这并不是免费的午餐。诚实的账本如下:

  • HyDE 增加了一次调用。可以使用小型快速模型(重写器不需要前沿大模型)。对于重复问题,可以按查询哈希进行缓存。
  • 多查询扇出增加了一次生成同义改写的调用,以及 N 个并行检索。检索是并行扇出的,因此实际运行时间(wall-clock cost)是一轮检索的时间,而不是 N 轮。
  • 查询分解增加了一次拆分调用,然后根据依赖关系进行顺序或并行检索。负责拦截分解请求的路由(Router)是节省预算的关键。

对于交互式聊天,预算非常紧张,路由拦截至关重要。而对于分析型、批处理或智能体驱动的检索,由于工具调用本身已经耗时数秒,重写器的成本只是四舍五入的误差。那些报告重写投资回报率(ROI)较低的团队,通常是在没有路由的情况下全局开启了重写,将延迟代价平摊到了每一个类似于“现在几点”的查询上,并由此得出该技术昂贵的结论。技术本身没有问题,问题在于部署方式缺乏约束。

真正有意义的成本对比是“重写器 vs 编码器微调”。微调嵌入模型需要标注数据对、训练流水线、评估工具链,并且每次发布新模型时都需要对存量语料库进行重新嵌入(re-embedding)。而一个查询重写器只需要五十行代码和一个 Prompt。重写器一周就能上线,而编码器微调则需要一个季度。它们带来的召回率提升通常是不相上下的,而且重写器甚至是微调能否产生可衡量效果的前提——如果你的查询本身“形状不对”(misshapen),你就是在针对噪声进行编码器微调。

评估:区分“找错了文档”与“从未请求正确的文档”

团队之所以会误诊检索失败,是因为他们的评估将两类截然不同的错误混为一谈。检索失败可能意味着:

  1. 检索器找到了错误的文档。 查询是合理的,但索引返回了不相关的文档。这是检索器或编码器的问题。
  2. 查询从未请求正确的文档。 用户的表述与文档的词汇量相距甚远,任何检索器都无法填补这一鸿沟。这是一个查询形状(Query-shape)问题。

单一的 recall@k 指标无法区分这两者,这就是为什么许多团队在需要重写器时却一直在购买编码器升级服务。请拆分评估流程:

  • 运行原生检索,测量 recall@k。
  • 运行一个理想重写器(Oracle rewriter)——由人类或强大的 LLM 将每个查询重写为接近目标文档词汇的内容,再次测量 recall@k。
  • 两者之间的差距就是你查询形状的上限。如果差距很大,说明你的编码器没问题,而是你的查询形状不对,重写器将弥补大部分差距。
  • 如果差距很小但绝对召回率仍然很低,那么编码器确实是瓶颈,微调才是合理的。

这种诊断方法可以防止“花费三个 Sprint 微调嵌入模型”却最终失败的情况。大多数运行该诊断的团队会发现,他们的编码器其实已经足够好了,而一个五十行的查询重写器本可以在一周内获得同样的收益。

应该先构建什么

如果你正面对一个已经陷入瓶颈的 RAG 流水线并考虑更换编码器,请按以下顺序进行:

  1. 构建上述诊断拆分。 这只需要一天时间。它会告诉你面临的是查询形状问题还是检索问题。
  2. 如果差距很大,上线一个重写器。 从多查询扇出加 RRF 开始——这是成本最低、收益最高的技术,且不需要针对特定领域进行调优。它还能与你后续做的任何工作完美堆叠。
  3. 针对原生检索评分置信度较低的查询添加 HyDE。 通过相似度阈值进行拦截,只在必要时支付延迟代价。
  4. 针对多跳问题增加路由和分解器。 这是成本最高的一步;最后再做,且仅在确认多跳问题在你的流量中占有重要份额后才做。
  5. 最后,如果残余差距确实存在,再微调编码器。 届时你将针对形状良好的查询进行微调,这是让结果具有可解释性的唯一前提。

查询重写层是 RAG 架构中最便宜、最被低估的部分。跳过它并不会让你的流水线变得更简单,反而会让流水线的其他部分承担本不该承担的压力。先构建重写器,进行诚实的衡量,编码器的问题通常会迎刃而解。

References:Let's stay in touch and Follow me for more thoughts and updates