为什么“搜什么”比“怎么搜”更重要?
做 RAG(Retrieval-Augmented Generation,检索增强生成)时,我们很容易把注意力放在向量数据库、Embedding 模型和大模型上,却忽略了最靠前的一步:用户的问题,通常并不是一个理想的检索词。
用户会省略上下文、使用口语、混合多个意图,也可能把症状当成原因。例如:
“升级之后一直转圈,怎么处理?”
这句话至少缺少产品、版本、页面、错误码和“升级”指什么。直接向量化当然也能搜,但检索器只能忠实地搜索一个信息不足的问题。

一套成熟的 RAG 查询链路通常包含四个层次:
- 理解查询:补全上下文、识别意图和约束;
- 变换查询:改写、拆分、扩展或生成假设文档;
- 多路召回:让关键词、语义、元数据和不同粒度互相补位;
- 融合与重排:去重、合并、重排,最终留下少量可引用的证据。
这篇文章不讨论文档如何清洗和切块,而是专注于查询发生之后、答案生成之前的这段链路。
先判断:这个问题需要哪种策略?
并不是每条查询都要经过所有策略。一个包含准确错误码的短查询,BM25 可能一次就能命中;一个需要对比三种方案的问题,则更适合先拆成子查询。
| 查询特征 | 例子 | 优先策略 |
|---|---|---|
| 指代、省略、依赖对话 | “那第二种怎么配置?” | 对话改写 |
| 表述口语化、与文档术语不同 | “接口老是卡住” | 多查询改写、稠密检索 |
| 包含错误码、型号、函数名 | “ORA-00942” | BM25/稀疏检索 |
| 一个问题包含多个条件 | “比较 A、B 的成本和延迟” | 子查询分解 |
| 用户不知道准确术语 | “一种能让检索更准的二次排序” | HyDE、查询扩展 |
| 问题过于具体,知识库只有概述 | “某版本某参数为什么失效” | 回溯检索 |
| 需要整章背景,又要精确证据 | “解释认证流程并给出配置项” | 多粒度检索 |
一个实用原则是:先用最便宜的路径,如果置信度不足,再逐步升级。 否则每个问题都调用多次 LLM、发起多路搜索,延迟和费用会迅速上涨。
查询改写:把“聊天问题”变成“可检索问题”
查询改写(Query Rewriting)不是简单换一种说法,而是把对话中的隐含信息显式化,同时保留原始意图。
假设对话是:
用户:Milvus 的 HNSW 和 IVF_FLAT 有什么区别?
助手:……
用户:那它的 ef 应该怎么调?第二句单独进入知识库时,“它”没有明确指向。可以改写为:
Milvus HNSW 索引中的 ef 参数有什么作用,应该如何调节?好的改写应遵循三个约束:
- 自包含:脱离聊天记录也能理解;
- 不擅自回答:只整理查询,不把模型猜测写进问题;
- 保留关键字面量:版本号、错误码、专有名词、时间范围不能被“润色”掉。
可以让模型返回结构化结果,方便后续路由:
{
"standalone_query": "Milvus HNSW索引中的ef参数有什么作用,应该如何调节?",
"intent": "parameter_tuning",
"keywords": ["Milvus", "HNSW", "ef"],
"filters": { "product": "Milvus" }
}多查询改写:用多种表达扩大召回面
单次改写追求“更准确”,多查询改写(Multi-Query)则用多个视角覆盖不同措辞。例如原问题是“如何降低 RAG 幻觉”:
Q1:RAG系统如何减少无依据回答?
Q2:检索增强生成中如何提高引用证据的一致性?
Q3:当召回内容不足时,怎样让LLM拒绝回答?每个查询分别召回,再合并结果。它适合知识库术语不统一、用户表达与文档标题差异较大的场景。
风险也很明显:改写数量越多,召回越广,噪声和成本也越高。通常从 2~4 个查询开始评测,比固定生成十几个查询更稳妥。
子查询分解:不要用一次检索回答四个问题
复合问题常常包含多个独立的信息需求:
“比较 HNSW 和 IVF_FLAT 的原理、内存占用、查询参数,并说明百万级文本向量应该怎么选。”
直接检索时,排名靠前的文档可能只覆盖其中一个方面。子查询分解(Sub-query Decomposition)会把它拆为:
- HNSW 和 IVF_FLAT 的索引原理有什么不同?
- 两者的内存、构建成本和延迟如何比较?
- HNSW 的
ef与 IVF_FLAT 的nprobe如何影响召回率? - 百万级文本向量在什么约束下选择哪一种索引?
每个子查询独立召回证据,最后再按原问题组织答案。这样做的核心收益不是“搜得更多”,而是让每个检索任务的目标更单一。
分解时要避免两个极端:
- 拆得太少:多个意图仍挤在同一条查询里;
- 拆得太碎:丢失问题之间的约束关系,产生大量重复结果。
工程上可以为子查询设置最大数量,并要求模型输出 depends_on,保留依赖关系。对于“先找到版本,再查该版本配置”的问题,应串行执行;互不依赖的比较项则可以并行检索。
HyDE:先“假想答案”,再用答案去搜索
HyDE(Hypothetical Document Embeddings)是一种很有启发性的查询变换方法:先让 LLM 根据问题生成一段假设性文档,再对这段文档做 Embedding,用它检索真实知识库。
用户问题
↓
LLM生成一段“可能的答案/文档”
↓
对假设文档生成向量
↓
检索与它语义相近的真实文档为什么这可能有效?用户问题往往很短,而知识库 Chunk 通常是陈述句、说明文或操作步骤。假设文档在语言形态和信息密度上更接近被检索的内容,Embedding 更容易把它们放到相邻区域。
例如问题是“为什么召回结果看起来都差不多”,HyDE 可能生成包含“向量同质化、Chunk 重叠、去重、MMR、多样性”等术语的段落,从而找到用户没有说出口的相关文档。
但必须明确:HyDE 生成的内容不是证据。 它可能包含幻觉,只能作为检索探针。最终答案必须依据从知识库召回的真实内容。
HyDE 更适合:
- 用户不知道专业术语;
- 查询太短,语义信号不足;
- 知识库内容是长段说明,和疑问句形态差异大。
它不一定适合精确实体、错误码和数字查询;这些场景通常应该保留原始查询,并让 BM25 同时参与召回。
回溯检索:从具体问题退回上位概念
回溯检索(Step-back Prompting / Step-back Retrieval)的思路与子查询不同。子查询把一个复杂问题横向拆开,回溯检索则把一个过于具体的问题向上抽象一层。
例如:
原问题:为什么HNSW在过滤tenant_id后召回率突然下降?
回溯问题:向量近邻检索与标量过滤组合时,会如何影响候选集合和召回率?原问题用于寻找精确案例,回溯问题用于寻找原理性材料。把两路证据合并后,模型既能获得针对性细节,也能获得解释现象所需的背景。
这对以下场景很有帮助:
- 知识库只有原理文档,没有与用户一模一样的案例;
- 问题带有很多偶然细节,向量搜索被细节“带偏”;
- 答案需要先讲通用规律,再分析具体情况。
稀疏、稠密与混合检索
查询变换解决“搜什么”,检索算法解决“怎样匹配”。RAG 中最常见的是稀疏检索、稠密检索和两者结合的混合检索。
| 方式 | 主要信号 | 擅长 | 容易错过 |
|---|---|---|---|
| 稀疏检索(如 BM25) | 词项重合、词频、逆文档频率 | 错误码、型号、函数名、精确短语 | 同义改写、语义相近但用词不同 |
| 稠密检索(Embedding) | 向量空间中的语义相似度 | 自然语言、近义表达、跨措辞匹配 | 冷门实体、数字、相似但不相关的段落 |
| 混合检索 | 同时利用两类信号 | 兼顾精确命中与语义召回 | 需要融合、调参与评测 |
用 RRF 融合不同分数体系
BM25 分数和向量相似度的量纲不同,直接相加往往不可靠。RRF(Reciprocal Rank Fusion,倒数排名融合)只使用名次:
其中 是多路结果列表, 是文档 在第 路中的排名, 是平滑常数,常见默认值为 60。
假设文档 A 在 BM25 中排第 1、向量检索中排第 4,那么:
一篇在多路检索中都靠前的文档,会获得稳定优势。RRF 简单、无需校准分数,是混合检索很实用的起点。
如果业务需要加权,可以让精确关键词查询偏向 BM25,让自然语言问题偏向向量检索;但权重应由离线评测决定,而不是凭直觉写死。
多粒度检索:小块找得准,大块读得懂
Chunk 越小,通常定位越精确,但上下文可能不完整;Chunk 越大,语义更连贯,却容易混入无关内容。多粒度检索的目标是同时获得二者的优点。
常见做法是“小块索引,大块返回”:
- 文档先按章节切成 Parent Chunk;
- Parent 再切成更小的 Child Chunk;
- 对 Child 建索引并执行精确召回;
- 命中后根据
parent_id取回包含完整上下文的 Parent; - 对 Parent 去重、裁剪,再交给 LLM。
还可以同时建立标题级、段落级、句子级索引,让查询根据意图动态选择。例如“某参数默认值是多少”优先搜索句子/段落,“解释某模块架构”则优先搜索章节摘要。
多粒度不是无条件返回整篇文档。父块过大仍会挤占上下文窗口,并稀释关键证据。更稳妥的方式是为父块设置长度上限,并保留命中子块在父块中的位置。
重排:召回负责不漏,Reranker负责排准
第一阶段检索追求高召回率,可以取 30~100 个候选;但这些内容不应该全部塞给 LLM。Reranker 会同时阅读查询和候选文本,重新估计二者的相关性,再保留少量结果。
典型的两阶段结构是:
BM25 / 向量 / 多查询召回 → 30~100个候选
Cross-Encoder或LLM重排 → 5~10个证据块向量检索通常分别编码查询和文档,速度快;Cross-Encoder 把查询与文档放在一起编码,能捕捉更细致的交互,但计算更贵。因此“先召回、后重排”能兼顾速度和精度。
重排之后还需要:
- 按文档 ID、内容指纹或相似度去重;
- 使用 MMR 等方法控制结果多样性;
- 检查租户、权限和时间过滤是否仍然生效;
- 保留来源、页码、标题等引用信息;
- 当最高分仍低于阈值时,拒绝回答或请求澄清。
一条可落地的自适应查询链路
如果把所有策略串起来,一个实用的实现可以是:
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 给出有限结果。
推荐的渐进式建设顺序
不要第一天就把所有策略一次性堆上去。更容易验证的顺序是:
- 建立原始查询 + 单路检索的基线;
- 加入 BM25 与向量混合召回;
- 加入 RRF 和 Reranker;
- 对对话问题加入自包含改写;
- 根据失败案例选择子查询、HyDE 或回溯检索;
- 最后引入自适应路由与多粒度索引。
每增加一个模块,都应该回答一个具体问题:它修复了哪一类失败查询?带来了多少延迟和费用?如果说不清楚,这个模块很可能只是让系统变复杂。
如何评测查询策略?
只看最终回答“感觉不错”很难定位问题。应该把评测拆成检索层和生成层。
检索层指标
| 指标 | 关注点 |
|---|---|
| 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、去重和阈值控制最终证据质量。
真正有效的策略不是“全部开启”,而是从真实失败案例出发,用评测证明某个模块值得它带来的复杂度。先让查询可检索,再让召回不遗漏,最后让证据足够精确。