code2cat头像
【随笔】RAG混合检索:让关键词与向量一起找证据封面图

【随笔】RAG混合检索:让关键词与向量一起找证据

用户问:“支付报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错误码查单流程131/61+1/63≈0.032266
D2付款后转圈处理311/63+1/61≈0.032266
D3幂等与重复扣款保护未进入21/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条;模型再强也无法利用根本没有召回的其他合格证据。应将过滤下推到检索器,并根据业务选择足够的候选窗口。

两路候选经过RRF融合与重排,再进入证据上下文

四、完整实现一个可检查的融合器

下面使用两路固定的有序候选,集中观察融合行为。代码在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把原始分差压缩为排名信息,解释简单、接入方便。若某一路的高分与低分差距本身很有价值,可以比较归一化分数融合;如果证据必须精确包含某个标识,应把要求写成查询约束。不要试图只靠融合权重表达所有业务规则。

七、🧠 思维导图

RAG混合检索

两路召回

排名融合

证据整理

评估

关键词与语义向量

相同权限与版本

稳定ID与单路去重

RRF与候选窗口

重排与上下文预算

召回质量与延迟

八、总结

总结要点

两路召回互补:关键词抓住标识与词项,向量连接相近语义。共用权限、版本和片段标识,才能把它们合成可用证据。

RRF按排名累计贡献,避免直接混加不同尺度的分数。单路重复去重、候选窗口与平局规则都要明确,才能得到稳定且可解释的结果。

融合之后仍需整理证据。重排、去重和上下文预算继续决定哪些材料进入模型;最终用标注查询与下游答案质量验收收益。

下一篇聊RAG评估:怎样准备问题与证据标注,把召回、排序和最终回答的问题分开定位。

👉 如果你觉得这篇文章对你有所帮助,欢迎点赞、收藏、分享!😊

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/QWERTYwqj/article/details/169559444

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--