RAG查询改写

RAG 的效果不只取决于 Embedding、分块和向量数据库。很多时候,真正拖垮召回率的是用户输入本身:它可能口语化、缺少上下文、包含多个意图,甚至只是一句“刚刚说的那个呢?”。

**查询改写(Query Rewriting / Query Transformation)**位于用户问题与检索器之间,目标不是把句子写得更漂亮,而是把问题转换成更适合检索的表达,同时保留原始意图与约束。

一、为什么原始 Query 往往不适合检索

1. 用户语言与知识库语言存在语义鸿沟

用户倾向于使用口语、缩写或业务黑话,知识库则使用规范术语。例如用户问“怎么让模型记住前文”,文档中可能写的是“对话状态管理”“长上下文”或“持久化记忆”。即使语义接近,短 Query 的向量也可能无法稳定命中目标文档。

2. Query 缺少必要背景

“它支持本地部署吗?”对人类来说可以结合上文理解,但检索器只看到这一句时,并不知道“它”指的是哪个模型。多轮对话中的指代、省略和话题切换,都会使检索输入失真。

3. 一个问题包含多个检索目标

“比较 Milvus 和 Elasticsearch 的混合检索能力,并给出低成本部署建议”至少包含三个任务:

  • Milvus 的混合检索能力;
  • Elasticsearch 的混合检索能力;
  • 两者在成本和部署复杂度上的比较。

用整句做一次向量检索,结果往往只覆盖其中一个方面。

4. 用户问题过窄或表达方式与答案不同

查询通常是疑问句,而文档通常是陈述句。比如用户问“为什么 temperature=0 仍然不稳定?”,相关文档可能描述“浮点误差、并行规约顺序和相同概率下的 token 选择”。Query 与答案在长度、结构和词汇上都不一致。

二、查询改写在 RAG 中的位置

一个更完整的检索链路可以表示为:

1
2
3
4
5
6
7
8
9
10
11
用户问题 + 对话历史

意图识别 / 路由

Query 改写、扩展或拆分

多路检索(向量、关键词、结构化查询)

结果合并、去重与重排序

基于原始问题生成答案

这里需要区分三个概念:

  • Query Rewriting:重写措辞与结构,得到一个更明确的查询;
  • Query Expansion:保留原查询,同时增加同义词、别名或多个查询变体;
  • Query Decomposition:把复杂问题拆成多个可独立检索的子问题。

它们可以组合使用,但不应对所有请求无差别地全部开启。

三、六类常用策略

1. 规范化表达

基础改写负责纠错、补全术语、去除无意义口头语,并保留时间、版本、对象和否定条件等关键约束。

1
2
原始问题:最新版那个模型在 Mac 上能跑吗?
改写结果:用户所指模型的最新版本是否支持在 macOS 的 Apple Silicon 设备上本地运行?

适合短文本、错别字较多、术语不规范,但意图相对单一的场景。

2. 多查询扩展

让 LLM 生成 3~5 个语义不同但意图一致的 Query,分别检索,再合并结果。变体可以覆盖:

  • 同义词与别名;
  • 专业术语与口语表达;
  • 原因、机制、限制等不同视角;
  • 中文名、英文名和缩写。
1
2
3
4
原问题:vLLM 为什么快?
变体 1:vLLM 推理吞吐量高的技术原因
变体 2:PagedAttention 如何降低 KV Cache 显存碎片
变体 3:vLLM continuous batching 工作原理

多查询会提升召回率,但也会带来重复结果和噪声。工程上通常使用 Reciprocal Rank Fusion(RRF) 合并不同结果列表:

$$
score(d)=\sum_{r \in R}\frac{1}{k+rank_r(d)}
$$

RRF 只依赖文档在各路结果中的排名,不要求不同检索器的分数处于同一量纲,适合融合 BM25 与向量检索。

3. HyDE:用“假设文档”靠近答案空间

HyDE(Hypothetical Document Embeddings)的做法不是直接编码问题,而是先让 LLM 生成一段可能的答案或相关文档,再对这段假设文档进行 Embedding,并用它检索真实文档。

1
Query → LLM 生成假设文档 → Embedding → 向量检索 → 真实文档

它解决的是问题表达和答案表达不在同一个语义空间的问题。需要注意:假设文档可能包含事实错误,但它只用于寻找语义邻域,不能作为最终答案的事实依据。

HyDE 更适合:

  • Query 很短,语义信息不足;
  • 知识库文本偏长、偏专业;
  • 零样本场景,缺少相关性标注数据。

它不适合精确实体、编号、错误码和日期查询,这类请求通常应优先使用关键词检索或元数据过滤。

4. Step-back:先退一步检索上位概念

Step-back Prompting 会先把具体问题抽象为更高层的问题,再同时检索原问题与上位概念。

1
2
原问题:为什么把 vLLM 的 tensor_parallel_size 从 1 调到 2 后输出出现差异?
Step-back:分布式推理中的并行计算为什么会影响数值确定性?

原问题负责找具体答案,Step-back Query 负责补充原理、规则和背景。它适合知识密集型、原理解释和多跳推理,不等于把复杂问题拆成多个子问题。

5. 子查询分解

当问题含有比较、并列、多跳关系或多个约束时,可以生成若干相互独立的子查询,并行检索后再聚合。

1
2
3
4
5
6
7
原问题:比较 LangGraph 与传统 Workflow 在状态管理和故障恢复上的差异。

子查询:
1. LangGraph 如何管理持久化状态?
2. 传统 Workflow 如何保存执行状态?
3. LangGraph 的 checkpoint 和故障恢复机制是什么?
4. 两种方案在可观测性与恢复粒度上的差异是什么?

拆分时要避免两个极端:子问题过少会遗漏信息,过多则会导致延迟、成本和上下文膨胀。一般先限制在 2~5 个,并要求每个子问题可独立检索。

6. 多轮对话改写

多轮 RAG 不应直接把完整聊天历史拼到 Query 中,否则无关信息会污染向量。更稳妥的做法是结合最近若干轮对话,把当前问题改写成一条脱离历史也能理解的独立问题

1
2
3
历史:用户正在比较 Qwen2.5-7B 与 Qwen2.5-14B 的显存占用。
当前:那量化以后呢?
独立 Query:Qwen2.5-7B 和 Qwen2.5-14B 在 4-bit 量化后的显存占用分别是多少?

改写器必须继承历史中的实体与约束,但不能擅自补充用户没有表达的条件。对话很长时,可只提供近期窗口、当前话题摘要和已确认实体,而不是全部原始消息。

四、不要让改写器自由发挥

一个可用于生产环境的基础 Prompt 可以这样设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
你是 RAG 检索查询改写器。请根据对话历史,把当前问题改写为可独立理解、适合知识库检索的查询。

要求:
1. 保留原问题的实体、时间、版本、范围、否定和比较条件;
2. 解析指代和省略,但不要添加未确认的事实;
3. 不要回答问题;
4. 如果原问题已经明确,保持原意并仅做必要调整;
5. 输出严格 JSON。

输出格式:
{
"standalone_query": "...",
"keywords": ["..."],
"filters": {},
"confidence": 0.0,
"need_clarification": false
}

在支持结构化输出的模型中,应直接约束 JSON Schema,而不是只靠“请输出 JSON”。filters 可进一步承载时间范围、产品版本、文档类型和租户权限等元数据条件。

五、生产级架构:先路由,再改写

最常见的错误,是任何 Query 都调用最重的改写链路。更合理的方案是先做轻量分类:

Query 类型推荐策略
明确的单一事实问题原 Query + 混合检索
口语化、术语不规范基础改写或多查询扩展
依赖对话历史独立问题改写
复杂比较、多目标问题子查询分解
原理解释、多跳推理原 Query + Step-back
短 Query 与长文档语义不匹配HyDE
编号、专有名词、错误码关键词检索 + 精确过滤

注意,最终重排序应优先使用原始问题计算相关性,防止改写后的 Query 把检索结果带偏。

六、三类核心风险

1. 查询漂移

LLM 可能把用户的范围改宽、改窄,或把不确定的指代直接猜成某个实体。

可采用以下约束:

  • 原 Query 与改写 Query 同时检索,而不是完全替换;
  • 计算两者的 Embedding 相似度,低于阈值时回退;
  • 对实体、数字、时间、版本号和否定词做前后校验;
  • 低置信度或存在多个候选实体时,向用户澄清;
  • 用原始问题对召回结果做 Cross-Encoder 重排序。

2. 延迟与成本

多查询、HyDE、重排序都会增加模型调用和检索次数。优化方式包括:

  • 使用小模型完成分类与基础改写;
  • 子查询并行检索;
  • 对规范化后的 Query 做缓存;
  • 限制生成数量、长度和超时时间;
  • 只有在首轮召回质量较低时,才触发二次改写;
  • 为简单 Query 提供零改写快速路径。

3. 召回增加,但噪声也增加

更多 Query 不等于更好。多路结果需要去重、融合和重排序,还要根据 token 预算控制最终上下文。如果只是盲目扩大 Top K,生成模型反而更容易被无关片段干扰。

七、如何评估查询改写是否有效

不能只观察最终回答看起来更好了。建议分层评估:

检索层

  • Recall@K:相关文档是否进入前 K 条;
  • MRR:第一个相关文档出现得有多靠前;
  • nDCG@K:多个相关文档的排序质量;
  • Hit Rate:是否至少命中一条相关文档;
  • Query Drift Rate:改写后丢失或改变关键约束的比例。

生成层

  • 答案正确性;
  • 对检索证据的忠实度;
  • 引用是否覆盖关键结论;
  • 完整性与拒答准确率。

系统层

  • P50 / P95 延迟;
  • 单次请求 token 与费用;
  • 平均检索次数;
  • 缓存命中率;
  • 触发澄清和回退的比例。

评测集应覆盖单轮、多轮、错别字、别名、否定、版本号、时间范围、复合问题和无答案问题。上线时使用 A/B 测试,比较“无改写”、“统一改写”和“路由式改写”,而不是只测试某一种 Prompt。

八、工程实践建议

  1. 先打好基础,再做高级改写。 如果分块、Embedding、权限过滤和混合检索本身有问题,Query 改写只能掩盖问题。
  2. 保留原始问题。 原 Query 用于回退、重排序、生成和审计,改写 Query 只服务于检索。
  3. 优先混合检索。 向量检索处理语义,BM25 处理实体、代码、编号和术语,两者互补。
  4. 结构化输出。 把查询文本、关键词、过滤条件和置信度分开,避免从自然语言中二次解析。
  5. 按场景路由。 基础改写、多查询、HyDE、Step-back 和分解解决的是不同问题。
  6. 建立可观测性。 记录原 Query、改写结果、路由类型、召回文档、融合分数、重排序结果及最终引用。
  7. 设置回退机制。 模型超时、结构化输出失败或相似度异常时,直接使用原 Query 检索。

九、总结

查询改写的本质,是在不改变用户意图的前提下,把人类表达转换成检索器更容易理解的表达。它不是单一 Prompt,而是一套包含意图识别、上下文补全、查询扩展、任务分解、结果融合、重排序和评估的系统工程。

对于多数生产级 RAG,可以从以下路径逐步演进:

1
2
3
4
5
6
原 Query
→ 独立问题改写
→ 向量 + BM25 混合检索
→ 多查询或子查询
→ RRF 融合 + Cross-Encoder 重排序
→ 基于评测数据做路由与动态回退

与其为每个请求都生成更多 Query,不如让系统先判断这个问题究竟缺了什么,再选择成本最低、最有针对性的改写策略。

参考资料