#大模型#RAG

检索的 Query 改写进阶

用户的原始提问往往不是好的检索词——有指代、太模糊、是多问合一。Query 改写是 RAG/搜索里 ROI 极高的一步。本文系统讲清五种改写(指代消解/子问题分解/HyDE/Multi-query/同义扩展)、各自的适用场景、工程实现与成本,以及改写本身也要评估。

本文要点

  • 主题聚焦:大模型、RAG
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2025年12月16日·更新于 2026年7月23日

在 RAG 和搜索系统里,有一个反直觉的事实:用户敲进搜索框的那句话,几乎从来不是最好的检索词。你在召回阶段做的所有努力——混合检索、Rerank、调 embedding 模型——都在用一个可能有缺陷的 query 做输入。Query 改写(Query Rewriting / Query Transformation)就是把这第一步做对。它是整条检索链路里 ROI 最高的优化之一,却最容易被当成"锦上添花"忽略。

本文系统讲清五种主流改写、各自的代价与适用场景,以及工程上怎么落地、怎么评估。

一、原始 query 为什么永远不够

先看用户的原始提问会在检索管线里踩哪些坑。几乎所有"召回不准"的 case,拆到底都能归到下面四类:

  1. 指代缺失(多轮对话)。用户上一句问"iPhone 17 Pro 怎么样",这一句问"它的电池续航呢"。"它"对人无歧义,对检索器却是灾难——拿"它的电池续航呢"去做向量检索,召回的多半是讲"电池"的泛泛文档,而非 iPhone 17 Pro 的电池。检索器没有上下文记忆。

  2. 太模糊。"我想学 Rust"——是入门教程?还是和 Go 的对比?还是异步编程?一个模糊 query 映射到向量空间是一个模糊的簇, top-k 里什么都有,什么都不精。

  3. 多问合一(compound question)。"对比一下 Postgres 和 MongoDB 在事务和水平扩展上的优劣"——这其实是 4 个原子查询(Postgres 事务、Postgres 扩展、MongoDB 事务、MongoDB 扩展)。用一句话去检索,每一路都被稀释;拆开来分别检索再合并,每一路才精准。

  4. 精确词 vs 语义的错配。用户问"ErrorCode=E_4021 怎么解决",这是精确串——交给 BM25 没问题,交给纯向量检索却常失手,因为向量编码器更懂"语义"而非"符号"。反过来,问"怎么处理订单一直 pending"是纯语义,BM25 又抓不到文档里"卡单排查流程"的表述。这不是改写能独立解决的(要混合检索),但改写可以把"订单 pending"归一成文档里实际的"卡单",帮上忙。

一句话:检索的天花板,在改写这一步就被定下了。后面再强的 Rerank,也救不回一个根本没召回相关文档的 query。

二、五种改写,各擅其场

1. 指代消解(Coreference / Conversational Query Rewriting)

最基础也最高频。多轮对话里,把带指代的当前 query,结合历史改写成一个自包含(standalone)的检索 query。

text
历史: 用户: iPhone 17 Pro 的价格是多少?   助手: 999 美元起。
当前: 它的电池容量呢?
改写: iPhone 17 Pro 的电池容量是多少?

这是 LLM 一句话就能干的事,代价极低(一次小模型调用),但对多轮场景收益巨大——不做指代消解的多轮 RAG,基本不可用。

实现上用一个轻量 prompt 即可,关键是把最近 N 轮(通常 3-5 轮)的对话历史塞进去。生产里建议用最快的模型(Haiku/Flash 级),别上大模型——这一步不需要推理深度,需要的是延迟和成本。

2. 子问题分解(Sub-question Decomposition)

面对复合问题,先让 LLM 把它拆成多个原子子问题,每个子问题独立检索,最后把多路结果合并喂给生成。

text
原问题: 对比 Postgres 和 MongoDB 在事务和水平扩展上的优劣
子问题:
  - Postgres 的事务支持如何?
  - Postgres 的水平扩展能力如何?
  - MongoDB 的事务支持如何?
  - MongoDB 的水平扩展能力如何?

每个子问题都是精准的小 query,召回质量远高于拿一整句去检索。代价是 N 倍的检索调用(N = 子问题数),以及合并逻辑的复杂度。适合问题本身就是"多维度对比"或"多步推理"的场景(比如"X 和 Y 哪个更适合做 Z,各自的代价是什么")。

不适合简单事实型问题——拆了反而引入噪声,还白付 N 倍成本。所以这一步前面通常有个判断:这个问题该不该拆。

3. HyDE(Hypothetical Document Embeddings)

思路最巧妙的一种。原理:问题和答案在向量空间里其实隔得远,但"假设答案"和"真实文档"却挨得近——因为它们都是"答案的表述",用词、结构更接近。

所以 HyDE 的做法是:先让 LLM 针对原问题生成一个假设性答案(哪怕有幻觉、哪怕不准),然后拿这个假设答案去做向量检索,而不是拿原问题。

ts
// HyDE 核心两步
const hypothetical = await llm.chat({
  messages: [
    { role: "system", content: "请针对用户问题,写一段 100 字左右的、可能包含答案的文档段落,即使你不确定也要写得像那么回事。" },
    { role: "user", content: originalQuery },
  ],
});

// 用假设答案(而非原问题)做向量检索
const docs = await vectorStore.similaritySearch(await embed(hypothetical), k);

HyDE 对开放域语义问答效果显著,尤其是 query 短而文档长的场景(用户问得很简略,文档写得很详尽)。但它有两个坑:一是假设答案里若混入了错误的事实,反而会把检索带偏(把"跑偏的语义"投向向量空间);二是它只对向量检索有效,对 BM25 反而有害(假设答案里的精确词和真实文档对不上)。所以 HyDE 通常只在向量召回这一路开,别喂给 BM25。

4. Multi-query(同义改写多版本合并)

让 LLM 把原 query 改写成 N 个不同表述的同义 query,每个各检索一次,再合并去重。

text
原问题: 怎么优化 React 的渲染性能?
改写:
  - 减少 React 组件的不必要重渲染有哪些方法?
  - React 性能优化的最佳实践
  - React memo / useMemo / useCallback 什么时候用?

不同表述覆盖了文档里可能的不同措辞——有的文档写"重渲染",有的写"re-render",有的写"性能优化"。多版本召回合并后,召回率(不漏)显著提升。

合并用 RRF(Reciprocal Rank Fusion) 或简单的分数加权,和混合检索的融合是一回事。代价同样是 N 倍检索调用,适合"召回率优先、怕漏"的场景。

Multi-query 和子问题分解长得很像,但本质不同:子问题分解是拆"不同维度",Multi-query 是换"同一意思的不同说法"。前者解决多问合一,后者解决表述错配。

5. 同义扩展 / 术语归一

最朴素的一类,不一定需要 LLM。把用户用的词,扩展或归一到文档库里实际用的词。

  • 同义扩展:"卡顿" → 扩展成 "卡顿 OR 延迟 OR lag OR jank"。
  • 术语归一:"DB" → "数据库","ES" → "Elasticsearch","K8s" → "Kubernetes"。

可以用同义词表(运营维护)、用 embedding 找近义词、或让 LLM 做术语归一。它的价值在领域术语密集的语料里特别明显——医疗、法律、公司内部黑话。一个简单的同义词表,往往就能把召回率拉一截,成本几乎为零。

三、工程实现:何时改写、成本与缓存

别"总是全开"。五种改写,每一种都不是免费的——多数都要多调一次 LLM,而且越是复杂改写(分解、HyDE、Multi-query),后面跟的检索调用也越多。把所有改写无脑串成一条管线,首字延迟(TTFT)会飙升,用户体感会很差。

工程上推荐按需路由:先用廉价信号判断这个 query 需要哪种改写,再触发对应策略。

ts
async function routeAndRewrite(
  query: string,
  history: ChatMessage[],
): Promise<string[]> {
  // 1. 有历史且当前 query 短/含指代词 → 做指代消解(几乎必做)
  if (history.length > 0 && /\b(它|这个|那个|上面|刚才)\b/.test(query)) {
    return [await resolveCoreference(query, history)];
  }

  // 2. 用便宜的小模型判断:这是个复合问题吗?
  const isCompound = await cheapClassify(query, "single_or_compound");
  if (isCompound === "compound") {
    return decompose(query); // 返回多个子问题
  }

  // 3. 短而开放的语义问题 → HyDE(只用于向量路)
  // 4. 默认:轻量 Multi-query(2 个改写)兜底召回率
  return [query, ...await multiQuery(query, 2)];
}

几个实战原则:

  • 指代消解几乎总是值得的(成本低、多轮刚需),其他改写按场景开。
  • 把改写结果缓存起来。同一个(改写后的)query 检索结果,用 query 哈希做 key 缓存几分钟到几小时。用户重复问、相似问,直接命中,省掉 LLM 和检索的开销。
  • 改写用快模型,生成用强模型。改写是把 query 弄"对",不需要深度推理;把推理预算留给最后的答案生成。
  • 给改写设延迟预算。比如改写这一步总耗时不超过 300ms,超了就降级用原 query——别为了完美改写拖垮响应时间。

四、改写也要评估(否则就是玄学)

改写不是"加了就一定好"。过度改写是真实风险:LLM 可能把用户的问题"脑补"跑偏,改写出一个语义漂移的 query,反而召回一堆不相关文档,把原本能答对的问题搞砸。HyDE 尤其容易踩这个坑——假设答案里编了一个错误前提,整个检索就跟着偏。

所以改写必须测。最直接的测法:固定一个评估集,对比"开改写 vs 不开改写"的召回指标变化。

  • Recall@k(召回率):该被召回的相关文档,开改写后召回了多少?这是改写最该提升的指标——改写的核心价值就是"不漏"。如果开了 Multi-query,Recall 不升反降,说明改写跑偏了,赶紧回滚。
  • Context Precision:召回的文档里相关的占比。分解和 Multi-query 在提升 Recall 的同时,可能拉低 Precision(召回更多但更杂)——这时候靠后面的 Rerank 找补。
  • 端到端答案质量(Faithfulness / Answer Relevancy):最终答案变好才是真的变好。中间指标涨了但答案没变好,说明改写的收益被下游吃掉了。

实操:在你的评估集(50-200 条 问题 → 期望引用文档)上,A/B 跑两遍——一遍原 query,一遍改写后——对比 Recall@k。改写这一步的净收益,一目了然。没有这个评估集,所有改写优化都是凭感觉。

五、组合拳:按场景编排

成熟系统不会只用一种改写,而是按场景编排:

  • 多轮对话型(客服 / 助手):指代消解(必做) + 必要时子问题分解。HyDE 和 Multi-query 一般不必要。
  • 单轮深度问答(知识库搜索):HyDE(向量路) + 轻量 Multi-query 提召回。指代消解不需要(没有历史)。
  • 对比 / 多维问题(研究类):子问题分解为主,每路子问题再各自做一次 Multi-query 兜底。
  • 领域术语密集(医疗 / 法务 / 内部知识库):同义扩展 / 术语归一打底,几乎必做,成本极低。

组合不是越多越好。每加一种改写,就多一层延迟和成本,也多一个跑偏的可能。先上 ROI 最高的那一种,评估有正向收益,再叠加下一种。

六、与混合检索、Rerank 的配合

改写不是孤立的,它和下游的混合检索、Rerank 是一条链路,要协同设计:

  1. 改写 × 混合检索:不同改写给不同的检索路。HyDE 只喂向量路(别喂 BM25);子问题分解的每个子问题,可以都走"向量 + BM25"的混合;同义扩展的 OR 查询,主要喂给 BM25(精确词扩展)。别一刀切地把同一个改写结果灌给所有检索器。

  2. 改写 × Rerank:改写(尤其 Multi-query、分解)倾向提升 Recall、牺牲 Precision——召回多了,但更杂。这正好是 Rerank 的用武之地:先用改写 + 混合检索召回 30-50 条(求全),再用 Reranker 选出最相关的 3-5 条(求精)。两者是天然的搭档,改写负责"不漏",Rerank 负责"不噪"。

  3. 整体链路:原 query → 按需改写 → 各路混合召回(N×K) → RRF 融合 → Rerank 取 top-k → 注入 prompt → 生成。改写是这条链路的第一环,也是杠杆最大的一环。

小结

  • 原始 query 几乎从不是好的检索词:指代缺失、表述模糊、多问合一、词表错配——这四类问题在召回的第一步就把天花板压低了。
  • 五种改写各擅其场:指代消解(多轮刚需)、子问题分解(复合问题)、HyDE(拉近问答语义距离)、Multi-query(换表述提召回)、同义扩展(术语归一)。没有银弹,按场景选。
  • 按需路由,别全开:改写有 LLM 成本和延迟代价。指代消解几乎总值得,其他用廉价信号判断后再触发,并给改写设延迟预算、做缓存。
  • 改写必须评估:核心看 Recall@k 的变化,警惕过度改写把 query 跑偏。没评估集,优化就是玄学。
  • 和 Rerank 是搭档:改写提 Recall(多召回),Rerank 提 Precision(选最好),一前一后,缺一不可。

金句:检索的天花板,在改写这一步就被定下了——后面再强的 Rerank,也救不回一个根本没召回相关文档的 query。把第一步做对,胜过把后面做精。

Related

相关推荐