当前位置:首页 > 文章列表 > 科技周边 > 人工智能 > RAG 混合检索结果重复时怎么做去重和排序

RAG 混合检索结果重复时怎么做去重和排序

来源:17golang原创 2026-09-08 01:35:16 0浏览 收藏

RAG 的关键词检索和向量检索各自返回一份候选时,重复结果并不代表召回失败,真正的问题是“重复项按什么身份合并、合并后怎么排”。实用做法是先用稳定的文档 ID 归并两路候选,再保留最有价值的切片;排序上优先使用 RRF(Reciprocal Rank Fusion),因为 BM25 分数和向量相似度通常不在同一尺度上,不能直接相加。

一条可落地的规则是:切片层保留证据,文档层负责去重,排名层使用 RRF,最后再按业务需要做一次轻量重排。对于编号、型号这类精确词,提高关键词通道的候选覆盖比盲目调整向量阈值更有效。
要点速览
  • 先确定去重粒度:同文档不同切片不是同一条结果。
  • 两路结果先带上来源排名,再融合,不要直接比较原始分数。
  • RRF 负责稳定合并,Top-K 截断前保留可解释的 doc_id、chunk_id 和来源。

为什么混合检索会出现重复结果

关键词检索擅长命中“ORD-2026-0187”“ZX-450”这样的原词,向量检索擅长理解“订单被拒绝的原因”和“采购单审核失败”之间的语义关系。两路都找到同一份文档时,如果结果对象使用的是 chunk_id,看起来可能是多条;如果它们属于同一个 doc_id,对用户来说通常只是同一份文档的重复展示。

因此要先定义身份层级。推荐保留 doc_id 作为展示去重键,chunk_id 作为证据定位键。一个文档命中多个切片时,保留融合分数最高的切片;若回答确实需要上下文,再保留一条相邻且信息互补的切片。不要在召回刚返回时按文本完全相等去重,因为相邻切片文字不同,却可能来自同一篇手册。

RAG 混合检索中关键词 BM25 与向量 Embedding 共同产生候选文档的静态关系图
图1:关键词通道负责精确词,向量通道负责语义描述,二者在候选文档层汇合。

两路候选怎样保留有用信息

每条候选至少保存 doc_idchunk_idrankscoresourcesource 可以是 sparsedense,不要在融合前丢掉它,否则出现重复或排序异常时很难判断是哪一路贡献了结果。

两路通常各取比最终 Top-K 更大的候选池,例如最终展示 5 条时先各取 20 条。这个数字不是固定答案,关键是给融合和去重留出空间。编号查询可以让关键词池更充分;自然语言查询则要避免向量池过窄。若过滤条件(租户、权限、文档状态)会影响可见范围,应在两路检索中使用一致的过滤规则。

def merge_candidates(sparse_hits, dense_hits, limit=5):
    # 用文档 ID 做展示层去重,用切片 ID 保留证据位置
    grouped = {}
    for hit in sparse_hits + dense_hits:
        doc_id = hit["doc_id"]
        old = grouped.get(doc_id)
        # 同一文档只留下融合前更有价值的切片,来源和排名仍然保留
        if old is None or hit["score"] > old["score"]:
            grouped[doc_id] = hit
    # 这里先按候选分数排序,正式系统可在此处接入 RRF 分数
    return sorted(grouped.values(), key=lambda item: item["score"], reverse=True)[:limit]

上面的示例只说明“按文档归并”的数据边界,不建议把两路原始分数直接放进同一个排序器。BM25 和向量相似度的数值范围、分布和含义不同,直接相加会让某一路因为量纲更大而长期占优。

RRF 和加权融合该怎么选

RRF 不比较原始分数,只看候选在各自列表里的名次。一个文档在关键词列表排名第 2、在向量列表排名第 5,就把两次排名贡献相加:

def rrf_score(ranks, rank_constant=60):
    # ranks 是同一文档在各召回列表中的名次,名次从 1 开始
    return sum(1.0 / (rank_constant + rank) for rank in ranks)

def fuse_by_rank(sparse_hits, dense_hits, limit=5):
    # 先按 doc_id 建立两路排名,重复文档会自然合并
    ranks = {}
    for source_hits in (sparse_hits, dense_hits):
        for rank, hit in enumerate(source_hits, start=1):
            ranks.setdefault(hit["doc_id"], []).append(rank)
    scored = [
        {"doc_id": doc_id, "score": rrf_score(doc_ranks)}
        for doc_id, doc_ranks in ranks.items()
    ]
    # 最后再截断,避免某一路先截断导致另一条证据失去机会
    return sorted(scored, key=lambda item: item["score"], reverse=True)[:limit]

Elastic 的官方文档把 RRF 定义为合并多个结果集的排名方法,并说明 rank_constant 越大,低排名结果的影响越明显;NVIDIA RAG Blueprint 也把 hybrid 检索和 reranker 作为可配置的检索链路。工程上可以先用 RRF 获得稳定基线,再根据评测数据调整候选池大小和重排策略。

只有当业务明确要求“精确编号必须优先”时,才考虑加权融合。此时先把两路分数分别归一化,再使用例如 0.65 * sparse + 0.35 * dense 的可解释权重;这个比例只是起点,不能当成通用最优值。没有标注查询集时,RRF 通常比凭感觉调权重更容易维护。

RAG 混合检索按文档 ID 去重并通过 RRF 排名输出最终 Top-K 的静态关系图
图2:融合层把关键词候选和向量候选按文档 ID 合并,再输出稳定的最终 Top-K。

去重和排序完成后检查什么

不要只看“最终列表没有重复”就结束。至少准备三类小样本:包含订单号或产品型号的精确查询、完全改写的自然语言查询、同时包含编号和描述的混合查询。分别记录首个正确文档的位置、最终 Top-K 的文档重复率、两路各自覆盖了多少正确文档,以及同一文档保留的是哪个切片。

现象优先检查处理方向
列表有多条同文档结果doc_id 与 chunk_id 是否混用文档层去重,切片层留证据
编号命中靠后关键词候选池和过滤条件扩大 sparse 候选,再观察 RRF
语义结果被精确词挤掉融合前是否过早截断两路先取更大池,最后统一 Top-K

还要把权限过滤、文档状态和索引更新时间纳入回归样本。若同一文档的旧切片与新切片同时出现,应先在索引侧处理版本状态,而不是靠展示层随机删除一条。对 RRF 的 rank_window_size 也要保持稳定,否则分页时可能出现顺序漂移。

RAG 混合检索常见问题

应该按文本内容去重吗?

不建议。文本相同只能说明内容相似,不能可靠识别文档身份。优先使用稳定的 doc_id,再用 chunk_id 定位证据。

RRF 是否还需要向量分数?

RRF 排序本身只使用各列表排名,但可以继续保存原始向量分数用于解释、调试或后续重排,不要把它和 RRF 分数直接混加。

什么时候应该调大候选池?

当正确文档只出现在某一路的较后位置,或去重后剩余文档不足 Top-K 时,先调大该路召回数量,再观察延迟和覆盖率。

同一文档要保留几个切片?

默认保留一个最高贡献切片;回答需要跨段上下文时再增加一个相邻且互补的切片,并设置每个文档的上限,避免上下文被单一文档占满。

混合检索的核心不是把两份列表简单拼接,而是把“身份、排名、证据”三个层次分开:用文档 ID 消除展示重复,用 RRF 处理不同分数尺度,用切片 ID 保留可追溯证据。这样再接入业务权重或 reranker,调参也会有清晰的落点。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go 运行时指标采样间隔太短导致开销变大怎么办Go 运行时指标采样间隔太短导致开销变大怎么办
上一篇
Go 运行时指标采样间隔太短导致开销变大怎么办
Go math/rand/v2 和 crypto/rand 怎么按用途选择
下一篇
Go math/rand/v2 和 crypto/rand 怎么按用途选择
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    14次使用
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    174次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    109次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    36次使用
  • Generrated:DALL·E 2/3 AI绘画提示词灵感库与图像对比平台
    Generrated
    Generrated汇集9300+张DALL·E生成图像及对应提示词,支持查看完整图集、对比DALL·E 2与3版本差异,是AI绘图新手学习Prompt设计与获取创作灵感的实用工具。
    12次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码