RAG 从提问到回答:查询改写、多路召回、精排与上下文生成

文档已经切好、向量已经入库,为什么用户提问后,系统仍可能找错资料,或者拿着相关资料答非所问?

问题出在检索和生成之间还有几次判断:用户的问题是否表达完整,不同检索方法找到的结果如何合并,看起来相似的片段是否真的回答了问题,以及最终留下的证据怎样放进模型上下文;这些环节连接起来,才是从 "能搜索" 到 "能回答" 的完整过程

一、查询改写:让检索意图更清晰

原始问题什么时候需要改写:

用户不会总按文档里的术语提问,也不一定把背景说完整;有的问题本身很短,有的依赖前文,还有的省略了讨论对象

原始问题 需要处理的地方 改写示例
WebSocket 握手过程 可以补足表达,但短问题本身不一定检索不好 WebSocket 连接建立的握手流程
那第二个呢 "第二个" 依赖已有对话 ZooKeeper 集群选举的第二个阶段
怎么部署 没有说明部署什么 RAG 系统的部署流程

后两种补全必须有上下文依据:只有前文确实在讨论 ZooKeeper 选举,才能把 "第二个" 解释为选举阶段;只有已知讨论对象是 RAG 系统,才能补出部署对象;改写的任务是恢复用户意图,不能凭空替用户决定问题

查询改写希望缩小用户表达与文档表达之间的差异;是否启用,应看原始问题的完整程度和实际检索效果,不能仅凭字数判断

常见改写方法怎样选择:

方法 工作方式 适合处理的问题 额外开销
MultiQuery 生成多个语义等价的问法,分别检索后合并 一种表达可能漏掉其他表述 生成变体、编码和多次检索
对话改写 结合历史,把代词和省略还原成独立问题 "那第二个呢" "详细说说" 通常增加一次模型调用
HyDE 先生成假设答案,再用它的向量检索真实文档 问题与文档的表达方式相差较大 假设文档生成和向量编码
Step-Back Prompting 先提出更高层次的问题,寻找相关原理,再处理原问题 细节很多、需要原理支撑的问题 增加抽象问题生成和检索
Query Decomposition 把复杂问题拆成多个子问题,分别寻找证据 多要点比较、多步推理 多个子问题的生成与检索
查询路由 判断意图,选择数据源、组件或提示模板 向量库、SQL、知识图谱等多种来源 取决于使用模型判断还是向量匹配

MultiQuery 的核心是 "一件事换几种问法":先生成变体,再让各变体独立召回,最后合并重复片段;Query Decomposition 则是 "一件复杂的事拆开查",子问题可能分别负责不同事实,两者不能只因为都产生多个查询就混为一谈

增加查询数量通常会增加总计算量,但端到端延迟不一定按数量线性增加;独立查询可以并行或批量处理,有先后依赖的子问题则需要顺序执行

HyDE 的特别之处在于,它用一段看起来像答案的文本,去寻找同样以陈述方式写成的真实资料;假设答案可能包含错误,所以它只能提供检索线索,最终回答仍应依据检索到的真实文档;这一流程可见 HyDE 原始论文

实现时,LangChain 生态中的 MultiQueryRetriever 对应多问法召回,create_history_aware_retriever 对应历史感知查询,HypotheticalDocumentEmbedder 对应 HyDE;LlamaIndex 的 CondenseQuestionChatEngine 用于把对话还原成独立问题,HyDEQueryTransform 用于假设文档转换;LangChain 的相关组件分布在不同包中,旧版示例里的导入路径不能直接当成所有版本的通用写法

Step-Back 和问题分解可以通过提示词与检索流程组合实现;不要把 MultiQueryRetriever 理解为天然具备复杂任务分解能力,生成什么查询、如何处理依赖,仍取决于具体编排

查询路由可以用 RunnableBranch 配合分类结果实现,也可以使用 LlamaIndex 的 RouterQueryEngine 与选择器;不通过生成模型时,还可以比较问题向量与各路由描述的相似度;路由决定的是 "去哪儿查",改写决定的是 "用什么问题查"

二、粗排:多路召回与 RRF 融合

为什么语义检索还需要关键词检索:

稀疏表示与稠密表示各自保留什么:

稀疏向量通常在很大的词表空间里表示文本,绝大多数位置是零,只有少量词项带有权重;BM25 根据词频、文档频率和长度等统计量计算相关性,学习式稀疏表示则由模型产生词项权重

稠密向量把文本映射成固定长度的数值序列,例如 1024 维;每个维度通常没有可以直接读出的词义,语义信息分布在整个向量中;这里的 "稠密" 并不要求每个数值都绝对非零,维度也并不固定为 1024

特性 稀疏检索 稠密检索
表示空间 通常对应词项,维度很高、非零位置较少 固定长度的连续向量
匹配依据 词项及其权重 学到的语义表示及相似度
可解释性 通常能看到哪些词项作出了贡献 单一维度一般难以解释
训练需求 BM25 不需要训练;学习式稀疏模型需要训练 使用训练好的编码模型
典型优势 函数名、错误码、型号等精确词项 同义表达、近义表达和语义关联
可能的不足 纯词项匹配容易漏掉用词不同的资料 容易把主题相似但条件不同的内容混在一起

"稀疏" 不等于 "完全不懂同义词";纯 BM25 在没有同义词扩展等处理时,主要依赖词项重合,部分学习式稀疏模型则能够产生扩展词项;能否补上语义差异,要看具体模型和检索配置

单路召回会漏掉什么:

用户问 "如何部署系统",文档写的是 "安装步骤如下",稠密检索有机会借助语义关联找到它;但如果用户搜索函数名 processPayment,仅按语义找 "支付处理" "交易流程",可能返回主题相关却没有目标函数的片段

关键词检索更容易保留 processPayment 这样的精确线索;可是在没有同义词处理的情况下,"如何部署" 与 "安装步骤"、"手机" 与 "移动电话" 之间缺少共同词项,就可能匹配不上

多路召回的作用是让这些能力互补;某一路负责保住精确字面线索,另一路帮助跨越表达差异,再把候选交给后续融合和精排

BM25 为什么仍值得保留:

函数名、API 名、错误码、产品名和型号,常常只有字面完全一致才有意义;BM25 不需要查询端模型推理,也不需要先训练领域模型,构建倒排索引后就能提供一条可用的检索路径

Dense + BM25、Dense + 学习式 Sparse,以及再增加 BM25 补充或回退,都是可以评估的组合;学习式稀疏检索并不会自动取代 BM25,最终应比较真实问题上的召回质量、运行成本和维护复杂度

多路召回怎样落到工程上:

两路逻辑独立,并不代表共用的数据库连接允许同时执行查询;连接或驱动不支持并发时,可以串行执行,也可以使用独立连接或连接池

假设某环境下 Dense 查询编码耗时 300ms,Sparse 编码耗时 50ms,两段独立计算串行需要约 350ms,理想并行约为 300ms;这只是忽略检索与调度开销的算术示例,不能当作通用性能基准;如果模型共享前向计算,或瓶颈在数据库而不是编码阶段,结果还会不同

并行是否值得做,要看它实际减少了多少延迟,以及是否增加连接管理和资源竞争;不能仅凭 "两路" 就断定应该并行,也不能把某一组耗时推导成所有生产系统的架构选择

各路还必须遵守相同的搜索范围:例如只检索活跃素材、排除归档内容,并应用相同的访问约束;否则合并出来的候选并不来自同一个允许搜索的集合

为什么不能直接比较两路分数:

不同检索器的分数可能连方向都不一样:余弦距离通常越小越接近,余弦相似度则越大越接近;BM25 得分和学习式稀疏检索得分又有各自的尺度,具体数值范围取决于实现

因此,BM25 得分 15 和余弦距离 0.3,不能直接用数值大小判断谁更相关;学习式稀疏检索的相关性分数通常也要通过查询和文档的词项权重共同计算,并非直接取模型输出的某一个词权重

融合通常有三条路径:

归一化后加权 :先把各路分数处理成同方向、可比较的尺度,再计算 α × dense + (1 − α) × sparse;权重和归一化方法都会影响结果

学习排序:训练模型综合多个检索特征;需要相关性数据,并承担训练与维护成本

倒数排名融合 RRF:只使用每一路的名次,绕开原始分数尺度不一致的问题;不需要训练,但仍有平滑常数和候选窗口等配置

RRF 怎样把名次变成一个分数:

RRF 对文档在每一路中的名次求倒数贡献,然后相加:

复制代码
RRF_score(d) = Σ_i 1 / (k + rank_i(d))

rank_i(d) 从 1 开始;如果某文档没有出现在一路返回的候选窗口里,这一路贡献记为 0;相同文档应先通过稳定的标识对齐,才能把不同路线对它的支持合起来

平滑常数 k 用来控制头部名次之间的差距;不加平滑时,第 1 名贡献为 1,第 2 名为 0.5,相差一倍;当 k = 60 时,两者分别约为 0.016393 和 0.016129,相对差距约 1.64%,单路第 1 名的优势不再那么悬殊

看两个片段的例子:

片段 Dense 名次 Sparse 名次 RRF,k = 60
A 1 5 1/61 + 1/65 ≈ 0.031778
B 3 2 1/63 + 1/62 ≈ 0.032002

B 的总分略高于 A;A 虽然拿到一路第一,但 B 在两路中都比较靠前,累计贡献更高;RRF 因而能够把多路都支持的候选往前推,但它并没有直接阅读文本、判断事实是否相关

哪些框架提供融合能力:

框架或产品 对应能力 使用时关注
Elasticsearch RRF retriever rank_constant 是平滑常数,rank_window_size 控制每路参与融合的窗口
OpenSearch score-ranker-processor,组合方式为 rrf 通过搜索流水线处理排名
LangChain EnsembleRetriever 支持组合多个检索器,当前参考实现位于 langchain_classic
LlamaIndex QueryFusionRetriever 可组织多查询、多检索器及不同融合方式
Vespa reciprocal_rank_fusion 在排序表达式中组合排名
Solr JSON Combined Query DSL 中的 RRF 需要确认部署版本提供对应能力

这些入口并不使用完全相同的配置结构,接入时应分别对照 Elasticsearch RRFOpenSearch 排名融合处理器LangChain EnsembleRetrieverVespa 分阶段排序Solr 组合查询 的接口说明

精排:从 "可能相关" 到 "真正回答问题"

为什么分成粗排和精排:

第一阶段面对的是十万、百万级片段,需要尽快留下几十个可能有用的候选;这时更看重召回,尽量别漏掉相关资料

第二阶段只处理这几十个候选,可以对问题和片段进行更细的比较,再取最终 Top-10 等结果;这时更看重精确性,尽量把主题相似却回答不了问题的内容排下去

这种分工让昂贵的计算集中在少量候选上;粗排和精排的实际耗时取决于模型、硬件、文本长度与批处理方式,不能固定理解为 "粗排必然毫秒级、精排必然秒级"

Bi-Encoder 和 Cross-Encoder 的差别:

Bi-Encoder,双编码器方式,把问题和文档分别编码成向量,再计算相似度;由于文档编码不依赖当前问题,可以在入库时提前完成,查询时只需编码问题,再搜索已有向量

便利也带来了信息压缩:在单向量方案中,一整段文本被压成一个固定长度表示,某些细粒度差异可能不够突出;例如 "如何部署系统" 和 "如何卸载系统" 共享很多词和句式,检索模型可能把两者排得很近,但这不代表所有双编码器都无法区分它们

Cross-Encoder,交叉编码器方式,把问题与候选片段一起输入模型,让两段文本在编码过程中发生细粒度交互,最后直接输出相关性分数;模型因而有机会结合 "部署" 和 "卸载" 等关键差异判断候选是否真正符合问题

这种联合计算不能像文档向量那样提前完成,因为换一个问题,就换了一个问题---片段组合;假设在某个 CPU 环境下逐对推理需要 100ms,50 对就约需 5 秒,考虑其他开销还可能更长;批处理、加速设备和更小的模型会改变耗时,不能把它当作所有 Reranker 的固定速度;两阶段组合的基本流程可参考 Sentence Transformers 的 Retrieve & Re-Rank

Cross-Encoder 内部怎样判断相关性:

把问题与片段放进同一序列:

用 BERT 风格的记号表示,输入可以写成:

复制代码
[CLS] query [SEP] chunk [SEP]

[CLS] 位于序列开头,分类任务可以使用它经过编码后的表示;[SEP] 用来标记文本边界;这些是便于理解的记号,具体模型的特殊 token 和拼接规则由 tokenizer 决定,XLM-RoBERTa 等模型并不直接采用这套字面符号

经过多层 Transformer 后,用于分类的序列表示包含了问题与片段交互后的信息,再交给打分头;不能把它简单理解为模型一开始就已经知道 "匹配度是多少"

多层编码没有固定的语义分工表:

不同模型的层数和注意力头数并不一样;以 bge-reranker-v2-m3 为例,其公开配置包含 24 层、16 个注意力头、1024 维隐藏表示和 4096 维中间层;这些数值来自 模型配置,不能套用为所有 Reranker 的结构

多层计算会逐步变换文本表示,但不能机械规定 "前 3 层只认同义词、中间几层只认句式、最后几层只认意图";注意力头也不是预先分配好职责的几个语义专家,它们的行为由训练形成

注意力:决定从哪些 token 取信息

每个 token 的表示经过线性变换,产生 Query、Key、Value,也就是 Q、K、V;这里的 Query 是注意力计算中的名称,并不等同于用户输入的那一句问题

Q 与 K 的匹配产生注意力分数,经过缩放和 softmax 得到权重,再按权重汇总 V:

复制代码
Attention(Q, K, V) = softmax(QKᵀ / √dₖ) V

dₖ 是键向量的维度;softmax 让一组权重归一化,在通常的注意力计算中,它们加起来为 1;假设三个位置的权重是 0.6、0.3、0.1,输出就是 0.6V₁ + 0.3V₂ + 0.1V₃,并非简单取出权重最大的那个位置

多头注意力同时进行多组这样的计算,再组合结果;在交叉编码器中,问题和片段处于同一个可交互的输入中,所以问题里的词可以利用片段中的信息,片段里的词也可以结合问题重新形成表示

前馈网络与打分头:

注意力完成信息交互后,前馈网络继续对各位置的表示进行非线性变换;以隐藏维度 1024、中间维度 4096 的配置为例,可以概括为:

复制代码
1024 维 → 线性映射到 4096 维 → GELU → 线性映射回 1024 维

最后的打分可以用 s = Wx + b 理解:把序列表示 x 的多个特征加权,得到一个标量;真实分类头可能还包含额外的线性层、激活和 dropout,不能把这个简式当成所有模型的完整实现

"多个特征加权投票" 只是理解计算的比喻;1024 个维度并不是 1024 条可以直接读出的判断规则,权重与表示都需要通过训练学习

模型从哪里学会相关与不相关:

训练样本可以表示为 (query, chunk, label):相关样本标为 1,不相关样本标为 0;若采用二分类交叉熵,令预测值为 p、标签为 y,损失为:

复制代码
Loss = −[y·log(p) + (1−y)·log(1−p)]

正样本希望 p 高,负样本希望 p 低;只写 −log(p) 仅能表达正样本这一侧;实际排序模型也可能使用成对或列表式目标,不一定都按这个二分类形式训练

数据可以来自人工相关性标注,也可以从已有问答对中构造远程监督样本;MS MARCO 是常用的数据来源之一,而自动构造的数据通常需要面对噪声问题

难负样本挖掘尤其有价值:让模型看到 "内容很像,但答不到点上" 的片段,例如把卸载说明作为部署问题的负样本;它要学会的,是相关性判断中真正影响答案的差异

Reranker 模型与框架怎样选择:

模型或服务 特点与边界
bge-reranker-v2-m3 BAAI 提供的多语言模型,Apache-2.0;可与 bge-m3 组合,但不要求召回模型必须与它同系列
Cohere Rerank 可以通过托管服务调用,减少本地模型部署工作
Jina Reranker 有多语言模型与服务;不同版本应分别核对许可证,不能统一理解为无使用限制
bge-reranker-large BGE 系列中的另一种模型,"large" 不能直接推出它一定优于 v2-m3

例如 Jina v2 多语言模型 标注的是 CC-BY-NC-4.0,而 BGE v2-m3 模型卡 标注 Apache-2.0;模型效果和部署条件之外,具体版本的许可也是选型信息的一部分

推理框架中,FlagEmbedding 是 BAAI 的官方库,覆盖向量模型与 Reranker;Sentence Transformers 提供 CrossEncoder;FlashRank 提供轻量重排方案,包括基于 ONNX 的路径;是否更快必须在相同模型、文本长度、硬件和批量条件下比较,不能直接套用固定的 "每对 30ms" 或 "三倍提升"

流程框架则提供连接位置:LangChain 可通过 ContextualCompressionRetriever 接入重排组件,LlamaIndex 使用节点后处理组件,Haystack 提供 TransformersSimilarityRanker;Elasticsearch 的 text_similarity_reranker 可用于语义重排,LTR 是另一类学习排序能力,不能与交叉编码器直接画等号

分数阈值要怎样校准:

Reranker 可能输出原始 logit,也可能通过 sigmoid 映射到 0~1;即使分数处于 0~1,也不能直接把 0.8 读成 "有 80% 的概率相关",除非已经验证过概率校准

阈值相当于候选的准入线,Top-K 则限制最多留下多少个;两者作用不同,进入 Top-10 的片段仍可能低于阈值,最后不一定要凑满 10 个

阈值示例 可能出现的结果 需要关注
0.8,较高 保留的片段少,可能只剩少量候选 是否漏掉必要证据
0.1,较低 更多片段进入上下文 是否带入无关内容
0.3,中间候选值 可以作为待测试的配置之一 不能预先认定是平衡点

在同一批候选中,提高阈值只会让保留集合变小;精确率是否上升、召回损失有多大,则需要看实际相关性标签;这些数字都不能跨模型直接照搬

校准时,给评估问题准备相关性标注,记录每个候选的分数,观察相关与不相关片段的分布,再扫描不同阈值下的精确率和召回率;最终选择取决于业务更难接受漏掉证据,还是混入噪声,并应在独立评估数据上确认效果

四、结果生成:把检索结果变成模型可用的上下文

注入之前,先计算预算:

精排留下的片段要组合成 context block,和系统指令、对话历史、当前问题一起进入模型;不能只检查检索片段是否小于上下文窗口,还需要给回答留下空间

假设窗口为 8K token,检索证据已经占 6K,那么其他输入与回答合计只剩 2K;剩余空间是否足够,取决于实际问题、历史长度和输出需求,而不是看到 "还有 2K" 就自动认为够用

计数应使用目标模型对应的 tokenizer;超过预算时,可以按精排分数优先移除低分片段,再确认剩下的证据是否足以回答问题

同样的片段,位置也可能影响使用效果:

长上下文研究发现,在一些任务和模型上,把相关信息放在输入中间,效果可能比放在首尾差;这就是常说的 Lost in the Middle;它提示我们关注证据位置,但不是所有模型、所有任务都遵守的固定规律,详见 原始研究

一种可测试的安排是:最相关片段放在开头,第二相关片段放在末尾,第三相关片段放在倒数第二位,其余片段填入中间

例如 C1 到 C6 已按相关性从高到低编号,可以排成:

复制代码
[C1] [C4] [C5] [C6] [C3] [C2]

如果是 10 个片段,对应顺序就是 C1、C4、C5、C6、C7、C8、C9、C10、C3、C2;编号代表原始相关性次序,摆放位置代表最终注入次序,两者不能混淆

排布无法弥补证据本身不相关;先通过召回和精排得到有用内容,再比较不同位置策略的效果,才能判断它是否适合当前模型

温度改变的究竟是什么:

模型生成下一个 token 时,会为候选 token 计算一组 logits;它们是尚未归一化的分数,温度 T 在 softmax 前缩放这些分数,从而改变采样概率

复制代码
P(w_i) = exp(logit_i / T) / Σ_j exp(logit_j / T)    (T > 0)

T 较小时,分数差距被放大,概率更集中在高分候选上;T 较大时,分布变平,低分候选更容易被选中;温度改变的是下一步选择的随机性,并不会检查候选说法是否符合检索证据

假设只有 "部署" "安装" "配置" 三个候选,logits 分别是 3、2、1.5,按公式得到:

温度 部署 安装 配置
T = 0,按贪心选择理解 100% 0% 0%
T = 0.3 95.93% 3.42% 0.65%
T = 0.7 73.69% 17.66% 8.65%
T = 1.0 62.85% 23.12% 14.02%
T = 2.0 48.10% 29.18% 22.72%

表格是固定 logits 的数学示例,不是某个模型的实测输出;百分比因四舍五入可能与 100% 有微小误差

T = 0 不能直接代入上面的除法;很多接口把它作为贪心解码的约定,即选择分数最高的 token;这也不等于系统层面保证每次调用逐字相同,实际行为还与推理实现有关;支持哪些温度数值,应以所用模型接口为准

RAG 不同阶段怎样设置温度:

场景 可测试的起始范围 想控制的行为
MultiQuery 改写 0.3~0.5 在保持意图的同时生成不同表述
知识库事实回答 0.1~0.3 减少不必要的表达发散
文章或大纲生成 0.5~0.7 增加表达多样性
对话问答 0.3~0.5 在连贯表达与稳定性之间尝试平衡

这些范围只适合作为实验起点;温度降低并不保证忠实于资料,温度提高也不保证内容更有创造性;对于事实回答,证据是否准确、是否充分,以及模型是否按证据作答,仍然需要单独评估

温度与 Top-p 有什么区别:

温度通过缩放 logits 改变整个概率分布;Top-p 则把候选按概率从高到低排列,保留累计概率达到或超过 p 的最小前缀,再在保留集合中重新归一化并采样

参数 改变什么 直接效果
Temperature 所有候选之间的概率差距 控制分布集中还是平缓
Top-p 可以参与采样的候选集合 截去累计概率之外的长尾候选

Top-p 保留的 token 数量不是固定值,取决于当前分布;分布很集中时可能只留下少量候选,比较平坦时则可能留下更多

调试时可以先固定一个参数、调整另一个,便于辨别变化来源;例如把 top_p = 0.9 作为候选配置进行评估,而不是认定它是所有 RAG 系统的最优值;temperature = 0top_p = 1 也不是错误组合,在按贪心方式执行的接口中,它可以用于较稳定的输出需求

从问题到回答,查询改写负责把意图说清楚,多路召回负责减少遗漏,RRF 负责合并不同尺度的排名,精排负责辨别候选能否回答问题;最后,上下文预算、证据位置和采样参数共同影响模型如何使用这些资料;每一步处理的都是不同问题,把它们各自做好,检索到的内容才更有机会成为可靠答案

相关推荐
智码看视界3 小时前
Day72-文档预处理实战:PDF解析 + 文本切块的正确姿势
pdf·pdfbox·预处理·rag·文档解析·tika·文本切块
yxlalm8 小时前
SpringAI+RAG-检索文档变知识:从上传到精准检索的完整链路
spring·知识库·rag
宁渡AI大模型10 小时前
河南宁渡科技有限公司|宁渡课堂 AI 全栈面试分享,RAG 项目面试深挖问题解析
人工智能·机器学习·rag
XLYcmy1 天前
DeepMMSearch-R1: Empowering Multimodal LLMs in Multimodal Web Search论文分享
llm·sft·强化学习·多模态·苹果·rag·检索
java_logo1 天前
Docker 部署 Milvus:轻松搭建高性能向量数据库平台
数据库·docker·私有化部署·milvus·向量数据库·rag·轩辕镜像
宁渡AI大模型1 天前
河南宁渡科技有限公司|宁渡课堂 AI 全栈面试分享,AI 应用开发高频考点汇总
java·c++·人工智能·python·深度学习·神经网络·rag
码农飞哥1 天前
企业级RAG系统架构详解
java·人工智能·ai编程·rag·ai应用
letisgo51 天前
JAVA 高级进阶18篇《生产级RAG:切分、检索、评估与Graph RAG全链路实战》
java·面试·检索增强·rag·graph rag
不是株1 天前
RAG 检索基础:多路表示、向量数据库与索引
rag