ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RAG混合检索实战:稀疏向量+稠密向量+RRF融合优化问答质量

RAG混合检索实战:稀疏向量+稠密向量+RRF融合优化问答质量 去年有个同学把一批内部文档接进了大模型问答系统做成了一个典型的RAG问答系统。第一版很顺利文档能切块能向量化问题抛进去也有答案返回。但真正用业务问题去问他时问题就开始露馅了——文档里明明写着的服务器IP系统答不上来文档里根本不存在的信息大模型反而一本正经地“编”了一段。排查到最后问题不是出在Prompt也不是出在生成模型而是出在召回阶段他只用了向量检索没有做稀疏检索更没有做多路融合。这个案例不是个例。很多RAG项目跑起来之后效果不稳定根因都不是“大模型不够聪明”而是“检索回来的资料不够准”。今天这篇内容就把RAG实战里最容易忽略、又最能决定问答质量的一段技术讲透稀疏向量检索、稠密向量检索以及用RRF倒数排名融合算法把两路结果合并成一张稳定排序表。它不会让大模型变聪明它负责让大模型查到的资料更准。1. 先用一个真实案例说清RAG系统的难点不在“连接”而在“召回”1.1 一个只做了向量检索的RAG为什么会答非所问先复盘那个案例。文档里有一句话“服务器A的固定IP地址是10.0.0.1由网络管理员统一下发。”用户问的是“我们内网那台A节点地址是多少”看起来很简单但只用向量检索就可能出问题。因为向量模型擅长的是语义相似它对“服务器”“节点”“地址”“是多少”这种表达差异有很好的泛化能力但对“10.0.0.1”这样的精确字符串并不敏感。一旦文档库里有大量相似内容向量检索会把语义相近但不包含准确答案的片段也召回来真正的关键片段反而可能被挤到后面。更麻烦的是数字、型号、版本号这类信息。比如“v2.0.1”和“v2.0.10”向量化之后距离很接近语义上也有重叠但对用户来说这是两个完全不同的版本。只依赖稠密向量这类精确匹配几乎必然出错。这不是向量检索不好用而是它的能力边界就是“模糊匹配”。RAG要落地不能只靠模糊匹配。1.2 RAG的本质是给大模型“查资料”不是“背资料”RAG的标准流程并不复杂文档切块、构建索引、用户提问后先做检索召回再把召回的片段拼进Prompt最后交给大模型生成回答。很多新手把重心放在Prompt和生成模型上觉得只要大模型够强答案就能更准。但实际工程里检索质量决定了问答质量的天花板生成模型只是在这个天花板上做文章。你可以把RAG理解成“开卷考试”。大模型不是凭记忆答题而是要先去资料区找到对应段落再组织语言写答案。如果资料区里那几页最关键的内容根本没能被翻出来后面Prompt写得再漂亮也没用。所以做RAG的第一个原则是不要先调Prompt先看检索。检索如果断了后续一切白费。2. 稀疏向量检索为什么关键词精准匹配在RAG里仍然不可丢弃2.1 稠密向量擅长语义但会漏掉精确信息稠密向量检索Dense Retrieval把文本映射到一个高维向量空间然后通过向量距离或相似度来找相关文本。它的优势是能处理同义改写、语义泛化、长句表达。比如用户说“怎么退掉我买的东西”文档里写的是“申请售后并退款”语义上其实一致向量检索能把它们关联起来。但它的短板也很明显。对数字、型号、ID、专有名词、精确枚举值等向量模型很难保证把它们当作“硬约束”。我见过一个典型场景用户问“上海区域的库存是多少”文档里写的是“华东地区库存120件”。如果向量模型没有把“上海”和“华东”的关系学透这个片段可能不会被召回就算召回了也可能同时混进来一大批“华东地区”的其他信息。这时候关键词检索就派上了用场。它不看语义只看词项是否命中。2.2 BM25 与稀疏检索的实现思路在RAG的混合检索方案里稀疏检索最常和BM25绑定。BM25是一种经典的关键词打分算法核心思路是一个文档越包含查询词并且这些词在文档里越少见得分就越高。它不依赖向量模型只依赖分词和词频。下面是中文场景里一个常见的实现结构。用jieba做中文分词用rank_bm25建索引然后查询打分# 示例结构不是完整可运行项目用于说明 BM25 的使用方式 from jieba import lcut from rank_bm25 import BM25Okapi docs [ 华为云ECS弹性云服务器支持按需购买可以随时释放资源。, 服务器A的固定IP地址是10.0.0.1由网络管理员统一下发。, RRF是一种倒数排名融合算法常用于将多路搜索结果合并为单一路径排序。, RAG系统通常包括文档切分、索引构建、召回检索和问答生成几个阶段。, ] tokenized_docs [lcut(doc) for doc in docs] bm25 BM25Okapi(tokenized_docs) query 服务器A的固定IP地址是多少 query_tokens lcut(query) scores bm25.get_scores(query_tokens) sparse_rank sorted(range(len(scores)), keylambda i: scores[i], reverseTrue) print(BM25 召回顺序, sparse_rank)这里有一个常见坑中文必须分词。如果不分词把整句话当成一个“词”去匹配效果会非常差如果切分太细比如把“华为云”拆成“华为”和“云”又会损失专有名词的准确性。实际项目里建议在分词阶段加入自定义词典把公司名、产品名、术语、型号等先保护起来。2.3 稀疏与稠密的互补关系稀疏检索和稠密检索不是替代关系而是互补关系。稀疏检索强在精确匹配和多关键词约束稠密检索强在语义泛化和模糊表达。两者各有缺点维度稀疏检索BM25稠密向量检索匹配方式关键词与词频向量语义相似度擅长场景型号、编号、专有名词、精确短语同义改写、口语化表达、长句意图弱项语义鸿沟换一种说法就召回不到精确信息容易丢失偶发低相关高相似对中文资源要求依赖分词质量依赖向量模型质量可解释性较强能看出命中了哪些词较弱难以解释为什么相似正是因为这种互补关系RAG项目到了中期基本都会走向“混合检索”两路召回同时跑再用一个融合策略把结果合并。3. RRF倒数排名融合把多路检索结果变成一张稳定排序表3.1 为什么要融合而不是简单拼接拿到两路结果后最直接的想法是把两路结果拼在一起去重。但这样会有两个问题第一排序不稳定两路的分数分布差异很大简单地按不同分数混合没有统一口径第二一个文档只在某一路排得比较高但在另一路完全没出现如果只是拼接它可能被埋没在后面。更合理的方式是“融合排序”。融合排序要回答一个问题当两路检索对同一份文档有不同的排序位置时最终顺序怎么定。加权分数融合是一种思路但需要为两路分别调权重且分数分布一变化就要重新调。RRF的优势是它不看原始分数只看排名位置。排名这个信号比分数稳定得多。3.2 RRF公式、手工推导与代码实现RRF的公式很简洁。对于每一条召回结果只累计它在每一路里的排名倒数值score(d) Σ 1 / (k rank_i(d))其中 rank_i(d) 表示文档 d 在第 i 路检索里的排名k 是平滑参数通常取60。来做一个简单推导。假设文档A在稠密检索里排第2在稀疏检索里排第5文档B只在稠密检索里排第9。取 k60文档A得分 1/(602) 1/(605) ≈ 0.0161 0.0154 ≈ 0.0315文档B得分 1/(609) ≈ 0.0145尽管文档A在每一路都不是第一名但因为两路都认为它相关最终反而排到了最前面。这正是RRF的核心价值把“多路共同认可”的文档抬升到前排而不是只依赖某一路的绝对分数。对应代码也不复杂# 示例结构假设 sparse_rank 和 dense_rank 是两路返回的文档索引列表 k 60 rrf_scores {} for rank, doc_id in enumerate(sparse_rank): # 这里把排名从 1 开始计数避免 rank0 时公式失真 rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(dense_rank): rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (k rank 1) final_rank sorted(rrf_scores, keyrrf_scores.get, reverseTrue)不同实现里有人用rank_i从0开始有人从1开始。不影响整体排序但为了统一公式建议代码里把排名从1开始计数。3.3 参数 k 的作用与调参建议k 控制着“排名位置”在融合中的敏感程度。k越小高排名文档的优势越明显k越大融合结果越平滑会低估单个文档的靠前排名更看重“整体出场次数”。实际操作里不需要一开始就精调k。先用60跑通一版再根据效果观察如果觉得融合后精准命中的文档反而被挤下去了可以把 k 调小到30~40如果觉得结果太保守、与单路结果差异不大可以调大到80~100如果文档库非常大、召回链路很多k可以保持60作为基线再去调节各路召回数量。这里有一个朴素的道理RRF不是为了成为一个很高级的算法而是为了让多路结果能稳定合并。在大多数RAG项目里把k从60调到80带来的效果变化远不如把某一路的召回质量做扎实来得明显。4. 一个完整可跑的实战流程稀疏检索 稠密检索 RRF 大模型生成4.1 环境准备与数据准备开始写代码前我建议你先准备一个极小的语料把它跑通再替换成自己的知识库。这样能区分“代码问题”和“业务数据问题”。常见依赖如下可以用 pip 按需安装pip install jieba rank-bm25 faiss-cpu大模型API的调用依赖根据你实际使用的服务接入。当前很多平台提供OpenAI兼容格式也可以用自己本地部署的模型。注意在真实项目中一定要确认好API地址、模型名、鉴权方式和超时设置这一块最容易在环境切换时踩坑。下面准备一个最小演示语料# 示例结构真实项目请替换为你的业务文档 docs [ 华为云ECS弹性云服务器支持按需购买可以随时释放资源。, 服务器A的固定IP地址是10.0.0.1由网络管理员统一下发。, RRF是一种倒数排名融合算法常用于将多路搜索结果合并为单一路径排序。, RAG系统通常包括文档切分、索引构建、召回检索和问答生成几个阶段。, ]文档不要大重点是走通链路。4.2 建立双路召回BM25索引与向量索引先建BM25索引。这一步要注意中文分词并考虑在正式项目中加入自定义词典避免专有名词被拆分。from jieba import lcut from rank_bm25 import BM25Okapi tokenized_docs [lcut(doc) for doc in docs] bm25 BM25Okapi(tokenized_docs)再建稠密向量索引。向量模型可以选用外部API也可以使用本地模型。示例里用embed_texts作为占位函数import faiss import numpy as np def embed_texts(texts): # 示例结构替换为你实际使用的向量模型比如 text-embedding-3-small / bge / m3e 等 pass vectors embed_texts(docs) dim vectors.shape[1] index faiss.IndexFlatIP(dim) # 内积相似度注意向量是否已归一化 index.add(vectors)Faiss 的 IndexFlatIP 是暴力精确检索数据量小的时候最好用数据量大时可以换成 IVF 或 HNSW 等索引。但不要一开始就追求高复杂度先把结果调对再优化性能。4.3 RRF融合排序与TopK选择用户提问后两路分别返回排序结果然后用RRF合并query 服务器A的固定IP地址是多少 query_tokens lcut(query) bm25_scores bm25.get_scores(query_tokens) sparse_rank sorted(range(len(bm25_scores)), keylambda i: bm25_scores[i], reverseTrue) query_vector embed_texts([query])[0].reshape(1, -1) similarities, dense_idx index.search(query_vector, klen(docs)) dense_rank dense_idx[0].tolist() k 60 rrf_scores {} for rank, doc_id in enumerate(sparse_rank): rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(dense_rank): rrf_scores[doc_id] rrf_scores.get(doc_id, 0) 1 / (k rank 1) final_rank sorted(rrf_scores, keyrrf_scores.get, reverseTrue) top_k [docs[i] for i in final_rank[:2]]这里TopK选择也需要思考。TopK太小可能漏掉关键信息TopK太大会浪费大模型上下文甚至引入噪声。对常见FAQ类知识库2到5个片段通常够用对复杂长篇文档可以适当扩大到5到8个但一定要在Prompt里要求模型只依据片段内容回答。4.4 Prompt构造与生成检索完成后就到了RAG流程的“增强”和“生成”阶段。一个有效的Prompt模板应该把资料编号给模型并且明确限制回答范围。context \n\n.join([f[{i1}] {docs[i]} for i in final_rank[:3]]) messages [ {role: system, content: 你是一个知识库问答助手。请只依据提供的资料回答问题如果资料中没有答案请直接说明无法从资料中找到相关信息不要自行补充。}, {role: user, content: f资料内容\n{context}\n\n问题{query}}, ] def call_llm(messages): # 示例结构替换为你实际使用的大模型API pass answer call_llm(messages) print(answer)这段代码看起来简单但有一个容易被忽略的工程点把“资料中没有答案”作为显式约束写进System Prompt。RAG系统和大模型原生对话最大的区别就是“可溯源、可约束”如果这一步不做模型遇到资料缺失时就会用参数记忆去“补全”也就是幻觉。4.5 从单条验证到批量评估单条跑通只代表流程没有断。真正发现问题靠的是批量评估。准备一组测试问题每条最好标注“期望命中的文档ID”或“期望答案要点”。然后批量跑一遍记录每一轮检索是否命中、生成是否包含关键内容。# 示例结构一条极简的批量评估框架 test_cases [ {query: 服务器A的固定IP地址是多少, expected_doc_id: 1}, {query: RAG系统的核心流程包含哪些阶段, expected_doc_id: 3}, ] for case in test_cases: # 这里复用前面的查询、RRF融合逻辑得到 final_rank hit case[expected_doc_id] in final_rank[:3] print(case[query], 命中 if hit else 未命中)这一步看起来朴实但它是后续所有调优的基础。没有评估集任何算法改进都只能靠体感判断没法量化。5. 问答质量怎么评估不要只看“答得对不对”5.1 一个最小可行的评估流程RAG评估最容易犯的错误是只盯生成答案是否正确忽略检索阶段是否已经出错。一个最小可行的评估流程应该是准备测试集每条问题标注期望命中的文档块或答案要点先只跑检索链路统计检索命中率再跑完整链路看生成答案是否能用上检回的资料最后记录失败案例分类是“检索失败”还是“生成失败”。这个流程可以让你分清问题出在哪一层。如果检索命中率已经很低那调整Prompt、换更大参数的生成模型都解决不了根因。5.2 检索阶段指标召回率、命中率、排序质量检索阶段最需要的三个指标指标含义怎么观察命中率TopK结果里是否包含正确答案所在文档直接看每条测试用例是否命中召回速率正确答案在第几顺位出现如果把TopK从1调到5才命中说明排序不够好排序稳定性多次查询同一问题时结果是否一致固定输入重复跑看输出波动这里有一个实操建议先看“第几顺位命中”不要只看“最终有没有命中”。如果正确答案每次都排在第4位而TopK只取前3那调整RRF的k值、调整融合权重、甚至单独提高某一路的召回数是更有效的方向。5.3 生成阶段指标正确性、忠实度、可溯源生成阶段不能只看回答是否通顺还要看正确性答案是否回答了用户问题忠实度答案是否在检索片段里有所依据还是模型自己编的可溯源回答能否对应到具体的资料片段。这也是RAG相对纯大模型对话的巨大优势每一个回答理论上都可以“回头看证据”。上线前可以抽样检查凡是模型答案里出现片段中不存在的具体数值、日期、人名都要视为风险输出。注意RAG不是用来解决大模型胡编乱造的银弹。它的价值是让幻觉“有据可查”减少幻觉对关键决策的干扰。6. 工程化落地中的常见坑点与排查链路6.1 一个容易忽略的问题查询端和文档端描述不一致很多RAG系统上线后效果不佳原因不在算法而在“用户的话术”和“文档的话术”对不上。用户习惯说“售后流程”文档里写的是“退款申请与处理”。虽然意思一致但稀疏检索几乎召回不到稠密检索也要依赖向量模型的理解能力。处理思路有三种在检索前加一个“查询改写”步骤把用户问题扩展成多个同义表达在文档切分前做“知识元补齐”把常见别名写进文档元数据建立同义词表在分词阶段直接映射。这类工作不神秘但能显著提升真实场景下的检索效果。6.2 检索为空或召回差的排查顺序一旦遇到答非所问、回答不出、结果不稳定建议按下面的顺序排查不要一开始就调大模型参数先看现象是检索结果为空、排序不对、还是生成结果不准确再看输入问题文本是否正常查询词是否过于口语化或过于简短再看分词与索引中文分词是否合理自定义词典是否覆盖核心专有名词再看向量模型使用的嵌入模型是否适合中文业务场景再看TopK与融合TopK是否过小RRF的k是否过于敏感两路召回数是否失衡最后看生成Prompt是否明确限制基于资料回答系统提示词是否包含“不知道”的出口。这个顺序的核心逻辑是从数据输入到检索再从检索到生成逐层检查而不是跳到最外层归因。6.3 混合检索和RRF的适用边界混合检索不是所有场景都必须上。它的适用场景很清晰知识库内容有一定规模、用户问题偏事实性、对答案可溯源有要求。比如产品手册问答、企业内部知识库、法律条文查询、售后服务FAQ都非常适合。不适合的场景也很明显开放式闲聊不需要固定答案反而希望模型自由发挥实时数据问答直接连数据库或API更可靠复杂表格推理检索到的片段很难直接作为表格语义输入数据量非常小的演示项目两路召回各自的收益还不明显RRF的价值有限。所以混合检索和RRF是“从Demo走向可用系统”阶段才需要重点投入的方向。如果你只是做一次课程设计单路向量检索也能跑通但你要清楚它的边界。6.4 上线前需要补哪些工程化能力最后回到工程视角。一个能长期稳定的RAG系统除了检索算法还需要补上文档更新机制知识库内容变更后索引如何增量更新日志与监控每一轮问答耗时、召回文档ID、生成内容都要有记录失败案例复盘定期抽看检索失败和生成幻觉案例归纳问题类型权限与合规内部知识库要确认哪些内容允许被检索、被生成避免信息越权Token成本控制不要盲目把TopK拉大也不要每轮都塞大量历史记录。把这些能力补上RAG才能从“能回答”变成“敢上线”。真正决定RAG系统长期价值的不是某个算法有多前沿而是它有没有一套稳定可迭代的工程闭环。如果你现在手里正好有一套文档想接进大模型做问答我的建议是先别急着买GPU、也别急着调Prompt先找10个真实问题把文档切成小块让稀疏检索和稠密检索都跑起来再用RRF合并最后才轮到生成。等这条链路稳定了再谈优化和上线。
返回列表