本文是 RAG 系列博客的第二篇。第一篇我们详细拆解了索引阶段(文档加载 →文档切分 → embedding向量化 → 向量存储),本篇将聚焦于查询与生成阶段,涵盖查询理解、检索策略、重排序、Prompt 构造以及生成优化等核心环节。建议先阅读第一篇再继续本文。
1. 回顾与引入
在第一篇中,我们把 RAG 比作“给大模型配一个专属图书馆管理员”,并完成了第一步——建馆藏:把海量文档切碎、向量化、存入向量数据库,构建了一个可语义检索的知识索引。
现在,读者(用户)带着问题来了。第二阶段的任务就是:馆员(RAG 系统)如何快速准确地找到最相关的几页资料,并整理成一份条理清晰的摘要交给大模型(专家)阅读,最终由专家给出有理有据的回答。
整个第二阶段可以拆解为四个核心步骤:
用户问题 → [查询理解与改写] → [向量检索] → [重排序] → [Prompt 构造] → [大模型生成] → 最终回答 ↑ ↑ ↑ ↑ (在线实时) (相似度匹配) (精排过滤) (组装上下文)下面我们逐一深挖每个环节的技术细节、常见陷阱和最佳实践。
2. 查询理解与优化(Query Understanding & Enhancement)
第二阶段的第一步,不是直接去向量库检索,而是先审视和优化用户的问题。原因很简单:用户的问题往往是口语化、模糊、甚至带有歧义的,直接用原始问题去检索,结果可能差强人意。
2.1 问题改写(Query Rewriting)
场景:用户问“苹果昨天股价怎么样?”——这里的“苹果”可能指公司 Apple Inc.,也可能指水果。如果直接检索,可能会召回关于水果价格的文档。
解决方案:在检索之前,利用大模型对原始问题进行改写或消歧,生成一个更清晰、更具体的检索查询。
常见改写策略包括:
| 策略 | 说明 | 示例 |
|---|---|---|
| 多查询(Multi-Query) | 从不同角度生成多个同义变体,分别检索后合并结果。 | 原始:“如何提高睡眠质量?” → 生成:“改善睡眠的方法”、“失眠怎么办”、“助眠技巧” |
| 假设文档生成(HyDE) | 先让大模型生成一个假设性的答案文档,然后用这个“假答案”去检索真实文档。 | 问题:“什么是 Transformer 中的自注意力机制?” → 先生成一段解释文字,再用该文字去向量库召回更精准的技术文档。 |
| 上下文补全 | 结合对话历史,将指代不明的代词替换为具体实体。 | 历史:“我们讨论了 GPT-4。” 当前:“它的参数量是多少?” → 改写为“GPT-4 的参数量是多少?” |
📌 实战经验:HyDE 在开放域问答中效果显著,但在事实性极强的场景(如法律、医疗)中需要谨慎使用,因为生成的“假答案”可能引入误导性信息。
2.2 查询扩展(Query Expansion)
除了改写,还可以对查询进行扩展——即在原始关键词基础上补充同义词、相关词或上下位词,提升召回率。
传统方法:基于 WordNet、同义词词典进行扩展。
现代方法:利用 LLM 生成相关术语,或利用知识图谱(如 Wikidata)进行实体链接和扩展。
例如,用户搜索“治疗感冒的方法”,系统自动扩展为“感冒 治疗 药物 休息 食疗 退烧”。
3. 检索策略(Retrieval Strategies)
优化完查询后,就要真正去知识库中“找书”了。检索并非只有“向量检索”一条路,实际工程中往往会结合多种检索方式,以达到最佳效果。
3.1 纯向量检索
这是最基础的方案:将优化后的查询文本通过 Embedding 模型转为向量,然后在向量数据库中执行 ANN(近似最近邻)搜索,召回 Top-K 个最相似的文档片段。
优点:支持语义匹配,能召回同义词、隐含语义相关的文档。
缺点:对长尾实体、罕见专有名词、精确数字/ID 的匹配能力弱(例如搜索“型号 XF-2000”,向量检索可能不如关键词匹配准确)。
3.1.1稠密向量检索
defencode_query(model,query):"""使用 BGE-M3 模型同时生成查询的稠密向量和稀疏向量"""vectors=model.encode([query],return_dense=True,return_sparse=True)# 提取列表中的第一个(也是唯一一个)元素的向量dense_vec=vectors["dense_vecs"][0]sparse_vec=vectors["lexical_weights"][0]returndense_vec,sparse_vec# 5. 稠密向量查询defdense_vector_search_example(client,query:str,limit:int=5):model=BGEM3FlagModel(r"D:/bge/bge-m3")# 将用户的提问与模型一并传入该方法进行向量化(将用户的提问进行向量化),生成稠密向量dense_vec,_=encode_query(model,query)# 用户的提问(提示词)已经向量化了,然后去查询向量数据库,返回相似度匹配的chunk(文档块)# 这样返回的文档块还需进行重排(因为还有稀疏向量的查询),也就是说,我们需要将稠密和稀疏做混合查询后再经过reranker(重排模型)进行排序# 这样返回的结果才是最终的结果results=client.search(collection_name="demo_collection",data=[dense_vec],anns_field="vector",limit=5,search_params={"metric_type":"L2"},output_fields=["id","text","metadata"])returnresults[0]3.1.2 稀疏向量检索
# 6. 稀疏向量查询defsparse_vector_search_example(client,query:str,limit:int=5):model=BGEM3FlagModel(r"D:/bge/bge-m3")_,sparse_vec=encode_query(model,query)results=client.search(collection_name="demo_collection",data=[sparse_vec],anns_field="sparse_vector",limit=limit,search_params={"metric_type":"IP"},output_fields=["id","text","metadata"],)returnresults[0]3.2 关键词检索(BM25 / TF-IDF)
传统的倒排索引检索,基于词频和逆文档频率进行匹配。
优点:精确匹配能力强,对专有名词、编号、日期等非常友好。
缺点:无法理解语义同义关系,召回“汽车”相关文档时不会召回“轿车”。
defscalar_query_examples(client,keyword:str="大模型"):# 对text字段进行模糊检索like_res=client.query(collection_name="demo_collection",filter=f'text like "%{keyword}%"',output_fields=["id","text"],limit=5,)# 对metadata的JSON字段进行检索json_res=client.query(collection_name="demo_collection",filter='metadata["source"] like "%sample%"',output_fields=["id","metadata"],limit=5,)3.3 混合检索(Hybrid Search)
主流生产级方案:同时执行向量检索和关键词检索,然后将两路结果通过某种方式合并(如加权求和、RRF(倒数排名融合,Reciprocal Rank Fusion)等算法),得到最终的候选集。
RRF 融合公式(最常用):
Score(d) = Σ_{r ∈ 检索器} 1 / (k + rank_r(d))其中 k 通常取 60,rank_r(d) 是文档 d 在第 r 个检索器中的排名。这个公式无需归一化不同检索器的分数,简单且效果稳定。
# 混合检索与重排序defhybrid_vector_search_example_rrf(client,query:str,limit:int=5):model=BGEM3FlagModel(r"D:/bge/bge-m3")dense_vec,sparse_vec=encode_query(model,query)dense_req=AnnSearchRequest(data=[dense_vec],anns_field="vector",param={"metric_type":"COSINE"},limit=limit,)sparse_req=AnnSearchRequest(data=[sparse_vec],anns_field="sparse_vector",param={"metric_type":"IP"},limit=limit,)results=client.hybrid_search(collection_name="demo_collection",reqs=[dense_req,sparse_req],ranker=RRFRanker(k=60),limit=limit,output_fields=["id","text","metadata"],)returnresults[0]✅ 最佳实践:混合检索 + RRF 融合是目前 RAG 生产环境中最稳妥的检索基线方案,兼顾了语义泛化和精确匹配。
3.4 多路检索(Multi-Path Retrieval)
更进一步,可以设计多条检索路径,例如:
路径一:向量检索(语义路径)
路径二:BM25 检索(关键词路径)
路径三:时间衰减排序(针对时效性敏感的问题,近期文档加分)
路径四:知识图谱检索(针对实体关系明确的问题)
最终将多路结果融合,最大程度保证召回率。
4. 重排序(Reranking)
检索阶段召回的 Top-K 个候选(比如 K=50)中,排名靠前的未必最相关。这是因为:
向量检索的 ANN 算法追求效率,牺牲了一定的精度;
混合检索融合后的排序可能不够精确;
向量相似度是“粗粒度”的语义匹配,无法精准判断哪个片段最有助于回答具体问题。
重排序的作用就是:用一个更强大、更精细的模型对候选集合重新打分排序,选出最相关的 Top-N 个(例如 N=5)交给大模型。
4.1 交叉编码器(Cross-Encoder)
重排序的核心是交叉编码器模型,它与双编码器(即常规 Embedding 模型)的区别在于:
| 类型 | 结构 | 输入 | 输出 | 特点 |
|---|---|---|---|---|
| 双编码器(Bi-Encoder) | 两个独立的编码器 | (查询, 文档) 分别编码 | 两个独立向量,计算相似度 | 速度快,可离线建索引,但精度相对低 |
| 交叉编码器(Cross-Encoder) | 单个联合编码器 | (查询, 文档) 拼接后一起输入 | 单个相似度分数(0~1) | 精度高,但每次计算需在线推理,耗时长 |
重排序的做法是:将查询与每个候选文档拼接成 [CLS] 查询 [SEP] 文档 [SEP] 的形式,送入 Cross-Encoder,得到精细化的相关性分数,然后按新分数重新排序。
主流模型:
开源:BGE-reranker、Cohere Rerank、Sentence-Transformers 中的 cross-encoder 系列
商业 API:Cohere Rerank API(效果优秀且易用)
4.2 重排序的工程权衡
性能问题:Cross-Encoder 推理较慢。如果候选集是 50,N=5,则每请求需 50 次推理。可通过 GPU 批处理加速,或使用更轻量级的 reranker 模型。
时机选择:重排序应发生在所有检索路径合并之后,对最终候选集做一次精细筛选。
经验法则:检索阶段召回 50~100 个候选,重排序后取前 5~10 个送入大模型。这样既保证了召回率,又控制了上下文长度。
5. Prompt 构造(Prompt Construction)
重排序完成后,我们获得了最相关的若干文档片段。接下来的任务就是把这些片段和原始问题组合成一份高质量的提示词(Prompt),喂给大模型。
5.1 基础模板
最经典的 Prompt 模板如下:
你是一个乐于助人的助手。请根据以下参考资料回答用户的问题。 如果参考资料中不包含相关信息,请明确告知“我无法从提供的资料中找到答案”, 不要编造信息。 参考资料: [1] {chunk_1} [2] {chunk_2} ... [N] {chunk_N} 问题:{user_query} 请给出准确、有条理的回答:5.2 高级构造技巧
| 技巧 | 说明 | 示例 |
|---|---|---|
| 引用标注 | 要求模型在回答中标注信息来源,增强可溯源性。 | “根据参考资料[2]中的说明,该产品的保修期为 12 个月。” |
| 去重与压缩 | 多个检索片段可能存在内容重叠,使用 LLM 进行初步压缩或摘要,减少上下文冗余。 | 对 5 个片段各生成一句摘要,再拼接。 |
| 条件性指令 | 告诉模型哪些情况下应该拒绝回答(如资料不足、信息矛盾)。 | “如果参考资料之间存在矛盾,请指出矛盾点,不要强行统一。” |
| 角色扮演(Role Prompting) | 根据业务场景设定角色,如“你是一位法律顾问”、“你是一位客服经理”,影响回答风格。 | |
| Few-shot 示例 | 在 Prompt 中加入 1-2 个问答示例,引导模型按期望格式输出。 | 适用于结构化输出场景(如 JSON、表格)。 |
5.3 上下文长度管理
即使现代 LLM(如 GPT-4、Claude)支持 100K+ 的上下文窗口,也不建议把所有检索片段都塞进去,原因有三:
成本:输入 Token 越多,成本越高(尤其是 API 调用)。
性能:长上下文会增加推理延迟。
“中间迷失”问题:研究表明,大模型对长上下文中间位置的信息关注度会显著下降(Lost-in-the-Middle 现象)。因此,最相关的片段应放在 Prompt 的开头或结尾,而非中间。
📌 推荐策略:将最相关的 1-2 个片段放在开头,次相关的放在结尾,其余放在中间——但这与“按相似度降序排列”的直觉相反,需要特别注意。
6. 生成与输出(Generation & Output)
大模型接收到构造好的 Prompt 后,进行推理生成,输出最终的回答。这一步看似“黑盒”,但仍有不少可以优化的空间。
6.1 生成参数调优
6.2 流式输出(Streaming)
对于需要实时交互的应用(如聊天机器人),建议启用流式输出(SSE/WebSocket),让用户逐字看到回答生成过程,显著提升体验。
6.3 引用与置信度
生产级 RAG 系统通常会额外输出:
引用来源:回答中每个观点对应的参考资料编号或文档链接。
置信度评分:模型对自己回答的信心程度(可用 Token 概率或单独训练一个置信度判别器)。
当置信度低于阈值时,系统可以主动触发“无法回答”或“转人工”流程。
7. 进阶优化:RAG 的“自我进化”
除了上述线性流程,还有一些更高阶的优化策略,值得有追求的团队探索:
7.1 自反思 RAG(Self-RAG)
在生成过程中,模型会插入特殊的 反思 Token(Reflection Token),对自己的输出进行自我评估(如“这个信息是否有据可查?”“我是否遗漏了什么?”),并据此决定是否需要再次检索或修正回答。这种方法能显著提升回答的准确性和完整性。
7.2 自适应检索(Adaptive Retrieval)
不是所有问题都需要检索。系统先判断问题的类型和复杂度:
简单事实性问答(如“今天星期几?”)→ 直接由 LLM 回答,无需检索。
复杂知识型问题(如“最新款 iPhone 的处理器型号?”)→ 触发检索。
这样可以节省成本并降低延迟。判断器可以是一个轻量级分类模型,也可以是一个基于 LLM 的 Router。
7.3 迭代检索(Iterative Retrieval)
对于复杂问题,可以执行多轮“检索→生成→再检索→再生成”的循环。例如,模型先生成一份初步回答,从中提取出新的关键词或实体,再进行第二轮检索补充信息,最终生成更完善的答案。典型实现如 ReAct 模式。
8. 完整流程总览
至此,整个 RAG 第二阶段的全貌已经展开。我们用一张流程图做最终总结:
┌─────────────────────┐ │ 用户输入问题 │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 查询理解与优化 │ │ · 改写 / 扩展 │ │ · HyDE / Multi-Query│ └──────────┬──────────┘ ▼ ┌────────────────────┼────────────────────┐ ▼ ▼ ▼ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ 向量检索 │ │ BM25检索 │ │ 其他路径 │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └────────────────────┼────────────────────┘ ▼ ┌─────────────────────┐ │ 结果融合(RRF) │ │ 候选集 K=50 │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 重排序(Rerank) │ │ Cross-Encoder │ │ 精选 Top-N=5 │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ Prompt 构造 │ │ · 引用标注 │ │ · 去重压缩 │ │ · 角色/指令设定 │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 大模型生成 │ │ · 参数调优 │ │ · 流式输出 │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 最终回答 + 引用溯源 │ └─────────────────────┘9. 总结
在本文中,我们深入剖析了 RAG 第二阶段——查询与生成阶段的全链路优化:
查询理解与优化:通过改写、扩展、HyDE 等方式,让检索“问对问题”。
检索策略:混合检索(向量 + BM25)结合多路召回,最大化召回率。
重排序:利用 Cross-Encoder 对候选集精筛,提升 Top-N 的相关性。
Prompt 构造:精心设计提示词模板,嵌入引用、角色、条件指令。
生成与输出:参数调优、流式输出、置信度评估。
进阶进化:Self-RAG、自适应检索、迭代检索等前沿方向。
至此,RAG 的两大阶段——索引阶段与查询生成阶段——已全部讲完。 你可以将这两篇文章作为 RAG 工程落地的技术检查清单,逐步对照优化你的系统。