Skip to main content

查询改写的三种做法

"用户问的和文档写的不是一回事"——这是检索召回差的最常见原因,而它其实有三种不同的不一致,对应三种不同的改法。

把这三种当成三种可选的优化手段轮流试,是浪费时间。正确的做法是先判断你遇到的是哪一种失配。

HyDE表述层级不一致先让模型写一段假设答案,嵌入它去检索风险:假设文档会编,只能当检索键Multi-Query用词不一致同一个意思改写成几种说法,各自检索后合并风险:已经很精确的问题改写没收益Step-Back问题太具体先退到更抽象的问题检索,再回到原问题作答风险:退得太远会召回入门内容每种改写解决一种几何失配;叠加使用通常不增效,只增加延迟
图:先判断失配类型,再选改写方式

1. 三种失配​

1.1 表述层级不一致 → HyDE​

用户问的是口语短句,文档写的是正式说明文。query 短、不完整、缺领域词汇;文档长、完整、术语密集。两者在向量空间里天然就远。

HyDE(Hypothetical Document Embeddings)的做法:先让模型写一段"假设的答案",把这段假设答案嵌入后去检索,而不是嵌原始问题。假设答案在长度、文体、用词上都更像被检索的文档,所以能落到正确的区域。

假设文档可以自由地编——它永远不会给用户看,只充当检索键。

1.2 用词不一致 → Multi-Query​

同一个意思可以有很多种说法,尤其涉及同义词、缩写、中英混用时。

Multi-Query 让模型把问题改写成几种不同的表述,各自检索,然后合并去重。它产生的是更大的候选池,后续的重排在这个池子上工作(见《重排修不了召回》)。

适合的信号是:如果同事听到这个问题会反问"你是指 X 还是 Y",那就是 multi-query 的地盘。

1.3 问题太具体 → Step-Back​

用户问"型号 X 在 2024 年 3 月的固件版本是多少",文档里没有这句话,但有一整节讲固件版本历史。检索原始问题什么都找不到。

Step-Back 先退一步,把问题抽象成"型号 X 的固件版本历史",用这个抽象问题检索,然后回到原始问题作答——检索用抽象层,回答用具体层。

这两步的契约要写清楚:只退检索、不退回答,否则用户会得到一个他没问的泛泛而谈的答案。

2. 近两年的实践经验倾向于:不要默认全用​

早期文章(包括原论文)给人的印象是"这些方法能提升召回"。这在特定条件下成立,但不是无条件的。

2.1 每次改写都要多一次模型调用​

这是最硬的成本。HyDE 等于在检索之前多加一次完整的生成调用,量级上和一次短文本生成相当,通常是这条链路上新增加的、最大的一块延迟。具体多少毫秒取决于模型大小、生成长度、硬件和有没有批处理——这类数字在不同环境里能差一个数量级,只能自己实测,不要照抄公开数据。

对交互式应用来说,这个代价不小。语音场景基本承受不起每轮都做。

2.2 相当比例的场景,朴素检索更好​

原因是:HyDE 生成的假设文档会"幻觉",把向量带偏到无关的邻域——尤其是模型对该主题本身不熟悉时(这一点 HyDE 原始论文自己也提到了)。

在部分数据集上 HyDE 反而比零样本直接检索更差,尤其是 embedding 模型本身做过 instruction tuning、或者查询侧已经带了任务前缀时:这类模型的原始 query 向量本来就已经被调整到了检索友好的位置,中间插一段生成的假设文档反而会把它推离正确的邻域。

所以出现"加了 HyDE 之后整体指标没变甚至变差"是完全正常的,不是你配错了。

2.3 叠加通常不增效​

HyDE + multi-query + step-back 三连,是常见的过度设计。每种改写解决一种失配,一个问题通常只有一种失配。叠三种的结果是:延迟翻三倍,召回没有额外提升。

一条实用规则:每条 query 最多用一种改写,且要先分类。

3. 怎么做 gating​

既然不能默认全用,就要决定"什么时候用"。四档做法:

第一档:不做。 先建立基线——原始问题直接检索的 recall@K 是多少,按查询类型分开记。没有基线就无法判断改写有没有用。

第二档:规则分档。 用可观测的信号分:查询长度(很短的口语 query 更可能缺领域词)、是否命中(未命中或低分时才改写)、问题类型(是否含多个实体)。

第二档里的"检索失败后再改写"是最划算的一种:只在朴素检索没召回时才付这次延迟。

第三档:分类器。 用一个小模型把 query 分成 factual / advisory / comparative,只对特定类别启用。代价是又一次调用——所以第三档通常只在改写收益已被验证后才值得。

第四档:检索质量门控。 不看 query 长什么样,只看第一轮检索的结果本身好不好:top-1 分数低于阈值、或者候选分数分布太平(top-1 和 top-5 差不了多少),就说明这一轮没找对,再触发改写。这是 CRAG 那类"先评估检索质量、再决定要不要加工"思路的简化版。

第四档的好处是比分类器省一次调用——判据是第一轮已经算出来的分数,不需要额外跑模型。代价有两条:阈值必须在自己的评测集上标定,而分数分布会随语料变化漂移,要定期复核。

四档里第二档和第四档最划算,因为它们都只在"这一轮确实没找对"的时候才付延迟;第三档多一次调用,只在改写收益已经被量化验证之后才值得上。

4. 三条不能违反的规则​

4.1 永远保留原始 query​

任何改写都要和原始 query 一起检索,不能替换掉它。

原因是改写会丢信息:假设文档可能漏掉关键实体,抽象化会丢掉具体型号和版本号。而精确匹配那一路(BM25)正是靠这些具体词工作的——见《混合检索与 RRF 融合》,稀疏那一路的价值就在这里。

所以正确的结构是"原始 query 检索 + 改写 query 检索,合并去重",不是"用改写替换原始"。

4.2 假设文档绝不能当引用​

HyDE 生成的那段文字是编的。它不能进上下文,不能当证据,不能出现在引用里。

这一条在实现上要注意:假设文档和检索结果在同一个函数里来回传,很容易不小心把它一起塞进 prompt。安全的做法是用不同的变量名、不同的数据结构,让它们无法被误用。

引用只来自真实检索到的切片——这一点在《答案要能追溯到出处》里是硬要求。

4.3 trace 里要能区分检索键​

合并多个检索结果时,要知道每一条来自哪个检索键(原始 / 假设文档 / 某个子问题 / 抽象问题)。

否则当你发现召回变差时,无法判断是哪一路带来的。这也是《RAG 评测指标怎么选》里说的分层——检索层的问题要能定位到具体环节。

5. HyPE:换一个方向做​

HyDE 是在查询时生成假设文档。有人把这个方向反过来:在入库时为每个切片生成若干假设问题,用它们替代(或并存)切片向量参与匹配——论文里的做法(HyPE)是替代:切片存的是"假设问题"的向量,查询时直接用问题匹配问题(question-question matching),绕开文档与问题之间的表述鸿沟。实际落地两种用法都有人做:替代会丢掉原文语义、并存则索引体积翻倍,按语料试。

这个变体(HyPE)的好处是查询时零额外成本——生成发生在离线。代价是要重新索引,且索引体积变大。

如果你的语料稳定、查询量大,这个方向更划算:同样的收益,不在查询时付延迟。

6. 怎么验证有没有用​

不要看整体指标。用这个顺序:

  1. 先有基线:原始检索在你的评测集上的 recall@10,按查询类型分开
  2. 开改写,再测一遍,同样按类型分开
  3. 看交互项:HyDE 是不是提升了探索类但损害了事实类?如果是,就该加 gating 而不是整体启用或整体放弃
  4. 画延迟与召回的对照:p95 延迟为横轴,recall@10 为纵轴。延迟涨了但召回没动,就砍掉
  5. 检查改写本身:抽样看模型生成的假设文档或改写,有没有改变用户意图

第 5 步最容易被跳过,但它能发现"改写悄悄换了问题"这类错误——比如把"我不想续订"改成"如何续订",方向完全反了。

给每条 case 打上是否经过改写、用了哪种改写的标记,这一步才有数据可做。

7. 一个务实的结论​

如果你的系统已经有混合检索 + 重排,那么查询改写带来的边际收益通常不大——混合检索已经解决了部分词汇失配问题(稀疏那一路)。

改写真正值得做的场景是:

  • 检索日志里明显能看到"用户这么说、文档那么写"的失配
  • 查询很短、很口语,而语料是正式文档
  • 已经有基线,能测出收益

三条都满足再做,且从"失败后才改写"这一种开始。它是成本最低的一种:只在这一轮没找对的时候才付延迟。

最后提醒一句定位:改写和混合检索、重排一样,都属于《RAG 链路与失败模式》里在线侧的手段——它救不了离线侧的病,语料没解析干净、切片切坏了,改写得再花哨也检索不到。