RAG 查询改写:让检索更精准的关键技术

RAG 查询改写:让检索更精准的关键技术

前言

RAG(Retrieval-Augmented Generation)是目前构建知识密集型 LLM 应用的主流架构。但在实际落地中,一个常被忽略的关键环节是 查询改写(Query Rewriting)

用户的原始提问往往是模糊的、碎片化的、缺乏上下文的。直接拿这样的 query 去检索,召回效果可想而知。查询改写的作用,就是在检索之前对用户 query 进行优化,让检索系统找到更相关的文档,从而提升生成质量。


一、为什么需要查询改写?

先看几个真实场景中的用户 query:

原始 Query 问题
怎么治 缺少主语------治什么?
它疼怎么办 代词指代不明------"它"是什么?
Python 怎么读取文件 太宽泛------open()pandas、还是 pathlib
刚才说的那个药能吃吗 依赖对话历史,独立检索完全失效

直接拿这些 query 去向量库做语义检索,结果往往是:召回一堆似是而非的内容,真正的答案淹没在噪声中

查询改写要解决三个核心问题:

  1. query 信息不足 → 补全缺失的实体和上下文
  2. query 粒度不匹配 → 拆解复杂问题,或合并过散的子问题
  3. 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 系统中"投入产出比"极高的一个优化点。不需要改动索引结构,不需要重新向量化文档库,只需要在检索前加一层改写逻辑,就能显著提升召回质量。

在落地时,建议分阶段演进:

  1. Phase 1:规则改写(代词消解 + 关键词补全)
  2. Phase 2:引入 LLM 改写(版本 A,效果立竿见影)
  3. Phase 3:按需引入 Multi-Query / HyDE(版本 B,针对复杂场景)
  4. Phase 4:AB 测试,数据驱动地选择改写策略

查询改写不是银弹,但它是每个 RAG 系统都应该做好的基本功。检索的质量决定了生成的天花板------垃圾进,垃圾出,在 RAG 中同样成立。