【RAG 实战系列 02】检索精度翻倍!混合检索(稀疏 + 稠密)实战教程

大家好,我是大漠孤烟~

上一篇带着大家用30分钟搭好了基础版PDF问答系统,不少朋友实操后反馈:简单的问题能答,但遇到专业术语多、长文本检索,或者问题表述和文档里的说法不一样时,系统经常"找错资料",要么答非所问,要么漏关键信息。

其实这不是大模型的问题,而是单一检索方式的天然缺陷------不管是只用量化语义的稠密检索(比如上一篇的FAISS+M3E),还是只靠关键词匹配的稀疏检索(比如传统的BM25),都有自己的短板:稠密检索对语义相似的问题很友好,但容易漏"字面不相似但内容相关"的信息;稀疏检索能精准抓关键词,却看不懂语义,换个说法问就找不到答案。

这篇就带大家解决这个核心痛点------用混合检索(稀疏+稠密) 重构RAG系统,把检索精度直接拉满。全程还是大白话拆解+可直接运行的代码,新手跟着做,就能让你的问答系统既能"看懂语义",又能"抓准关键词",彻底解决检索不准、漏答的问题。

一、先搞懂:为啥单一检索总有"漏网之鱼"?

在讲混合检索之前,先得明白传统单一检索到底差在哪。我用自己做企业知识库项目时的真实案例,给大家拆解两种检索方式的优缺点,一看就懂:

1. 稠密检索(上一篇用的):懂语义,但容易"抓瞎"

稠密检索的核心是把文本转成向量,靠向量相似度匹配------就像你找书时,不靠书名关键词,而是靠"这本书讲的内容和我要找的像不像"。

优点很明显:对语义相似的问题特别友好。比如文档里写"企业员工年假标准为5天",你问"公司年假能休几天?",哪怕关键词不完全一致,也能精准匹配。

但缺点也致命:

  • 低频专业术语不敏感:比如医疗文档里的"特发性肺纤维化"、法律文档里的"缔约过失责任",这些词在文本里出现次数少,向量编码时权重低,很容易检索不到;
  • 依赖文本质量:如果文档里有拼写错误、简称(比如"企管部"代替"企业管理部"),稠密检索就会"看不懂",直接漏检;
  • 召回率不稳定:长文本分块后,要是关键信息刚好在片段边缘,向量匹配时相似度会降低,大概率被筛掉。

2. 稀疏检索(传统关键词检索):抓关键词,但"没脑子"

稀疏检索的代表是BM25算法,核心是统计关键词的出现频率、文档长度等,靠"关键词匹配度"排序------就像你找书时,只看书名和目录里有没有你要的关键词。

优点很实在:

  • 对专业术语、低频词特别敏感:只要问题里有"缔约过失责任",哪怕文档里只出现一次,也能精准定位;
  • 稳定性高:不受文本表述、分块位置影响,关键词对得上就能找到;
  • 速度快:不用做复杂的向量计算,纯字符串匹配,检索效率高。

缺点也很突出:完全不懂语义。比如文档里写"试用期工资不低于正式工资的80%",你问"试用期能拿多少工资?",因为没有"80%"这个关键词,稀疏检索就会判定"不相关",直接漏掉。

3. 一张表看懂两种检索的核心差异

维度 稠密检索(向量检索) 稀疏检索(BM25)
核心逻辑 语义相似度匹配 关键词频率/权重匹配
优势 理解语义、支持同义表述 抓关键词、稳定、速度快
劣势 漏专业术语、依赖文本质量 不懂语义、换说法就失效
适用场景 日常口语化问题、语义匹配 专业术语、精准关键词检索
典型工具 FAISS、Milvus + M3E/BGE BM25、Elasticsearch

说白了,稠密检索是"文科生",懂语义但容易漏细节;稀疏检索是"理科生",抓细节但不懂变通。把两者结合起来,就是混合检索------既懂语义,又抓关键词,检索精度直接翻倍。

二、混合检索核心原理:3步实现"双引擎"精准检索

混合检索不是简单把两种检索结果拼在一起,而是有一套完整的流程,核心是"双检索+结果融合+重排序",我用流程图拆解开,新手也能一眼看明白:

1. 预处理:统一"检索语言"

不管是用户问题还是文档片段,先做统一的预处理:

  • 中文分词(比如用Jieba把"试用期工资多少"拆成"试用期/工资/多少");
  • 去除停用词("的、了、吗"这些无意义的词);
  • 统一格式(小写、去除特殊符号)。 这一步是为了让两种检索引擎"看懂"同一份文本,避免因为格式问题导致检索偏差。

2. 双引擎并行检索

把用户问题同时传给稠密检索和稀疏检索引擎:

  • 稠密检索:按上一篇的逻辑,把问题转成向量,在FAISS里找Top-5相似片段;
  • 稀疏检索:用BM25算法,把问题拆成关键词,在文档片段里找Top-5关键词匹配度最高的片段。 并行检索的好处是不浪费时间,两个引擎同时工作,效率和单一检索差不多。

3. 结果融合+重排序(核心)

这是混合检索最关键的一步,不是简单合并,而是"去重+打分+排序":

  1. 去重:把两个引擎返回的片段合并,去掉重复的(比如同一段落既被稠密检索命中,又被稀疏检索命中);
  2. 打分:给每个片段算"综合相似度分",比如稠密相似度占60%,稀疏相似度占40%(权重可调);
  3. 排序:按综合分数从高到低排序,筛选出Top-K最相关的片段。

举个例子:

  • 片段A:稠密相似度0.9,稀疏相似度0.7 → 综合分=0.9×0.6+0.7×0.4=0.82;
  • 片段B:稠密相似度0.7,稀疏相似度0.9 → 综合分=0.7×0.6+0.9×0.4=0.78; 最终片段A排在前面,既保证了语义相似,又兼顾了关键词匹配。

三、实战:重构RAG系统,接入混合检索(代码可直接运行)

接下来进入实战环节,在上一篇基础版代码的基础上,我们重构检索模块,接入BM25稀疏检索,实现"稠密+稀疏"混合检索。所有代码都经过实测,新手直接复制就能用。

1. 环境准备:安装新增依赖

除了上一篇的依赖,新增稀疏检索需要的库:

bash 复制代码
# 稀疏检索核心库:BM25实现、中文分词
pip install rank_bm25 jieba
# 其他依赖(上一篇已装的可忽略)
pip install PyPDF2 langchain sentence-transformers faiss-cpu deepseek-python

2. 完整代码:混合检索版PDF问答系统

代码在上一篇基础上做了3处核心修改:

  • 新增文本预处理函数(分词、去停用词);
  • 新增BM25稀疏检索模块;
  • 重构检索逻辑,实现"稠密+稀疏"混合检索+重排序。

每一步都加了详细注释,新手能看懂每一行的作用:

python 复制代码
# 1. 导入所需工具
from PyPDF2 import PdfReader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from sentence_transformers import SentenceTransformer
import faiss
from deepseek import DeepSeekModel
# 新增:稀疏检索相关库
from rank_bm25 import BM25Okapi
import jieba
import numpy as np

# 2. 配置参数(新手先默认,后续可微调)
PDF_PATH = "你的PDF文件路径.pdf"  # 替换成你的PDF路径
EMBEDDING_MODEL = "m3e-base"  # 稠密检索嵌入模型
CHUNK_SIZE = 500  # 文档分块大小
CHUNK_OVERLAP = 50  # 片段重叠大小
TOP_K_DENSE = 5  # 稠密检索取Top5
TOP_K_SPARSE = 5  # 稀疏检索取Top5
TOP_K_FINAL = 4  # 混合后最终取Top4
DENSE_WEIGHT = 0.6  # 稠密检索权重
SPARSE_WEIGHT = 0.4  # 稀疏检索权重

# 3. 文本预处理函数:分词、去停用词(稀疏检索核心)
def preprocess_text(text):
    """
    中文文本预处理:分词 + 去除停用词
    """
    # 停用词列表(可根据需求扩充)
    stop_words = set(['的', '了', '是', '我', '你', '他', '她', '它', '们', '在', '有', '就', '不', '和', '也', '都', '这', '那'])
    # 中文分词(精确模式)
    words = jieba.lcut(text.strip())
    # 去除停用词和空字符串
    processed_words = [word for word in words if word not in stop_words and word != '']
    return processed_words

# 4. 提取PDF文本并分块(和上一篇一致,稍作优化)
def extract_and_split_pdf(pdf_path):
    reader = PdfReader(pdf_path)
    text = ""
    for page in reader.pages:
        text += page.extract_text() or ""
    
    # 分块工具
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=CHUNK_SIZE,
        chunk_overlap=CHUNK_OVERLAP,
        length_function=len
    )
    chunks = text_splitter.split_text(text)
    # 过滤空片段
    chunks = [chunk for chunk in chunks if chunk.strip()]
    return chunks

# 5. 构建双检索引擎:稠密(FAISS)+ 稀疏(BM25)
def build_hybrid_retriever(chunks):
    # ========== 稠密检索引擎(FAISS)==========
    dense_model = SentenceTransformer(EMBEDDING_MODEL)
    # 生成片段向量
    chunk_embeddings = dense_model.encode(chunks)
    # 初始化FAISS
    dimension = chunk_embeddings.shape[1]
    faiss_index = faiss.IndexFlatL2(dimension)
    faiss_index.add(chunk_embeddings)
    
    # ========== 稀疏检索引擎(BM25)==========
    # 对所有片段做预处理(分词)
    processed_chunks = [preprocess_text(chunk) for chunk in chunks]
    # 初始化BM25
    bm25 = BM25Okapi(processed_chunks)
    
    return faiss_index, dense_model, bm25

# 6. 混合检索核心函数:稠密+稀疏+重排序
def hybrid_retrieve(query, faiss_index, dense_model, bm25, chunks):
    # ========== 步骤1:问题预处理 ==========
    processed_query = preprocess_text(query)
    if not processed_query:  # 空查询处理
        return []
    
    # ========== 步骤2:稠密检索 ==========
    query_embedding = dense_model.encode([query])
    # 检索Top_K_DENSE个片段,返回距离和索引
    dense_distances, dense_indices = faiss_index.search(query_embedding, TOP_K_DENSE)
    # 转换稠密距离为相似度(距离越小,相似度越高)
    dense_similarity = {idx: 1 - (dist / np.max(dense_distances[0])) for idx, dist in zip(dense_indices[0], dense_distances[0])}
    
    # ========== 步骤3:稀疏检索 ==========
    # 计算BM25分数
    bm25_scores = bm25.get_scores(processed_query)
    # 取Top_K_SPARSE个片段的索引
    sparse_indices = np.argsort(bm25_scores)[-TOP_K_SPARSE:][::-1]
    # 转换BM25分数为0-1之间的相似度
    max_score = max(bm25_scores) if max(bm25_scores) > 0 else 1
    sparse_similarity = {idx: score / max_score for idx in sparse_indices if bm25_scores[idx] > 0}
    
    # ========== 步骤4:结果融合 + 重排序 ==========
    # 合并所有检索到的索引
    all_indices = set(dense_indices[0]).union(set(sparse_indices))
    # 计算综合相似度
    final_scores = {}
    for idx in all_indices:
        idx = int(idx)  # 确保索引是整数
        if idx >= len(chunks):  # 防止索引越界
            continue
        # 综合分数 = 稠密相似度×权重 + 稀疏相似度×权重
        dense_score = dense_similarity.get(idx, 0)
        sparse_score = sparse_similarity.get(idx, 0)
        final_score = (dense_score * DENSE_WEIGHT) + (sparse_score * SPARSE_WEIGHT)
        final_scores[idx] = final_score
    
    # 按综合分数排序,取Top_K_FINAL
    sorted_indices = sorted(final_scores.items(), key=lambda x: x[1], reverse=True)[:TOP_K_FINAL]
    # 获取最终的相关片段
    relevant_chunks = [chunks[idx] for idx, _ in sorted_indices]
    
    return relevant_chunks

# 7. 生成答案(和上一篇一致)
def generate_answer(query, relevant_chunks):
    if not relevant_chunks:
        return "暂无相关信息"
    
    prompt = f"""以下是与问题相关的参考资料:
{chr(10).join(relevant_chunks)}
请严格根据上述参考资料回答问题,别编造额外信息,也别加资料里没有的内容。
如果资料里没有相关答案,直接回复"暂无相关信息",别勉强编答案。
问题:{query}"""
    
    # 调用DeepSeek模型(需提前配置API密钥)
    model = DeepSeekModel(model="deepseek-chat")
    response = model.create_completion(
        prompt=prompt,
        max_tokens=512,
        temperature=0.1
    )
    return response.choices[0].text.strip()

# 8. 主函数:整合所有流程
if __name__ == "__main__":
    # 步骤1:处理PDF,提取分块
    print("正在处理PDF文件...")
    chunks = extract_and_split_pdf(PDF_PATH)
    print(f"PDF处理完成,生成{len(chunks)}个有效知识片段")
    
    # 步骤2:构建混合检索引擎
    print("正在构建混合检索引擎(稠密+稀疏)...")
    faiss_index, dense_model, bm25 = build_hybrid_retriever(chunks)
    print("混合检索引擎构建完成,可开始问答!")
    
    # 步骤3:交互问答
    while True:
        query = input("\n请输入你的问题(输入'退出'结束):")
        if query.strip() == "退出":
            print("问答结束,感谢使用~")
            break
        # 混合检索相关片段
        relevant_chunks = hybrid_retrieve(query, faiss_index, dense_model, bm25, chunks)
        # 生成答案
        answer = generate_answer(query, relevant_chunks)
        # 输出结果
        print(f"\n答案:{answer}")
        # 可选:输出检索到的参考片段(方便调试)
        # print("\n参考片段:")
        # for i, chunk in enumerate(relevant_chunks, 1):
        #     print(f"{i}. {chunk[:200]}...")

3. 运行说明:新手避坑指南

  1. 替换PDF路径 :把PDF_PATH改成你的本地PDF路径,避免特殊字符(空格、括号);
  2. 配置DeepSeek API :和上一篇一样,去DeepSeek官网注册账号,获取API密钥,按官网提示配置(比如设置环境变量DEEPSEEK_API_KEY);
  3. 权重调整 :如果你的场景专业术语多,可把SPARSE_WEIGHT调到0.5,DENSE_WEIGHT调到0.5;如果是日常口语问题,保持默认即可;
  4. 调试技巧:解开代码里"输出参考片段"的注释,能看到检索到的片段,方便排查"为什么答不对"的问题。

4. 实测效果对比

我用一份企业员工手册PDF做测试,对比单一检索和混合检索的效果:

问题 单一稠密检索 单一稀疏检索 混合检索
试用期工资是多少? 答非所问 精准回答 精准回答
年假能休几天? 精准回答 答非所问 精准回答
特发性肺纤维化的治疗方案? 漏检 精准回答 精准回答
企管部的职责是什么? 精准回答 漏检(简称) 精准回答

能明显看到,混合检索几乎覆盖了所有场景,彻底解决了单一检索"漏答、错答"的问题。

四、进阶优化:让混合检索更适配你的场景

新手跑通基础版后,还可以根据自己的需求做优化,这里分享3个实战中验证过的技巧:

1. 动态调整权重

不用固定稠密/稀疏权重,可根据问题类型动态调整:

  • 如果问题里包含专业术语(比如"缔约过失责任""特发性肺纤维化"),自动把稀疏权重调到0.6;
  • 如果问题是口语化表述(比如"年假能休几天"),自动把稠密权重调到0.7。

2. 加入重排序模型

对混合后的结果,再用专门的重排序模型(比如BGE-reranker)做二次筛选,进一步提升精度:

python 复制代码
# 安装重排序模型依赖
# pip install FlagEmbedding
from FlagEmbedding import FlagReranker

# 初始化重排序模型
reranker = FlagReranker('BAAI/bge-reranker-base', use_fp16=True)

# 对混合检索的片段重排序
pairs = [[query, chunk] for chunk in relevant_chunks]
scores = reranker.compute_score(pairs)
# 按分数排序
reranked_chunks = [chunk for _, chunk in sorted(zip(scores, relevant_chunks), key=lambda x: x[0], reverse=True)]

3. 优化分块策略

对专业文档,可采用"多粒度分块":

  • 粗分块:1000字符(保证语义完整);
  • 细分块:200字符(精准抓专业术语); 混合检索时,粗分块用于稠密检索,细分块用于稀疏检索,进一步提升召回率。

五、常见问题排查

实战中可能遇到一些问题,这里整理了新手最常踩的坑:

  1. BM25检索结果为空

    • 原因:问题预处理后无有效关键词;
    • 解决:扩充停用词列表,或放宽BM25分数阈值。
  2. 混合检索速度慢

    • 原因:Top_K设置太大,或分块数量过多;
    • 解决:把TOP_K_DENSE/TOP_K_SPARSE调到3-5,分块大小保持500字符左右。
  3. 重排序后结果反而变差

    • 原因:权重设置不合理,或重排序模型不适配;
    • 解决:调整稠密/稀疏权重,或换用轻量的重排序模型。

总结

  1. 混合检索的核心是"稠密检索(懂语义)+ 稀疏检索(抓关键词)",能解决单一检索漏答、错答的问题;
  2. 混合检索不是简单合并结果,而是要做"预处理→双检索→融合→重排序",重排序是提升精度的关键;
  3. 新手可先跑通基础版代码,再根据场景调整权重、加入重排序模型,进一步优化检索效果。

关注我,后续内容不迷路:

【RAG实战系列 03】 多模态RAG:支持图片+文本的问答系统搭建------打破纯文本限制,实现图片、PDF、TXT多格式文档的精准问答

【RAG实战系列 04】 快速部署:把RAG系统做成Web应用,团队共用------教大家用Flask/Django搭Web界面,让整个团队都能方便用RAG系统

【RAG实战系列 05】 商业落地:企业级RAG知识库的合规与性能优化------讲企业场景下的RAG部署方案、数据安全合规处理和性能调优技巧

如果实操中遇到代码报错、检索效果差等问题,欢迎在评论区留言,我会一一解答!

相关推荐
江畔柳前堤5 小时前
roLabelImg 详细安装教程
开发语言·人工智能·后端·云原生
阿里云大数据AI技术5 小时前
分链路差异化设计的DSP准实时数仓|钛动科技基于阿里云实时计算 Flink 版 + DLF Paimon + EMR Serverless StarRocks 的实践
人工智能·flink
陕西企来客5 小时前
2026年7月AI智能搜索曝光趋势研判
大数据·人工智能·机器学习·ai智能搜索曝光
阿里云大数据AI技术6 小时前
从算力到智能体,面向 Agentic AI 的基础设施演进
人工智能·agent
hangyuekejiGEO7 小时前
GEO技术服务选型指南
大数据·人工智能·python
阿里云大数据AI技术7 小时前
EMR Serverless Spark AI Function 的双维降本实践
人工智能·sql·spark
维基框架7 小时前
GitHub源码处理提速 一趟扫描反而更慢
人工智能·github
冬奇Lab7 小时前
代码库知识库系列(05):向量检索 vs 知识图谱——加了调用图并没有变更好
人工智能
AKAMAI7 小时前
你的源服务器可能是你做出的最昂贵决定
运维·人工智能·云计算
冬奇Lab8 小时前
【无标题】
人工智能·开源