RAG查询策略

26 年 8 月 6 日 星期四
4166 字
21 分钟

为什么“搜什么”比“怎么搜”更重要?

做 RAG(Retrieval-Augmented Generation,检索增强生成)时,我们很容易把注意力放在向量数据库、Embedding 模型和大模型上,却忽略了最靠前的一步:用户的问题,通常并不是一个理想的检索词。

用户会省略上下文、使用口语、混合多个意图,也可能把症状当成原因。例如:

“升级之后一直转圈,怎么处理?”

这句话至少缺少产品、版本、页面、错误码和“升级”指什么。直接向量化当然也能搜,但检索器只能忠实地搜索一个信息不足的问题。

RAG查询策略:一个问题经过分解、多路检索与证据汇聚
查询不是一条直线,而是从原始问题出发,探索多条路径后汇聚成证据

一套成熟的 RAG 查询链路通常包含四个层次:

  1. 理解查询:补全上下文、识别意图和约束;
  2. 变换查询:改写、拆分、扩展或生成假设文档;
  3. 多路召回:让关键词、语义、元数据和不同粒度互相补位;
  4. 融合与重排:去重、合并、重排,最终留下少量可引用的证据。
从用户问题到最终证据的RAG查询流水线
先理解和变换查询,再多路召回,最后融合、重排并组装上下文

这篇文章不讨论文档如何清洗和切块,而是专注于查询发生之后、答案生成之前的这段链路。

先判断:这个问题需要哪种策略?

并不是每条查询都要经过所有策略。一个包含准确错误码的短查询,BM25 可能一次就能命中;一个需要对比三种方案的问题,则更适合先拆成子查询。

查询特征例子优先策略
指代、省略、依赖对话“那第二种怎么配置?”对话改写
表述口语化、与文档术语不同“接口老是卡住”多查询改写、稠密检索
包含错误码、型号、函数名“ORA-00942”BM25/稀疏检索
一个问题包含多个条件“比较 A、B 的成本和延迟”子查询分解
用户不知道准确术语“一种能让检索更准的二次排序”HyDE、查询扩展
问题过于具体,知识库只有概述“某版本某参数为什么失效”回溯检索
需要整章背景,又要精确证据“解释认证流程并给出配置项”多粒度检索

一个实用原则是:先用最便宜的路径,如果置信度不足,再逐步升级。 否则每个问题都调用多次 LLM、发起多路搜索,延迟和费用会迅速上涨。

查询改写:把“聊天问题”变成“可检索问题”

查询改写(Query Rewriting)不是简单换一种说法,而是把对话中的隐含信息显式化,同时保留原始意图。

假设对话是:

text
用户:Milvus 的 HNSW 和 IVF_FLAT 有什么区别?
助手:……
用户:那它的 ef 应该怎么调?

第二句单独进入知识库时,“它”没有明确指向。可以改写为:

text
Milvus HNSW 索引中的 ef 参数有什么作用,应该如何调节?

好的改写应遵循三个约束:

  • 自包含:脱离聊天记录也能理解;
  • 不擅自回答:只整理查询,不把模型猜测写进问题;
  • 保留关键字面量:版本号、错误码、专有名词、时间范围不能被“润色”掉。

可以让模型返回结构化结果,方便后续路由:

json
{
  "standalone_query": "Milvus HNSW索引中的ef参数有什么作用,应该如何调节?",
  "intent": "parameter_tuning",
  "keywords": ["Milvus", "HNSW", "ef"],
  "filters": { "product": "Milvus" }
}

多查询改写:用多种表达扩大召回面

单次改写追求“更准确”,多查询改写(Multi-Query)则用多个视角覆盖不同措辞。例如原问题是“如何降低 RAG 幻觉”:

text
Q1:RAG系统如何减少无依据回答?
Q2:检索增强生成中如何提高引用证据的一致性?
Q3:当召回内容不足时,怎样让LLM拒绝回答?

每个查询分别召回,再合并结果。它适合知识库术语不统一、用户表达与文档标题差异较大的场景。

风险也很明显:改写数量越多,召回越广,噪声和成本也越高。通常从 2~4 个查询开始评测,比固定生成十几个查询更稳妥。

子查询分解:不要用一次检索回答四个问题

复合问题常常包含多个独立的信息需求:

“比较 HNSW 和 IVF_FLAT 的原理、内存占用、查询参数,并说明百万级文本向量应该怎么选。”

直接检索时,排名靠前的文档可能只覆盖其中一个方面。子查询分解(Sub-query Decomposition)会把它拆为:

  1. HNSW 和 IVF_FLAT 的索引原理有什么不同?
  2. 两者的内存、构建成本和延迟如何比较?
  3. HNSW 的 ef 与 IVF_FLAT 的 nprobe 如何影响召回率?
  4. 百万级文本向量在什么约束下选择哪一种索引?

每个子查询独立召回证据,最后再按原问题组织答案。这样做的核心收益不是“搜得更多”,而是让每个检索任务的目标更单一。

分解时要避免两个极端:

  • 拆得太少:多个意图仍挤在同一条查询里;
  • 拆得太碎:丢失问题之间的约束关系,产生大量重复结果。

工程上可以为子查询设置最大数量,并要求模型输出 depends_on,保留依赖关系。对于“先找到版本,再查该版本配置”的问题,应串行执行;互不依赖的比较项则可以并行检索。

HyDE:先“假想答案”,再用答案去搜索

HyDE(Hypothetical Document Embeddings)是一种很有启发性的查询变换方法:先让 LLM 根据问题生成一段假设性文档,再对这段文档做 Embedding,用它检索真实知识库。

text
用户问题

LLM生成一段“可能的答案/文档”

对假设文档生成向量

检索与它语义相近的真实文档

为什么这可能有效?用户问题往往很短,而知识库 Chunk 通常是陈述句、说明文或操作步骤。假设文档在语言形态和信息密度上更接近被检索的内容,Embedding 更容易把它们放到相邻区域。

例如问题是“为什么召回结果看起来都差不多”,HyDE 可能生成包含“向量同质化、Chunk 重叠、去重、MMR、多样性”等术语的段落,从而找到用户没有说出口的相关文档。

但必须明确:HyDE 生成的内容不是证据。 它可能包含幻觉,只能作为检索探针。最终答案必须依据从知识库召回的真实内容。

HyDE 更适合:

  • 用户不知道专业术语;
  • 查询太短,语义信号不足;
  • 知识库内容是长段说明,和疑问句形态差异大。

它不一定适合精确实体、错误码和数字查询;这些场景通常应该保留原始查询,并让 BM25 同时参与召回。

回溯检索:从具体问题退回上位概念

回溯检索(Step-back Prompting / Step-back Retrieval)的思路与子查询不同。子查询把一个复杂问题横向拆开,回溯检索则把一个过于具体的问题向上抽象一层

例如:

text
原问题:为什么HNSW在过滤tenant_id后召回率突然下降?
回溯问题:向量近邻检索与标量过滤组合时,会如何影响候选集合和召回率?

原问题用于寻找精确案例,回溯问题用于寻找原理性材料。把两路证据合并后,模型既能获得针对性细节,也能获得解释现象所需的背景。

这对以下场景很有帮助:

  • 知识库只有原理文档,没有与用户一模一样的案例;
  • 问题带有很多偶然细节,向量搜索被细节“带偏”;
  • 答案需要先讲通用规律,再分析具体情况。

稀疏、稠密与混合检索

查询变换解决“搜什么”,检索算法解决“怎样匹配”。RAG 中最常见的是稀疏检索、稠密检索和两者结合的混合检索。

方式主要信号擅长容易错过
稀疏检索(如 BM25)词项重合、词频、逆文档频率错误码、型号、函数名、精确短语同义改写、语义相近但用词不同
稠密检索(Embedding)向量空间中的语义相似度自然语言、近义表达、跨措辞匹配冷门实体、数字、相似但不相关的段落
混合检索同时利用两类信号兼顾精确命中与语义召回需要融合、调参与评测
混合检索将稀疏与稠密结果融合并重排
原始查询同时进入BM25和向量检索,通过RRF融合后交给Reranker

用 RRF 融合不同分数体系

BM25 分数和向量相似度的量纲不同,直接相加往往不可靠。RRF(Reciprocal Rank Fusion,倒数排名融合)只使用名次:

RRF(d)=rR1k+rankr(d)\operatorname{RRF}(d)=\sum_{r\in R}\frac{1}{k+\operatorname{rank}_r(d)}

其中 RR 是多路结果列表,rankr(d)\operatorname{rank}_r(d) 是文档 dd 在第 rr 路中的排名,kk 是平滑常数,常见默认值为 60。

假设文档 A 在 BM25 中排第 1、向量检索中排第 4,那么:

RRF(A)=160+1+160+4\operatorname{RRF}(A)=\frac{1}{60+1}+\frac{1}{60+4}

一篇在多路检索中都靠前的文档,会获得稳定优势。RRF 简单、无需校准分数,是混合检索很实用的起点。

如果业务需要加权,可以让精确关键词查询偏向 BM25,让自然语言问题偏向向量检索;但权重应由离线评测决定,而不是凭直觉写死。

多粒度检索:小块找得准,大块读得懂

Chunk 越小,通常定位越精确,但上下文可能不完整;Chunk 越大,语义更连贯,却容易混入无关内容。多粒度检索的目标是同时获得二者的优点。

常见做法是“小块索引,大块返回”:

  1. 文档先按章节切成 Parent Chunk;
  2. Parent 再切成更小的 Child Chunk;
  3. 对 Child 建索引并执行精确召回;
  4. 命中后根据 parent_id 取回包含完整上下文的 Parent;
  5. 对 Parent 去重、裁剪,再交给 LLM。

还可以同时建立标题级、段落级、句子级索引,让查询根据意图动态选择。例如“某参数默认值是多少”优先搜索句子/段落,“解释某模块架构”则优先搜索章节摘要。

多粒度不是无条件返回整篇文档。父块过大仍会挤占上下文窗口,并稀释关键证据。更稳妥的方式是为父块设置长度上限,并保留命中子块在父块中的位置。

重排:召回负责不漏,Reranker负责排准

第一阶段检索追求高召回率,可以取 30~100 个候选;但这些内容不应该全部塞给 LLM。Reranker 会同时阅读查询和候选文本,重新估计二者的相关性,再保留少量结果。

典型的两阶段结构是:

text
BM25 / 向量 / 多查询召回  →  30~100个候选
Cross-Encoder或LLM重排    →  5~10个证据块

向量检索通常分别编码查询和文档,速度快;Cross-Encoder 把查询与文档放在一起编码,能捕捉更细致的交互,但计算更贵。因此“先召回、后重排”能兼顾速度和精度。

重排之后还需要:

  • 按文档 ID、内容指纹或相似度去重;
  • 使用 MMR 等方法控制结果多样性;
  • 检查租户、权限和时间过滤是否仍然生效;
  • 保留来源、页码、标题等引用信息;
  • 当最高分仍低于阈值时,拒绝回答或请求澄清。

一条可落地的自适应查询链路

如果把所有策略串起来,一个实用的实现可以是:

python
def retrieve(user_query: str, chat_history: list[dict]) -> list[Chunk]:
    query = rewrite_as_standalone(user_query, chat_history)
    route = classify_query(query)

    queries = [query]
    if route.is_multi_intent:
        queries.extend(decompose(query, max_queries=4))
    elif route.is_semantically_ambiguous:
        queries.extend(generate_query_variants(query, count=2))

    candidates = []
    for q in queries:
        candidates += bm25_search(q, top_k=30)
        candidates += vector_search(q, top_k=30)

    if route.should_use_hyde:
        hypothetical_doc = generate_hypothetical_document(query)
        candidates += vector_search(hypothetical_doc, top_k=30)

    fused = reciprocal_rank_fusion(candidates, k=60)
    parents = expand_to_parent_chunks(fused[:40])
    ranked = rerank(query, deduplicate(parents))
    return ranked[:8]

这只是结构示意。生产环境还应为每一步设置超时、缓存、降级和可观测性。例如查询改写超时,就退回原始查询;向量服务不可用,仍可用 BM25 给出有限结果。

推荐的渐进式建设顺序

不要第一天就把所有策略一次性堆上去。更容易验证的顺序是:

  1. 建立原始查询 + 单路检索的基线;
  2. 加入 BM25 与向量混合召回;
  3. 加入 RRF 和 Reranker;
  4. 对对话问题加入自包含改写;
  5. 根据失败案例选择子查询、HyDE 或回溯检索;
  6. 最后引入自适应路由与多粒度索引。

每增加一个模块,都应该回答一个具体问题:它修复了哪一类失败查询?带来了多少延迟和费用?如果说不清楚,这个模块很可能只是让系统变复杂。

如何评测查询策略?

只看最终回答“感觉不错”很难定位问题。应该把评测拆成检索层和生成层。

检索层指标

指标关注点
Recall@K标准答案所需证据是否进入前 K 个候选
Precision@K前 K 个候选中有多少真正相关
MRR第一个相关结果出现得是否足够靠前
nDCG@K多个结果的相关性等级与排序是否合理
Context Precision最终交给 LLM 的上下文是否精炼

生成层指标

  • 答案是否被检索证据支持;
  • 引用是否真的对应结论;
  • 证据不足时是否能够拒答;
  • 多意图问题是否覆盖完整;
  • 答案正确性、完整性与可读性是否达标。

评测集要包含真实的困难样本:错别字、口语、指代、精确编号、多跳问题、无答案问题和权限受限问题。只用“文档标题改写成问句”的合成数据,往往会高估系统效果。

还要记录每个阶段的中间结果:改写后的查询、每路 Top K、融合名次、重排分数和最终引用。否则答案出错时,很难判断是查询理解错了、召回漏了,还是 LLM 没有正确使用证据。

常见误区

1. 查询改写替换了原始查询

改写模型可能丢掉数字和专有名词。更安全的方式是保留原查询,让原查询与改写查询并行召回。

2. 召回越多,答案越可靠

无关上下文会分散模型注意力,甚至把错误信息带进答案。候选可以多,最终上下文必须经过重排、去重和预算控制。

3. HyDE生成的是参考答案

HyDE 文本没有经过知识库验证,只能帮助定位真实文档,不能作为引用来源。

4. 只调Top K,不建设评测集

top_k=5 还是 top_k=20 没有通用答案。Chunk 大小、索引、融合方式和 Reranker 都会影响结果,必须在同一评测集上比较。

5. 忘记权限过滤

查询扩展和多路召回会访问更多候选,但每一路都必须执行同样的租户和权限约束。不能先全库搜索,再指望生成阶段不泄露内容。

总结

RAG 查询不是“把用户问题转成向量”这么简单,它是一条从意图理解到证据选择的流水线:

  • 查询改写负责补全对话上下文;
  • 多查询与子查询负责扩大覆盖面、拆解复杂意图;
  • HyDE 用假设文档缩小问题与知识文本之间的表达差距;
  • 回溯检索同时寻找具体案例和上位原理;
  • 稀疏与稠密检索分别守住精确匹配和语义匹配;
  • 多粒度检索在定位精度与上下文完整性之间切换;
  • RRF、Reranker、去重和阈值控制最终证据质量。

真正有效的策略不是“全部开启”,而是从真实失败案例出发,用评测证明某个模块值得它带来的复杂度。先让查询可检索,再让召回不遗漏,最后让证据足够精确。

文章标题:RAG查询策略

文章作者:梦幻の风

文章链接:https://www.hstudent.xyz/posts/ai/rag%E6%9F%A5%E8%AF%A2%E7%AD%96%E7%95%A5[复制]

最后修改时间:


梦幻の风 梦幻の风

商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。
本文采用CC BY-NC-SA 4.0进行许可。