RAG 查询改写:让检索更精准的关键技术
前言
RAG(Retrieval-Augmented Generation)是目前构建知识密集型 LLM 应用的主流架构。但在实际落地中,一个常被忽略的关键环节是 查询改写(Query Rewriting)。
用户的原始提问往往是模糊的、碎片化的、缺乏上下文的。直接拿这样的 query 去检索,召回效果可想而知。查询改写的作用,就是在检索之前对用户 query 进行优化,让检索系统找到更相关的文档,从而提升生成质量。
一、为什么需要查询改写?
先看几个真实场景中的用户 query:
| 原始 Query | 问题 |
|---|---|
怎么治 |
缺少主语------治什么? |
它疼怎么办 |
代词指代不明------"它"是什么? |
Python 怎么读取文件 |
太宽泛------open()、pandas、还是 pathlib? |
刚才说的那个药能吃吗 |
依赖对话历史,独立检索完全失效 |
直接拿这些 query 去向量库做语义检索,结果往往是:召回一堆似是而非的内容,真正的答案淹没在噪声中。
查询改写要解决三个核心问题:
- query 信息不足 → 补全缺失的实体和上下文
- query 粒度不匹配 → 拆解复杂问题,或合并过散的子问题
- query 表达偏差 → 修正术语,适配索引的语义空间
二、常见的查询改写策略
2.1 基于规则的改写
最简单的方案,适合模式固定的场景。
python
def rewrite_by_rules(query: str, history: list[str]) -> str:
# 代词消解:用上一轮的核心实体替换代词
pronouns = {"它", "他", "她", "这", "那个", "这个"}
if any(p in query for p in pronouns) and history:
last_entity = extract_core_entity(history[-1])
for p in pronouns:
if p in query:
query = query.replace(p, last_entity)
return query
优点 :零延迟,高确定性
缺点:覆盖有限,无法处理复杂语义
2.2 基于 LLM 的改写
用小模型(如 Qwen-2.5-7B、GPT-4o-mini)对 query 进行重写,是目前最主流的方法。
python
rewrite_prompt = """你是一个查询改写助手。根据对话历史,将用户的最后一轮提问改写成独立、完整、清晰的检索查询。
要求:
1. 补全所有代词指代
2. 补充缺失的上下文实体
3. 保持原意不变
4. 输出仅包含改写后的查询,不要多余内容
对话历史:
{history}
用户提问:{query}
改写后的查询:"""
def rewrite_by_llm(query: str, history: list[str]) -> str:
prompt = rewrite_prompt.format(
history="\n".join(history[-3:]),
query=query
)
return llm_client.chat(prompt)
优点 :泛化能力强,能处理复杂语义
缺点:增加一次 LLM 调用延迟,小模型改写质量不稳定
2.3 多查询扩展(Multi-Query)
同一个问题生成多个不同角度的查询,分别检索后合并结果。核心思想是:与其依赖一次完美的改写,不如从多个维度包围目标文档。
python
multi_query_prompt = """生成 {n} 个不同角度的检索查询,覆盖以下维度:
1. 同义表达:换个说法描述同样的问题
2. 子问题拆分:将复杂问题拆成更具体的子问题
3. 关键词聚焦:提取核心关键词的查询
原始问题:{query}
请输出 {n} 个查询,每行一个:"""
def multi_query_retrieve(query: str, n: int = 3):
queries = llm_client.chat(multi_query_prompt.format(n=n, query=query)).split("\n")
all_docs = []
for q in queries:
all_docs.extend(vector_store.search(q, top_k=5))
# 合并去重
return deduplicate(all_docs)
优点 :显著提升召回率
缺点:检索开销翻倍,需要去重和重排序
2.4 HyDE(Hypothetical Document Embeddings)
反过来思考:不改写 query,而是让 LLM 先根据 query 生成一段假设的理想文档,然后用这段文档的向量去检索。
python
hyde_prompt = """根据以下问题,写一段能回答该问题的专业文档片段。
要求:内容详实、格式规范、使用专业术语。
问题:{query}
文档片段:"""
def hyde_retrieve(query: str):
hypothetical_doc = llm_client.chat(hyde_prompt.format(query=query))
# 用生成的文档向量去检索
return vector_store.search(hypothetical_doc, top_k=10)
直觉:query 和文档之间的语义 gap 可能较大(比如 query 很短),但"假设文档"和真实文档的语义空间更接近。在某些场景下(如医疗、法律)效果显著。
缺点:如果 LLM 生成的假设文档偏离事实,检索反而会被误导。
2.5 查询分解(Query Decomposition)
针对复杂问题,先拆解成多个子问题,逐个检索后再综合回答。
原始问题:"高血压患者能否同时服用布洛芬和氨氯地平?"
↓ 分解
子问题1:"布洛芬和氨氯地平的药物相互作用"
子问题2:"高血压患者服用布洛芬的禁忌"
子问题3:"氨氯地平的作用机制和注意事项"
python
def decompose_and_retrieve(query: str):
sub_queries = llm_client.chat(
f"将以下问题拆解为2-4个独立的子问题:\n{query}"
).split("\n")
docs = []
for sq in sub_queries:
docs.extend(vector_store.search(sq, top_k=3))
return deduplicate(docs)
三、改写策略的选择矩阵
| 策略 | 延迟开销 | 召回提升 | 适用场景 |
|---|---|---|---|
| 规则改写 | 几乎为零 | 低 | 代词消解、术语统一 |
| LLM 改写 | 1次 LLM 调用 | 中 | 通用场景,历史上下文补全 |
| Multi-Query | 1次 LLM + N 次检索 | 高 | 对召回率要求极高的场景 |
| HyDE | 1次 LLM + 1次检索 | 中高 | query 极短或表达不规范的场景 |
| 查询分解 | 1次 LLM + N 次检索 | 高 | 复杂多跳问题 |
四、实际落地中的注意事项
4.1 改写不是万能的
改写能提升召回上限,但无法解决索引本身的质量问题。先确保文档切分和质量,再优化检索策略,顺序不能反。
4.2 权衡延迟与效果
一次 LLM 改写增加约 200ms-1s 的延迟(取决于模型)。在延迟敏感的场景下,可以考虑:
- 用更小的模型(如 Qwen-2.5-1.5B)
- 只在 query 长度小于阈值时触发改写
- 改写和检索并行执行(推测性检索)
4.3 改写结果的验证
在系统上线后,建议对改写结果进行采样评估:
原始 query: "它多少钱?"
改写结果: "苹果iPhone 15 Pro Max多少钱?" ✓
改写结果: "它多少钱?" ✗ (未改写)
改写结果: "香蕉多少钱一斤?" ✗ (改写错误)
可以建立人工评估集,定期跟踪改写准确率。
4.4 改写与多轮对话
多轮对话中的查询改写最具挑战性。关键点:
- 滑动窗口:不要使用全部历史,通常保留最近 3-5 轮即可
- 压缩策略:历史过长时,先用 LLM 压缩成摘要再用于改写
- 意图检测:如果用户开启了新话题("换个问题"),清空历史
五、总结
查询改写是 RAG 系统中"投入产出比"极高的一个优化点。不需要改动索引结构,不需要重新向量化文档库,只需要在检索前加一层改写逻辑,就能显著提升召回质量。
在落地时,建议分阶段演进:
- Phase 1:规则改写(代词消解 + 关键词补全)
- Phase 2:引入 LLM 改写(版本 A,效果立竿见影)
- Phase 3:按需引入 Multi-Query / HyDE(版本 B,针对复杂场景)
- Phase 4:AB 测试,数据驱动地选择改写策略
查询改写不是银弹,但它是每个 RAG 系统都应该做好的基本功。检索的质量决定了生成的天花板------垃圾进,垃圾出,在 RAG 中同样成立。