前面聊到混合检索时,已经能看到一个很现实的问题:
知识库问答的质量,不只取决于大模型会不会写回答,也取决于系统能不能先把正确资料找出来。
但“找出来”这件事,还可以继续拆。
很多时候,系统并不是完全没找到正确资料,而是出现了更微妙的问题:
所以这一篇想解决的问题不是“再学几个算法名”,而是把检索链路看完整:
从海量资料中先快速捞出候选,再认真重排,再融合多路结果,再控制重复和多样性。
如果第十篇讨论的是“关键词检索和向量检索为什么要配合”,这一篇更进一步:
检索结果已经有了,怎样让最终送给大模型的证据更可靠。
假设有一个售后知识库,里面有几段文档:
| 文档 ID | 内容 | 说明 |
|---|---|---|
return-7-days |
用户签收商品后 7 日内可申请无理由退货,商品需保持完好。 | 正确答案 |
refund-3-days |
退款将在收到退货并检验合格后的 3 个工作日内原路退回。 | 讲退款到账 |
exchange-15-days |
质量问题商品 15 日内可申请换货,需提供照片凭证。 | 讲换货 |
shipping-presale |
预售商品以商品页标注的发货时间为准。 | 讲发货 |
return-package |
退货时请保留原包装、配件、赠品和发票。 | 讲退货材料 |
用户问:
多少天内可以退货?
理想情况下,系统应该把 return-7-days 放在最前面,因为它直接回答了“多少天内可以退货”。
但粗检索可能会返回这样的结果:
| 粗排排名 | 文档 ID | 为什么会被找出来 |
|---|---|---|
| 1 | refund-3-days |
有“退”“3 个工作日” |
| 2 | exchange-15-days |
有“15 日内”和售后语义 |
| 3 | return-7-days |
真正回答退货期限 |
| 4 | return-package |
有“退货” |
| 5 | shipping-presale |
与时效相关,但偏题 |
这时如果系统只取第一条给大模型,大模型可能回答:
退款一般在 3 个工作日内到账。
这句话本身可能没错,但它没有回答用户问的“退货期限”。
问题不在生成阶段,而在检索阶段:正确证据被找到了,却排得不够靠前。
这就是检索质量优化要处理的典型场景。
可以先把整个链路看成四步:
这里有几个词先放平:
| 术语 | 可以先这样理解 |
|---|---|
| 召回 | 第一轮先把“可能相关”的内容捞出来,目标是尽量别漏 |
| 重排 | 对候选结果重新精读排序,目标是把真正能回答问题的放前面 |
| 融合 | 把 BM25、向量检索等多路结果合成一个最终列表 |
| 多样性 | 不要让最终结果全是重复角度,要覆盖问题的不同必要信息 |
| 精排 | 更细、更慢、更准的排序,通常只处理少量候选 |
这几个词放在一起,就能形成一个基本判断:
召回解决“有没有找进来”,重排解决“有没有排前面”,融合解决“多路结果怎么合”,多样性解决“结果是不是重复挤占上下文”。
入门时很容易把检索想成一次排序:系统从全库里找到最相关的 5 条,然后直接交给大模型。
真实系统通常不是这样。
原因很简单:全库可能有几万、几十万甚至上百万个文本块。每次都用最精细的模型逐条判断,延迟和成本都会很高。
所以常见做法是两段式:
第一阶段的目标不是“百分百排准”,而是:
把正确答案尽量放进候选集。
这和筛简历很像。
第一轮 HR 可能先按关键词、工作年限、技术栈筛出 100 份简历。这个阶段不能太苛刻,因为太苛刻会漏掉好候选。
第二轮技术面试官再认真看这 100 份,判断谁真的匹配岗位。
检索也是一样:
| 阶段 | 更关注什么 | 典型方法 |
|---|---|---|
| 召回 | 快、覆盖面、别漏正确证据 | BM25、向量检索、混合检索、HNSW |
| 重排 | 准、细节判断、把答案排前面 | Cross-Encoder、Rerank 模型 |
| 融合 | 多路结果统一排序 | RRF、加权融合 |
| 多样性控制 | 避免重复、覆盖不同角度 | MMR、去重、按来源控制 |
如果召回阶段没有把正确文档捞进候选集,后面的重排再强也救不回来。
这句话很关键:
重排只能在候选集里重新排序,不能凭空找回没被召回的文档。
所以实际调系统时,不能只看最终答案好不好,还要回头检查:
在讲重排之前,需要先补一个常见的向量检索底层问题:
如果知识库里有 100 万个向量,查询来了以后,系统难道要和 100 万个向量逐个算相似度吗?
理论上可以,但通常太慢。
这叫精确最近邻搜索:给一个查询向量,把它和库里所有向量都算一遍距离,再找最近的几个。
如果数据量小,这样做很直观。
但数据量一大,计算量就会迅速上来。
假设:
这就意味着一次查询要做大量向量运算。即使单次计算很快,累计起来也会拖慢系统。
于是就出现了近似最近邻搜索,英文常写作 ANN,Approximate Nearest Neighbor。
它的思路是:
不一定保证找到数学上绝对最近的那个向量,但要在很短时间内找到足够接近的一批结果。
HNSW 就是一种常见的 ANN 算法。
HNSW 的全称是 Hierarchical Navigable Small World,可以先理解成:
用多层图结构帮向量检索快速找方向。
如果在一个城市里找某个地址,大多数人不会从每条街挨个扫过去。
更自然的方式是:
HNSW 也有类似的分层结构:
它不是把所有向量平铺成一张巨大表格,而是构建成一个图。
图里的每个向量点会连接一些邻居:
可以把搜索过程理解成:
HNSW 的价值不是让结果“更聪明”,而是让大规模向量检索“跑得动”。
它解决的是召回阶段的性能问题:
| 问题 | HNSW 的作用 |
|---|---|
| 全量扫描太慢 | 用图结构减少需要访问的节点 |
| 数据量很大 | 用近似搜索换取速度 |
| 需要低延迟返回候选 | 毫秒级找出一批相近向量 |
| 可以接受极小召回损失 | 用速度换一点点精确度 |
这里要注意一个边界:
HNSW 让向量检索更快,但它不负责判断候选是不是最终最适合回答问题。
也就是说,HNSW 更像“高速找到候选”的工具,不是最终裁判。
召回阶段通常会故意多找一些候选。
但候选多了以后,就需要第二个问题:
这些候选里,哪几条最能回答用户的问题?
这就是重排。
重排的英文常写作 Rerank。它不是重新去全库搜索,而是接收第一阶段召回出来的一小批候选,比如 20 条、50 条、100 条,然后重新打分排序。
可以这样理解:
回到前面的售后例子。
查询是:
多少天内可以退货?
候选里有两条:
A:签收 7 日内可无理由退货,商品需保持完好。
B:退款将在收到退货并检验合格后的 3 个工作日内原路退回。
粗检索可能觉得两条都很相关,因为它们都有“退”“日”“货”这些字。
但重排模型会更关注问题和文档之间的对应关系:
| 查询关注点 | 文档 A | 文档 B |
|---|---|---|
| 问的是退货期限 | 直接回答 7 日内可退货 | 讲退款多久到账 |
| 问的是“多少天内” | 有 7 日内 | 有 3 个工作日,但对象是退款 |
| 能否支撑最终回答 | 能 | 容易答偏 |
所以重排应该把 A 排到 B 前面。
这就是重排的核心价值:
召回阶段负责把可能相关的内容捞进来,重排阶段负责把真正能回答问题的内容顶上去。
理解重排时,经常会遇到两个词:
它们不是厂商名,也不是某个具体产品,而是两类常见模型结构。
向量检索常用的是 Bi-Encoder 思路。
它的流程是:
问题和文档是分开编码的。
优点是快:
缺点是细节交互不够:
这就像两个人各自写一份摘要,再比较摘要像不像。速度快,但细节容易丢。
Cross-Encoder 的思路不同。
它把问题和文档拼在一起,让模型同时阅读:
[CLS] 用户问题 [SEP] 候选文档 [SEP]
然后模型直接输出一个相关性分数。
流程可以这样看:
因为问题和文档在模型内部可以互相“看见”,它更容易捕捉细节:
所以 Cross-Encoder 通常更适合重排。
但它也更慢。
原因是:每一个查询和每一篇候选文档都要重新拼接、重新跑模型。
如果有 100 万篇文档,就不能直接对全库逐条用 Cross-Encoder 打分。
更合理的做法是:
可以把两者对比成一张表:
| 对比项 | Bi-Encoder | Cross-Encoder |
|---|---|---|
| 输入方式 | 问题和文档分别编码 | 问题和文档拼在一起 |
| 文档能否预计算 | 可以 | 不可以 |
| 速度 | 快 | 慢 |
| 精度 | 适合粗召回 | 适合精排 |
| 适用数据量 | 大规模文档库 | 少量候选 |
| 典型位置 | 第一阶段 | 第二阶段 |
这也是为什么“召回 + 重排”会成为常见结构:
快模型负责大范围找候选,慢模型负责小范围认真判断。
第十篇讲过,真实系统里经常会同时使用关键词检索和向量检索。
例如同一个问题:
ERR-5021 支付一直没返回怎么办?
BM25 可能返回:
1. support-api-err-5021
2. support-payment-delay
3. support-api-err-5030
向量检索可能返回:
1. support-payment-delay
2. support-api-err-5021
3. support-refund-policy
现在问题来了:
两个列表怎么合成一个最终列表?
最直接的想法是把分数相加。
但这很容易出问题,因为 BM25 分数和向量相似度不是同一种尺子。
BM25 的分数可能是:
13.8, 9.4, 3.1
向量相似度可能是:
0.82, 0.79, 0.63
它们的数值范围、含义、方向都可能不同。直接相加就像把“摄氏度”和“公里数”加在一起。
RRF 就是为了解决这类排名融合问题。
RRF 的全称是 Reciprocal Rank Fusion,通常译作倒数排名融合。
它的关键思路是:
不直接看原始分数,只看每个文档在各个列表里的排名。
排名越靠前,贡献越大。
同一个文档如果在多个列表里都靠前,最终分数就会更高。
RRF 的公式是:
其中:
用一个小例子看:
| 文档 | BM25 排名 | 向量检索排名 |
|---|---|---|
support-api-err-5021 |
1 | 2 |
support-payment-delay |
2 | 1 |
support-api-err-5030 |
3 | 未出现 |
support-refund-policy |
未出现 | 3 |
如果 ,那么:
这两个文档都会得到较高分,因为它们在两路检索中都排得靠前。
而只在一路出现的文档,分数会低一些:
RRF 的好处是:
可以用一段伪代码理解:
def rrf(rankings, k=60):
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
scores.setdefault(doc_id, 0.0)
scores[doc_id] += 1 / (k + rank)
return sorted(scores.items(), key=lambda item: item[1], reverse=True)
这里的 rankings 可以是:
[
["support-api-err-5021", "support-payment-delay", "support-api-err-5030"],
["support-payment-delay", "support-api-err-5021", "support-refund-policy"],
]
RRF 关心的是“排第几”,不是“原始分数是多少”。
这对混合检索很友好。
RRF 解决的是多路结果怎么融合。
但融合之后,还有另一个问题:结果可能很重复。
假设用户问:
Python 怎么读取文件?
检索结果可能是:
| 排名 | 内容 |
|---|---|
| 1 | Python 可以使用 open() 读取文件。 |
| 2 | 使用 open(file, 'r') 可以读取文本文件。 |
| 3 | with open(...) as f 是读取文件的推荐写法。 |
| 4 | 读取文件后要注意关闭文件句柄。 |
| 5 | Python 文件模式包括读取、写入、追加。 |
这些都相关,但前 3 条高度重叠。
如果最终只给大模型 3 条证据,可能全都在讲 open(),而没有覆盖“关闭文件”“读取模式”“异常处理”等信息。
这时就需要多样性控制。
MMR 是一种常见方法。
MMR 的全称是 Maximal Marginal Relevance,通常译作最大边际相关性。
它想平衡两件事:
公式可以写成:
其中:
这条公式看起来有点长,但直觉很简单:
MMR 分数 = 与问题的相关性奖励 - 与已选结果重复的惩罚
如果一篇文档和问题很相关,它会加分。
如果它和已经选中的文档很像,它会被扣分。
假设系统已经选中了:
文档 A:Python 可以使用 open() 读取文件。
现在有两个候选:
文档 B:使用 open(file, 'r') 可以读取文本文件。
文档 C:读取文件时建议使用 with 语句自动关闭文件。
B 和查询很相关,但和 A 很像。
C 也相关,而且补充了“自动关闭文件”的信息。
如果只按相关性排序,B 可能排在 C 前面。
如果用 MMR,C 有机会被选上,因为它提供了新的信息角度。
MMR 不是为了故意降低相关性,而是为了避免上下文被重复内容占满。
它的步骤可以这样理解:
参数 很关键:
| 偏向 | 可能效果 | |
|---|---|---|
| 接近 1 | 更重视相关性 | 结果更集中,但可能重复 |
| 接近 0 | 更重视多样性 | 结果更分散,但可能偏离问题 |
| 0.5 到 0.8 | 平衡 | 常见起点,需要结合业务调 |
在知识库问答里,不建议为了多样性牺牲太多相关性。
因为最终目的是回答问题,不是展示五花八门的内容。
更稳的理解是:
MMR 适合在“候选都比较相关”的前提下,减少重复,提升信息覆盖。
到这里,几个概念就能连起来了。
它们不是并列替代关系,而是站在不同位置:
| 环节 | 解决的问题 | 典型方法 |
|---|---|---|
| 快速召回 | 从全库里先找出可能相关的候选 | BM25、向量检索、HNSW |
| 多路融合 | BM25 和向量检索结果怎么合并 | RRF、加权融合 |
| 精细重排 | 候选里谁最能回答问题 | Cross-Encoder、Rerank |
| 多样性控制 | 避免重复结果占满上下文 | MMR、去重、分组 |
| 最终截断 | 控制上下文长度和成本 | Top K、token 预算 |
一条比较完整的检索流水线可以这样设计:
这条链路里,每一步都有自己的目标。
BM25 负责抓硬线索。
比如错误码、字段名、函数名、编号、日期。
向量检索负责抓语义。
比如用户换一种说法、口语化表达、同义描述。
HNSW 让大规模向量检索跑得更快。
它解决的是性能问题,让系统不用每次全量扫描。
RRF 负责把多路排名合起来。
它不直接比较不同检索器的原始分数,而是用排名做融合。
Rerank 负责重新精读候选。
它把“可能相关”进一步筛成“更能回答问题”。
MMR 负责减少重复。
它避免最终上下文里全是同一类相似片段。
这样看,检索优化不是一个单点技巧,而是一条流水线设计。
检索优化最怕只看最终回答。
因为大模型很会把答案写顺。
即使证据不完整,它也可能生成一段看起来像样的回答。
更稳的做法是先检查检索结果。
可以从几个层面看。
第一,正确证据有没有被召回。
如果正确证据根本没有进入候选集,就优先调召回:
第二,正确证据排在第几名。
如果正确证据进来了,但排得靠后,就优先考虑重排:
第三,结果是否重复。
如果前几条都是同一段内容的改写,就要考虑:
第四,多路检索是否互相打架。
如果 BM25 和向量检索返回的结果差异很大,要看:
可以把排查顺序写成一个简单流程:
这张图背后的意思是:
不要一上来就怪大模型。很多回答问题,其实是检索证据没有整理好。
误区一:以为召回 Top K 越小越精准。
Top K 小只是候选少,不代表更精准。
如果第一阶段只取 3 条,正确证据可能连重排机会都没有。
更常见的做法是第一阶段多取一些,比如 Top 20、Top 50,再交给后面的重排和去重处理。
误区二:以为重排可以弥补召回失败。
重排只能重排已有候选。
候选里没有正确答案,重排模型再强也没法选出来。
误区三:直接相加不同检索器的分数。
BM25 分数、向量相似度、重排分数通常不是同一种尺度。
直接相加很容易得到没有意义的排序。
如果只是融合排名,RRF 往往比直接加分更稳。
误区四:结果越相关越好,不需要多样性。
如果前 5 条都在讲同一个点,相关性看起来很高,但信息覆盖可能不足。
尤其是复杂问题,最终证据需要覆盖多个必要方面。
误区五:把 HNSW 当成提升语义理解的算法。
HNSW 主要解决向量检索速度问题。
它能更快找到近邻,但不会让向量本身变得更懂业务。
误区六:只看最终回答,不看中间证据。
检索系统一定要保留可观察的中间结果:
没有这些中间结果,就很难知道问题出在哪一段。
这一篇的主线可以收束成一句话:
检索质量优化,不是简单把某个算法换成更高级的算法,而是把“找得到、排得准、合得稳、不重复”这几件事分开处理。
召回阶段追求的是覆盖:
正确证据要先进入候选集。
HNSW 解决的是大规模向量检索的速度:
不全量扫描,也能快速找到相近向量。
重排解决的是候选里的精度:
把真正能回答问题的证据排到前面。
RRF 解决的是多路结果融合:
不直接比较不同分数,而是用排名进行稳定合并。
MMR 解决的是重复和覆盖:
在相关的前提下,让最终证据不要全挤在同一个角度。
对刚开始搭知识库问答的人来说,不需要一开始就把所有优化都上满。更实际的顺序是:
这样理解以后,再继续学习 RAG,也就是“先查资料、再让大模型基于资料回答”的知识库问答架构,就不会只把它看成“把文档塞给大模型”。
更准确地说,这类系统的关键工程能力之一,就是把资料筛成一组足够可靠、足够相关、足够不重复的证据。
还没有公开评论
欢迎留下第一条想法,评论会在博主审核后显示。