用户问:“支付报ERR_PAY_042,页面一直转圈,是不是应该再付一次?”
知识库里可能有三份相关资料:错误码说明、支付超时处理流程、幂等键与重复扣款保护。只按错误码查,容易遗漏没有写错误码的流程文档;只按语义相似度查,又可能把另一个相似错误码的资料排到前面。
混合检索让关键词检索和向量检索各自找候选,再把两路结果融合成一份排名。它解决的是“怎样把证据找全并合理合并”。上一篇介绍的重排随后进一步判断候选与问题的相关程度,两者处在不同环节。
一、两路检索为什么值得一起用
关键词检索擅长利用文档中的词项,尤其是错误码、类名、型号和产品编号。向量检索把问题与片段编码成向量,适合连接不同措辞表达的相近含义。
| 查询线索 | 关键词检索的价值 | 向量检索的价值 |
|---|---|---|
| ERR_PAY_042 | 保留精确标识 | 联系超时、查单等语义 |
| 页面一直转圈 | 可能受分词和用词影响 | 匹配“响应超时”等表述 |
| 重复扣款 | 找到对应术语 | 联系“重复提交支付请求” |
关键词检索仍依赖分析器,BM25也不等于严格的字符串相等。错误码若必须完全一致,应保存到keyword等精确字段,用过滤条件或明确加权规则表达。向量相似度同样不能代替版本、租户或权限条件。
Weaviate将关键词与向量搜索组合为混合检索,并提供按排名和按归一化分数融合的策略。这说明混合检索是一种架构选择,具体融合算法仍需明确。官方说明

二、不要直接把两个分数相加
BM25分数与向量相似度没有天然统一的尺度。假设关键词结果分数为12.5、8.0,向量结果为0.88、0.86,直接求和会让前一路的数值范围左右结果。即使两路都归一化,结果也会受到候选分布和极端值影响。
一种直接可解释的起点是RRF(Reciprocal Rank Fusion,倒数排名融合):只看文档在每路中的排名,再累加贡献。
RRF(d) = 各路中 1 / (c + rank(d)) 的总和
本文排名从1开始。文档没有进入某一路的候选窗口,该路贡献为0;c是平滑常数,不是最终返回条数。较大的c会缩小靠前与靠后名次的相对差距。RRF保留名次信息,但舍弃原始分数的差距,因此不应把它的结果分数当作答案正确概率。
以c=60为例:
| 文档 | 关键词排名 | 向量排名 | 融合分数 |
|---|---|---|---|
| D1错误码查单流程 | 1 | 3 | 1/61+1/63≈0.032266 |
| D2付款后转圈处理 | 3 | 1 | 1/63+1/61≈0.032266 |
| D3幂等与重复扣款保护 | 未进入 | 2 | 1/62≈0.016129 |
| D4一般性排障 | 2 | 未进入 | 1/62≈0.016129 |
D1与D2获得两路支持,因此排在前面。D3与D4并列时,需要一个确定的平局规则;本例按文档ID排序,保证同样输入得到同样输出。这个规则保证可复现,并不代表D3的业务价值一定高于D4。
Elasticsearch的RRF检索器支持融合多个子检索器,其rank_constant与rank_window_size分别控制平滑常数和每路候选窗口,最终size控制输出数量。官方接口
三、先统一候选的身份与范围
要合并两路候选,首先得知道它们是不是同一个片段。关键词索引和向量索引应共用稳定标识,例如“文档ID+版本+片段编号”。不能用返回顺序当ID,也不能仅凭正文相同就把不同来源合成一条。
权限和版本条件应在两路召回中都应用:同一个租户、同一组可见范围、同一个知识版本。合并阶段再复核一次,防止索引延迟或接入错误把不合格候选送入后续模型。
召回后过滤不能完全代替召回时过滤。 如果每路只取10条,其中8条随后因权限被删除,最终只剩2条;模型再强也无法利用根本没有召回的其他合格证据。应将过滤下推到检索器,并根据业务选择足够的候选窗口。

四、完整实现一个可检查的融合器
下面使用两路固定的有序候选,集中观察融合行为。代码在Python3上运行,无外部依赖;KEYWORD与VECTOR是上游结果输入,不执行向量模型推理。
from collections import defaultdict
DOCS = {
'D1': {'tenant': 'shop-a', 'version': 3, 'title': 'ERR_PAY_042:支付请求超时后的查单流程'},
'D2': {'tenant': 'shop-a', 'version': 3, 'title': '付款后页面一直转圈,应先查询订单状态'},
'D3': {'tenant': 'shop-a', 'version': 3, 'title': '支付幂等键与重复扣款保护'},
'D4': {'tenant': 'shop-a', 'version': 3, 'title': '支付通道一般性排障'},
'OLD': {'tenant': 'shop-a', 'version': 2, 'title': '旧版支付流程'},
'PRIVATE': {'tenant': 'shop-b', 'version': 3, 'title': '其他租户的支付资料'},
}
# 两路检索器返回的有序候选,用固定输入观察融合行为。
KEYWORD = ['D1', 'D4', 'D2', 'D1', 'OLD', 'PRIVATE']
VECTOR = ['D2', 'D3', 'D1', 'PRIVATE']
def fuse(rankings, allowed, window=3, top_k=3, c=60):
if window < 1 or top_k < 1 or c < 1:
raise ValueError('window、top_k和c必须为正数')
scores = defaultdict(float)
evidence = defaultdict(dict)
for name, candidates in rankings.items():
seen = set()
kept = []
for doc_id in candidates:
if doc_id in allowed and doc_id not in seen:
seen.add(doc_id)
kept.append(doc_id)
for rank, doc_id in enumerate(kept[:window], start=1):
scores[doc_id] += 1 / (c + rank)
evidence[doc_id][name] = rank
ordered = sorted(scores, key=lambda d: (-scores[d], d))[:top_k]
return [(d, scores[d], evidence[d]) for d in ordered]
if __name__ == '__main__':
allowed = {d for d, meta in DOCS.items()
if meta['tenant'] == 'shop-a' and meta['version'] == 3}
rankings = {'keyword': KEYWORD, 'vector': VECTOR}
result = fuse(rankings, allowed)
for d, score, ranks in result:
print(f'{d} {score:.6f} {ranks} {DOCS[d]["title"]}')
assert [d for d, _, _ in result] == ['D1', 'D2', 'D3']
no_duplicates = {'keyword': ['D1','D4','D2'], 'vector': ['D2','D3','D1']}
assert result == fuse(no_duplicates, allowed)
assert not ({'OLD', 'PRIVATE'} & {d for d, _, _ in result})
assert fuse({}, allowed) == []
assert [d for d, _, _ in fuse(rankings, allowed, window=1)] == ['D1','D2']
print('PASS: 融合排序、单路去重、范围过滤、空结果、窗口截断。')
实际运行python hybrid_demo.py得到:
D1 0.032266 {'keyword': 1, 'vector': 3} ERR_PAY_042:支付请求超时后的查单流程
D2 0.032266 {'keyword': 3, 'vector': 1} 付款后页面一直转圈,应先查询订单状态
D3 0.016129 {'vector': 2} 支付幂等键与重复扣款保护
PASS: 融合排序、单路去重、范围过滤、空结果、窗口截断。
这里有三个重要顺序:先过滤范围,再去掉单路重复ID,最后截取窗口并编号。单路D1重复出现不能得到额外票数;跨路D1出现则应该分别贡献排名分数。
evidence保留各路排名,能解释某个文档为什么靠前。接入服务时,还应记录各路耗时、候选数和过滤数,方便区分“没召回”“召回后被过滤”和“融合后被截断”。
把window改为1,只剩两路各自的第一名D1与D2,D3不可能再进入结果。融合算法只能处理已有候选,扩大最终top_k不能找回在上游窗口之外的证据。
五、接入真实检索系统时怎样组织
可以把融合器的输入适配为如下流程:
查询与范围条件
├─ 关键词检索:返回有序片段ID
└─ 向量检索:返回有序片段ID
↓
稳定ID对齐 → RRF → 重排 → 证据去重与预算分配 → 带引用回答
关键词字段应保留适合分词的正文,同时为错误码、版本等保留精确字段。向量生成端与查询端要使用匹配的嵌入模型及处理方式,不能把不同模型的向量直接放在同一个相似度空间比较。
若使用Elasticsearch,可组合standard与knn子检索器,再交给rrf;查询向量来自实际嵌入服务,维度必须与索引字段一致。索引文档与查询采用同一套嵌入配置,才能得到可比较的向量。
候选窗口、最终返回数和重排输入数要分别设置。例如每路召回50条,融合后保留30条重排,最后按上下文预算选择证据。这里的数字是参数起点,最终应由测试集中的召回覆盖、延迟和模型上下文成本共同决定。
两路可以并行执行。若其中一路超时,系统可以使用另一条路径降级,但日志和指标应记录降级状态;不要将一次单路结果混入正常双路质量统计,导致调参结论失真。
六、怎样判断混合检索确实有收益
准备带相关证据标注的查询集,至少覆盖错误码、自然语言改写、复合条件和相近型号。固定文档版本与评估范围,比较三组:关键词单路、向量单路、混合检索。
| 检查项 | 要回答的问题 |
|---|---|
| Recall@K | 需要的证据是否进入前K条 |
| MRR或nDCG | 重要证据是否排得靠前 |
| 权限与版本正确性 | 是否返回不应出现的片段 |
| 延迟与成本 | 新增一路召回是否值得 |
| 下游回答质量 | 找回更多片段后,答案是否更完整 |
评估时还要看分组表现。错误码类问题可能主要受益于关键词路径,自然语言改写可能主要受益于向量路径;总体平均值会掩盖某一类问题退化。
RRF把原始分差压缩为排名信息,解释简单、接入方便。若某一路的高分与低分差距本身很有价值,可以比较归一化分数融合;如果证据必须精确包含某个标识,应把要求写成查询约束。不要试图只靠融合权重表达所有业务规则。
七、🧠 思维导图
八、总结
总结要点
两路召回互补:关键词抓住标识与词项,向量连接相近语义。共用权限、版本和片段标识,才能把它们合成可用证据。
RRF按排名累计贡献,避免直接混加不同尺度的分数。单路重复去重、候选窗口与平局规则都要明确,才能得到稳定且可解释的结果。
融合之后仍需整理证据。重排、去重和上下文预算继续决定哪些材料进入模型;最终用标注查询与下游答案质量验收收益。
下一篇聊RAG评估:怎样准备问题与证据标注,把召回、排序和最终回答的问题分开定位。
👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/QWERTYwqj/article/details/169559444




