欢迎光临
我们一直在努力

# RAG系统核心结构与四大检索方式全解析:从架构设计到工程落地

RAG系统核心结构与四大检索方式全解析:从架构设计到工程落地

导读:RAG(检索增强生成)看起来简单——“把文档切成块,转成向量,用户提问时搜一下,把结果丢给大模型回答”。但真到生产环境,你会发现检索不准、召回不全、模型还是爱瞎编。问题出在"每一步都有坑"。本文从 RAG 的 5 大核心模块讲起,深度拆解向量检索、关键词检索、混合检索、重排序 4 种检索方式的原理、适用场景和组合策略,附完整代码和选型决策树。

适合读者:

  • 正在搭建或优化 RAG 系统的开发者
  • 需要理解 RAG 全貌和检索选型逻辑的工程师
  • 准备 RAG 方向面试、需要系统回答"RAG系统结构"的同学
  • 对混合检索(向量+关键词+重排序)链路感兴趣的技术人员

阅读收益:

  • 掌握 RAG 5 大核心模块的职责和数据流转
  • 理解 4 种检索方式的原理、优缺点和适用场景
  • 学会设计混合检索链路(向量+BM25→RRF融合→重排序)
  • 获得可直接落地的检索选型决策树和代码实现
  • 了解工业界推荐的"hybrid_rerank"完整链路

目录

  • RAG核心思想:为什么需要检索增强
  • 五大核心模块拆解
  • 四大检索方式深度对比
  • 向量检索:语义理解的利器
  • 关键词检索(BM25):精确匹配的保底
  • 混合检索:RRF融合两路结果
  • 重排序:二次精筛提升质量
  • 完整检索链路代码实现
  • 检索选型决策树
  • 踩坑清单:检索链路的8个关键问题
  • 面试速答版
  • 总结与延伸
  • 文末互动

  • 1. RAG核心思想:为什么需要检索增强

    1.1 大模型的两大痛点

    痛点一:知识过时

    大模型的训练数据有截止日期。2024 年的政策、2025 年的产品更新,模型不知道。

    用户:2024年公积金提取新政策是什么?
    GPT-4(2023年训练):根据我所知,公积金提取需要满足以下条件…
    → 回答的是旧政策,可能已经变了

    痛点二:幻觉(Hallucination)

    模型遇到不知道的问题时,不会说"我不知道",而是会编一个听起来像真的答案。

    用户:你们公司的年假几天?
    大模型:根据一般企业惯例,年假通常是5-15天…
    → 编了一个通用答案,不是你们公司的实际规定

    1.2 RAG的解法

    传统方式:用户问题 → 大模型 → 回答(靠模型记忆,可能过时/编造)
    RAG方式:用户问题 → 检索知识库 → 检索结果+问题 → 大模型 → 回答(基于真实资料)

    核心思想:不让大模型"凭空想象",而是给它"开卷考试"——先检索相关资料,让模型基于检索到的真实文档回答。


    2. 五大核心模块拆解

    2.1 极简记忆口诀

    加载解析 → 文本分块 → 向量化入库 → 检索召回 → Prompt组装生成

    2.2 模块一:文档加载与解析

    职责:把各种格式的原始文档变成纯文本。

    PDF ──→ pypdf ──→ 提取文本 + 页码
    DOCX ─→ python-docx ──→ 提取段落 + 样式
    TXT ──→ 直接读取
    网页 ─→ requests + BeautifulSoup ──→ 提取正文

    关键注意点:

    • PDF 解析要保留页码和章节信息(用于后续引用标注)
    • 去重:同一文档多次上传会生成重复切片
    • 过滤:页眉页脚、页码、水印等无效内容要清洗掉

    2.3 模块二:文本切分(Chunk分块)

    职责:把长文档切成大小合适的文本块。

    为什么不能太大?
    → 向量语义混杂,一个块里包含多个主题,检索精度下降

    为什么不能太小?
    → 丢失完整语义,"上下文"残缺,模型看不懂

    常用策略:
    – 固定长度切分:简单,但不保语义边界
    – 递归字符切分:优先在段落、句子边界切分
    – 语义切分:用模型判断语义边界,计算成本高
    – 标题感知切分:按章节标题切分,保留上下文

    推荐方案(父子分块):

    # 父块:1200字符,用于召回时提供完整上下文
    # 子块:420字符,用于精确匹配
    # overlap:80字符,保证边界不截断语义

    # 实际检索时:
    # 1. 用子块做向量检索(小块匹配更精准)
    # 2. 返回对应的父块内容(给模型更多上下文)

    2.4 模块三:向量化与入库

    职责:把文本块转成向量,存入向量数据库。

    文本块 ──→ Embedding模型(如bge-m3)──→ 1024维向量

    向量数据库(Milvus/Chroma/FAISS)

    存储:向量 + 原始文本 + 元数据

    关键注意点:

    • 文档和查询要用同一个 Embedding 模型
    • 不同模型的向量维度不同(bge-m3是1024维,text-embedding-ada-002是1536维)
    • 向量库要支持元数据过滤(按部门、时间、状态过滤)

    2.5 模块四:检索召回

    职责:用户提问时,找到最相关的文本片段。

    用户问题 ──→ Embedding ──→ 查询向量

    向量库相似度搜索 ──→ Top-K相关片段

    可选:BM25关键词检索 ──→ Top-K相关片段

    RRF融合两路结果 ──→ 融合后的Top-K

    重排序精筛 ──→ 最终Top-N送入LLM

    2.6 模块五:Prompt组装与生成

    职责:把检索结果和提问拼接成 Prompt,让大模型基于资料回答。

    system_prompt = """你是一个企业政策问答助手。
    请只基于下面提供的参考资料回答用户问题。
    如果参考资料中没有相关信息,请如实说明。
    每条关键结论必须标注引用来源。

    参考资料:
    {context}
    """

    # context 由检索到的 Top-N 片段拼接而成


    3. 四大检索方式深度对比

    检索方式原理优点缺点适用场景
    向量检索 Embedding转向量,算余弦相似度 懂语义,同义词/转述也能召回 专有名词/数字/ID精确匹配差 语义类问答
    关键词检索(BM25) 基于词频和逆文档频率 专有名词/编号精确匹配好,速度快 不懂语义,同义词召回差 精确查询
    混合检索(Hybrid) 向量+关键词并行,RRF融合 兼顾语义+精确,召回全面提升 计算量略大,维护两套链路 生产首选
    重排序(Rerank) 用Cross-Encoder精排问题-文档相关性 大幅提升上下文质量,减少噪声 增加额外延迟,非初次召回 精度要求高

    4. 向量检索:语义理解的利器

    4.1 原理

    文本 → Embedding模型 → 稠密向量(如1024维)

    查询向量 vs 文档向量:
    余弦相似度 = (A·B) / (|A| × |B|)

    值域:[-1, 1]
    1 = 完全相同方向(语义最相似)
    0 = 正交(无关)
    -1 = 相反方向(极少出现)

    4.2 为什么向量检索"懂语义"

    传统关键词匹配:
    "怎么申请公积金提取" 和 "住房公积金提取流程"
    → 字面完全不同,关键词匹配不到

    向量检索:
    两个句子语义相近 → 向量在空间中距离近 → 被召回

    4.3 向量检索的短板

    场景1:用户问"订单A1002的状态"
    → "A1002"在向量空间中只是随机向量
    → 向量检索找不到精确匹配

    场景2:用户问"2024年3月的退款政策"
    → "2024""3月"作为数字,语义信息弱
    → 向量检索容易召回其他年份的政策

    场景3:用户问"BOS产品定价"
    → "BOS"是专有名词缩写
    → 向量检索可能召回"BOSS""BOC"等无关内容


    5. 关键词检索(BM25):精确匹配的保底

    5.1 原理

    BM25 是一种基于概率模型的关键词检索算法。核心思想:

    文档相关性得分 =
    查询词在文档中出现的频率(TF)
    × 查询词的稀有程度(IDF,越稀有的词权重越高)
    × 文档长度归一化(避免长文档占便宜)

    5.2 为什么需要BM25保底

    # 场景:用户查询订单号
    question = "订单 A1002 发货了吗"

    # 向量检索:"A1002"是专有名词,向量相似度低 → 可能召回不到
    # BM25检索:直接匹配"A1002"这个词 → 精确命中

    # 另一个场景:用户问具体政策条款
    question = "2024年公积金提取新政策"

    # 向量检索:可能召回2023年、2025年的政策
    # BM25检索:精确匹配"2024""公积金""提取" → 命中准确

    5.3 中文BM25的实现

    """中文BM25检索实现"""
    import jieba
    from rank_bm25 import BM25Okapi

    class ChineseBM25Retriever:
    def __init__(self, documents: list[str]):
    # jieba分词
    self.tokenized_docs = [
    list(jieba.cut(doc)) for doc in documents
    ]
    self.bm25 = BM25Okapi(self.tokenized_docs)
    self.documents = documents

    def search(self, query: str, top_k: int = 10) > list[tuple[str, float]]:
    tokenized_query = list(jieba.cut(query))
    scores = self.bm25.get_scores(tokenized_query)

    # 取Top-K
    top_indices = sorted(
    range(len(scores)),
    key=lambda i: scores[i],
    reverse=True,
    )[:top_k]

    return [
    (self.documents[i], scores[i])
    for i in top_indices
    ]


    6. 混合检索:RRF融合两路结果

    6.1 为什么需要混合

    向量检索强:语义理解、同义词召回、转述匹配
    向量检索弱:专有名词、数字ID、精确术语

    BM25强:精确匹配、专有名词、关键词命中
    BM25弱:语义理解、同义词、转述表达

    → 两者互补,融合后召回更全面

    6.2 RRF融合算法

    RRF(Reciprocal Rank Fusion)是一种简单有效的融合算法:

    def rrf_fusion(
    vector_results: list[Document], # 向量检索结果(已按相似度排序)
    bm25_results: list[Document], # BM25检索结果(已按分数排序)
    k: int = 60, # 平滑常数
    ) > list[Document]:
    """RRF融合:倒数排名融合

    原理:每条文档的得分 = 1/(k + rank)
    两路结果中同一文档的得分相加,总分越高排名越靠前

    k=60 是经验值,防止排名靠后的文档得分衰减过快
    """
    scores = {}

    # 向量检索结果赋分
    for rank, doc in enumerate(vector_results):
    doc_id = doc.metadata["chunk_id"]
    scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)

    # BM25结果赋分
    for rank, doc in enumerate(bm25_results):
    doc_id = doc.metadata["chunk_id"]
    scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)

    # 按总分排序
    sorted_ids = sorted(
    scores.keys(),
    key=lambda doc_id: scores[doc_id],
    reverse=True,
    )

    # 返回融合后的文档
    doc_map = {d.metadata["chunk_id"]: d for d in vector_results + bm25_results}
    return [doc_map[doc_id] for doc_id in sorted_ids]

    6.3 RRF为什么有效

    文档A:向量检索排第2,BM25排第10
    → RRF得分 = 1/(60+2) + 1/(60+10) = 0.016 + 0.014 = 0.030

    文档B:向量检索排第8,BM25排第3
    → RRF得分 = 1/(60+8) + 1/(60+3) = 0.015 + 0.016 = 0.031

    文档C:只在向量检索排第1,BM25没出现
    → RRF得分 = 1/(60+1) = 0.016

    → 文档B总分最高(两路都表现不错),文档A次之(一路好一路一般),
    文档C较低(只有一路召回)

    核心思想:两条链路都召回的文档更可信,单路召回的文档可能 noise 较大。


    7. 重排序:二次精筛提升质量

    7.1 为什么需要重排序

    向量检索和BM25只看"片段本身"的相似度:
    "这个片段和查询词有多像?"

    但实际需要的是"这个片段能不能回答用户的问题":
    "这个片段和问题-文档对的相关性有多高?"

    重排序模型(Cross-Encoder)直接对"问题+文档"做联合编码,
    输出相关性分数,精度远高于向量相似度。

    7.2 重排序原理

    向量检索(Bi-Encoder):
    问题 ──→ Embedding模型 ──→ 向量A
    文档 ──→ Embedding模型 ──→ 向量B
    相似度 = cos(向量A, 向量B)
    → 问题和文档分别编码,速度快但精度有限

    重排序(Cross-Encoder):
    [问题] + [SEP] + [文档] ──→ Cross-Encoder模型 ──→ 相关性分数
    → 问题和文档联合编码,精度高但速度慢

    7.3 完整重排序实现

    """重排序精筛实现"""
    from langchain_core.documents import Document

    class Reranker:
    def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
    # 加载Cross-Encoder重排序模型
    self.model = load_rerank_model(model_name)

    def rerank(
    self,
    query: str,
    documents: list[Document],
    top_k: int = 5,
    ) > list[tuple[Document, float]]:
    """对候选文档重排序,返回(文档, 分数)列表"""
    pairs = [(query, doc.page_content) for doc in documents]

    # 批量打分
    scores = self.model.predict(pairs)

    # 按分数排序
    scored_docs = list(zip(documents, scores))
    scored_docs.sort(key=lambda x: x[1], reverse=True)

    return scored_docs[:top_k]

    def filter_evidence(
    self,
    scored_docs: list[tuple[Document, float]],
    threshold: float = 0.3,
    ) > list[Document]:
    """过滤低分文档,低于阈值的不送入LLM"""
    return [doc for doc, score in scored_docs if score >= threshold]

    7.4 重排序在链路中的位置

    完整检索链路:

    用户问题

    并行执行:
    ├── 向量检索(召回Top-10)
    └── BM25检索(召回Top-10)

    RRF融合(20条 → 排序取Top-15)

    重排序(15条 → 按问题-文档相关性打分)

    过滤低分证据(阈值0.3,保留Top-5)

    送入LLM生成回答


    8. 完整检索链路代码实现

    """完整RAG检索链路实现"""
    import asyncio
    from dataclasses import dataclass
    from typing import Literal
    from langchain_core.documents import Document

    @dataclass
    class RetrievalResult:
    """检索结果数据结构"""
    documents: list[Document] # 最终送入LLM的文档
    mode: str # 使用的检索模式
    trace: dict # 调试追踪信息

    class RAGRetriever:
    def __init__(
    self,
    vectorstore, # 向量数据库
    bm25_retriever, # BM25检索器
    reranker=None, # 重排序模型
    vector_top_k: int = 10,
    bm25_top_k: int = 10,
    rerank_top_k: int = 5,
    score_threshold: float = 0.3,
    ):
    self.vectorstore = vectorstore
    self.bm25 = bm25_retriever
    self.reranker = reranker
    self.vector_top_k = vector_top_k
    self.bm25_top_k = bm25_top_k
    self.rerank_top_k = rerank_top_k
    self.score_threshold = score_threshold

    def _vector_search(self, query: str) > list[Document]:
    """向量检索"""
    return self.vectorstore.similarity_search(
    query, k=self.vector_top_k
    )

    def _bm25_search(self, query: str) > list[Document]:
    """BM25关键词检索"""
    return self.bm25.search(query, top_k=self.bm25_top_k)

    def _rrf_fusion(
    self,
    vector_results: list[Document],
    bm25_results: list[Document],
    k: int = 60,
    ) > list[Document]:
    """RRF倒数排名融合"""
    scores = {}
    doc_map = {}

    for rank, doc in enumerate(vector_results):
    doc_id = doc.metadata.get("chunk_id", str(id(doc)))
    scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    doc_map[doc_id] = doc

    for rank, doc in enumerate(bm25_results):
    doc_id = doc.metadata.get("chunk_id", str(id(doc)))
    scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
    doc_map[doc_id] = doc

    sorted_ids = sorted(
    scores.keys(),
    key=lambda x: scores[x],
    reverse=True,
    )

    return [doc_map[doc_id] for doc_id in sorted_ids]

    def _rerank(
    self,
    query: str,
    documents: list[Document],
    ) > list[tuple[Document, float]]:
    """重排序"""
    if not self.reranker or not documents:
    return [(doc, 0.0) for doc in documents]

    return self.reranker.rerank(
    query, documents, top_k=self.rerank_top_k
    )

    async def retrieve(
    self,
    query: str,
    mode: Literal["vector", "hybrid", "hybrid_rerank", "full"] = "full",
    ) > RetrievalResult:
    """统一检索入口

    四种模式:
    – vector:纯向量检索
    – hybrid:向量+BM25,RRF融合
    – hybrid_rerank:hybrid+重排序
    – full:hybrid_rerank+证据过滤
    """
    trace = {"query": query, "mode": mode}

    # 1. 向量检索
    vector_results = await asyncio.to_thread(
    self._vector_search, query
    )
    trace["vector_count"] = len(vector_results)

    if mode == "vector":
    return RetrievalResult(
    documents=vector_results[:self.rerank_top_k],
    mode=mode,
    trace=trace,
    )

    # 2. BM25检索(并行)
    bm25_results = await asyncio.to_thread(
    self._bm25_search, query
    )
    trace["bm25_count"] = len(bm25_results)

    # 3. RRF融合
    fused = self._rrf_fusion(vector_results, bm25_results)
    trace["fused_count"] = len(fused)

    if mode == "hybrid":
    return RetrievalResult(
    documents=fused[:self.rerank_top_k],
    mode=mode,
    trace=trace,
    )

    # 4. 重排序
    reranked = self._rerank(query, fused[:15])
    trace["rerank_count"] = len(reranked)

    if mode == "hybrid_rerank":
    return RetrievalResult(
    documents=[doc for doc, _ in reranked],
    mode=mode,
    trace=trace,
    )

    # 5. 过滤低分证据
    evidence = [
    doc for doc, score in reranked
    if score >= self.score_threshold
    ]
    trace["evidence_count"] = len(evidence)
    trace["refused"] = len(evidence) == 0

    return RetrievalResult(
    documents=evidence,
    mode=mode,
    trace=trace,
    )


    9. 检索选型决策树

    开始

    ├── 数据量 < 10万?
    │ ├── 是 → 用FLAT暴力扫描(测试环境)
    │ └── 否 → 继续

    ├── 是否需要语义理解?
    │ ├── 否 → 只用BM25关键词检索
    │ └── 是 → 继续

    ├── 是否需要精确匹配?
    │ ├── 否 → 只用向量检索
    │ └── 是 → 混合检索(向量+BM25)

    ├── 对精度要求极高?
    │ ├── 否 → hybrid模式(RRF融合)
    │ └── 是 → hybrid_rerank模式(融合+重排序)

    └── 生产环境?
    ├── 否 → vector/hybrid即可
    └── 是 → full模式(融合+重排序+证据过滤)

    生产推荐:full 模式 = 向量 + BM25(并行)→ RRF 融合 → 重排序 → 过滤低分证据


    10. 踩坑清单:检索链路的8个关键问题

    序号问题现象原因解决方案
    1 只用向量检索 专有名词/数字召回不到 向量对精确匹配弱 加BM25做混合检索
    2 只用BM25检索 同义词/转述召回不到 BM25不懂语义 加向量做混合检索
    3 串行执行两路检索 延迟翻倍 没并行 asyncio.gather并行
    4 top-k太大 重排序成本爆炸,噪声多 召回过多 向量/BM25各召回10条,rerank处理15条
    5 不重排序 低质量片段混入,LLM输出差 向量相似度≠问题相关性 加Cross-Encoder重排序
    6 不过滤低分证据 无关内容送入LLM 缺少阈值过滤 设score_threshold=0.3
    7 文档和查询用不同Embedding 召回率暴跌 向量空间不一致 统一使用同一个模型
    8 切片不带章节标题 语义不完整 缺少上下文 父子分块,子块带父块标题

    11. 面试速答版

    RAG系统分5大模块:加载解析→文本分块→向量化入库→检索召回→Prompt组装生成。检索有4种方式:向量检索懂语义但精确匹配弱,BM25精确匹配好但不懂语义,混合检索用RRF融合两路结果,重排序用Cross-Encoder精筛问题-文档相关性。生产推荐full模式:向量+BM25并行→RRF融合→重排序→过滤低分证据。记住:向量找意思相近的,BM25找字面匹配的,RRF把两路结果融合,Rerank再精排一遍挑最好的喂给大模型。


    12. 总结与延伸

    12.1 核心知识点回顾

    RAG五模块:加载解析 → 分块 → 向量化入库 → 检索召回 → Prompt生成

    四种检索:
    向量检索:语义理解,同义词召回
    BM25检索:精确匹配,关键词命中
    混合检索:RRF融合两路,互补增强
    重排序:Cross-Encoder精筛,提升质量

    生产链路:
    向量(Top-10) + BM25(Top-10) ──并行──┐

    RRF融合(取Top-15)

    重排序(Top-5)

    过滤低分(阈值0.3)

    送入LLM

    12.2 延伸方向

    • 多路召回:向量 + BM25 + 图检索 + 知识图谱,多路融合
    • 查询扩展:对查询做同义词扩展,提升向量检索召回
    • 查询重写:多轮对话中补全指代和省略(已在前文讲解)
    • 自适应检索:根据问题类型自动选择检索策略(精确查询走BM25,开放问题走向量)

    13. 文末互动

    你的 RAG 项目用的是哪种检索方式——纯向量、纯BM25、还是混合检索?有没有遇到"向量检索召回不到但BM25能命中"的情况?评论区聊聊你的实战经验。

    思考题:如果一个知识库里同时包含"产品说明书"(适合向量检索)和"订单查询"(适合BM25精确匹配),你的检索系统应该如何根据问题类型自动选择检索策略?欢迎在评论区讨论。


    本文从 RAG 系统全貌出发,完整拆解了五大模块和四大检索方式。如果觉得有帮助,欢迎点赞收藏,后续会更新多路召回和自适应检索的进阶内容。

    赞(0)
    未经允许不得转载:171主机测评 » # RAG系统核心结构与四大检索方式全解析:从架构设计到工程落地
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址