跳转至
RAG基础与环境RAG 核心概念深入

第 2 章:RAG 核心概念深入

上一章:项目概述与 Docker 环境搭建 下一章:LangChain 生态系统

本章目标

  • 深入理解向量检索的数学原理
  • 掌握 Embedding 模型的工作机制
  • 理解 Dense 检索和 Sparse 检索的区别与互补
  • 了解 Reranker(重排)在 RAG 链路中的作用

第一部分:前置知识 — Embedding 模型

1.1 什么是 Embedding

Embedding(嵌入) 是将非结构化数据(文本、图片、音频)转换为固定长度的浮点数向量的技术。

flowchart LR
    subgraph Input["输入:自然语言"]
        T1["入职流程需要准备哪些材料"]
    end

    subgraph Model["Embedding 模型 (BGE-M3)"]
        M["Transformer 编码器<br/>将语义映射到<br/>1024 维向量空间"]
    end

    subgraph Output["输出:向量表示"]
        V["[0.023, -0.451, 0.782, ..., 0.134]<br/>1024 个浮点数"]
    end

    Input --> Model --> Output

    subgraph Concept["概念类比"]
        C1["语义相近的文本"]
        C2["在向量空间中距离近"]
        C3["语义无关的文本"]
        C4["在向量空间中距离远"]
    end

    C1 --> C2
    C3 --> C4

    style Model fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
    style Output fill:#ECFDF5,stroke:#059669,stroke-width:2px

在文本领域,Embedding 模型接收一段文字,输出一串数字(通常是 512 到 4096 维的向量),这个向量在数学空间中代表了这段文字的"语义位置"。

1.2 为什么要 Embedding

计算机本质上只能做数值计算。要让计算机"理解"两段文字是否相似,必须先把文字变成它可以计算的数字。

早期方法(如 TF-IDF、Bag-of-Words)只统计词频和共现关系,无法理解语义: - "我很开心" 和 "我非常高兴" 的词完全不同,但语义相同 - "银行利率很高" 和 "河边的银行很美" 词相同,但语义完全不同

现代 Embedding 模型基于 Transformer 架构,通过大规模预训练学会了理解上下文中的语义。同一个词在不同上下文中会产生不同的向量表示。

1.3 向量相似度计算

Embedding 把文本变成向量以后,检索系统要回答一个问题:用户问题向量和知识库中哪个 chunk 向量更接近?

这里要先区分两个概念:

  • 相似度(similarity):分数越大越相似,例如 Cosine、Inner Product
  • 距离(distance):数值越小越接近,例如 Euclidean Distance

向量数据库通常会把距离或相似度统一转成“可排序的分数”。所以你在代码里看到的 score,不一定都严格等于数学公式原始值,而是 Milvus / LangChain 返回的检索排序分数。

本项目在第 10 章会额外返回 answer_confidence。它和这里的 score 不是一回事:score 表示单条候选的检索相关性;answer_confidence 是最终答案综合置信度,内部会先用 evidence_confidence 衡量生成前证据是否扎实,再用 generation_verification 核验 LLM 生成答案的行内引用、引用编号和上下文词面支撑度。

1.3.1 余弦相似度:看方向

余弦相似度 (Cosine Similarity) — 最常用

\[\text{cosine}(A, B) = \frac{A \cdot B}{||A|| \times ||B||}\]

其中:

  • \(A \cdot B\) 是两个向量的点积
  • \(||A||\)\(||B||\) 是两个向量的长度
  • 分母的作用是把长度影响消掉,只比较两个向量的方向
  • 取值范围 [-1, 1]
  • 1 表示方向完全一致,0 表示正交(不相关),-1 表示方向相反
  • 只看方向不看长度,适合文本语义比较

直观理解:

"新人入职流程"       → 方向接近 "新员工报到步骤"       → cosine 高
"新人入职流程"       → 方向远离 "VPN 连接失败"         → cosine 低

1.3.2 欧几里得距离:看直线距离

欧几里得距离 (Euclidean Distance) — 适合 L2 归一化后的向量

\[d(A, B) = \sqrt{\sum_{i=1}^{n}(A_i - B_i)^2}\]

欧氏距离比较的是两个点在向量空间中的直线距离。它是“距离”,所以:

  • 数值越小,表示越相似
  • 数值越大,表示越不相似

如果向量已经做过 L2 归一化,Cosine、L2、IP 的排序结果会非常接近。原因是所有向量都被压到单位球面上,长度都约等于 1,这时主要差别就来自方向。

1.3.3 内积:看方向和长度的乘积

内积 (Inner Product / Dot Product)

\[IP(A, B) = \sum_{i=1}^{n}A_i \times B_i\]

内积可以理解为“两个向量在同一方向上的重合程度”。如果向量没有归一化,内积会同时受方向和长度影响;如果向量已经 L2 归一化,内积和余弦相似度等价:

\[ ||A|| = ||B|| = 1 \Rightarrow \text{cosine}(A, B) = A \cdot B \]

所以很多向量检索系统会使用:

  • COSINE:语义检索中最容易解释
  • IP:归一化后效果等价,计算更直接
  • L2:适合一些几何距离场景
import numpy as np

# 两个语义相近的句子的向量
A = np.array([0.5, 0.3, 0.8, ...])  # "入职需要什么材料"
B = np.array([0.48, 0.32, 0.79, ...])  # "入职要准备哪些文件"

cosine_sim = np.dot(A, B) / (np.linalg.norm(A) * np.linalg.norm(B))
# 结果 ≈ 0.95(很高)

# 两个语义不同的句子的向量
C = np.array([-0.3, 0.7, -0.2, ...])  # "今天天气怎么样"

cosine_sim_ac = np.dot(A, C) / (np.linalg.norm(A) * np.linalg.norm(C))
# 结果 ≈ 0.1(很低)

1.3.4 本项目在哪里用相似度计算

本项目不是只在一个地方做“相似度计算”,而是在检索链路中分层使用:

使用位置 代码位置 计算方式 作用
Dense 向量检索 qa_core/retrieval/store.pysimilarity_search_with_score() BGE-M3 生成 1024 维 dense 向量,Milvus 按 dense 向量字段检索 找到语义相近的 FAQ / 文档 chunk
Dense 索引示例 第 4 章 pymilvus 示例 metric_type="COSINE" 示例展示 Milvus 如何按向量相似度搜索
Sparse BM25 检索 qa_core/retrieval/milvus_compat.pyBM25BuiltInFunction 中文分词 + BM25 词频/逆文档频率评分 找到精确包含关键词、术语、编号的内容
Hybrid 融合 qa_core/retrieval/store.py ranker_type="weighted",权重 [0.55, 0.45] 将 dense 语义分数和 sparse BM25 分数融合排序
CrossEncoder 重排 qa_core/retrieval/ranking.py query + passage 成对输入 reranker 模型 对 Milvus 召回候选做二阶段精排

本项目的核心检索配置可以概括为:

用户问题
  → BGE-M3 生成 dense query vector
  → Milvus BM25 Function 生成 sparse query representation
  → Milvus 同时检索 dense 字段和 sparse 字段
  → WeightedRanker 按 0.55 / 0.45 融合
  → CrossEncoder Reranker 二阶段重排

1.3.5 Sparse 检索也是用余弦相似度吗?

不是。至少在本项目中不是。

Dense 检索比较的是 BGE-M3 生成的连续浮点向量,例如 1024 维:

dense = [0.012, -0.034, 0.876, ...]

这类向量适合用 Cosine / IP / L2 这类几何相似度计算。

Sparse 检索虽然也叫“稀疏向量”,但本项目的 sparse 字段由 Milvus 的 BM25 Function 生成,核心思想不是“两个语义向量夹角有多小”,而是:

query 里出现的词,是否也出现在文档里?
这个词在当前文档中出现得多不多?
这个词在整个语料库中稀不稀有?
这篇文档是不是因为太长而天然占便宜?

BM25 的典型形式是:

\[\text{BM25}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \times \frac{f(q_i, D) \times (k_1 + 1)}{f(q_i, D) + k_1 \times (1 - b + b \times \frac{|D|}{\text{avgdl}})}\]

它关注的是:

  • TF:词在当前文档里出现多少次
  • IDF:词在整个语料中稀不稀有,越稀有越重要
  • 长度归一化:长文档不能因为词多就天然分数高

可以这样总结:

Dense 相似度:比较语义向量,常见公式是 Cosine / IP / L2。
Sparse BM25:比较关键词匹配贡献,核心是词频、逆文档频率和文档长度归一化。
Hybrid Search:不是让两者用同一个公式,而是分别计算各自分数,再融合排序。

1.4 BGE-M3 模型介绍

本项目使用的是 BGE-M3(BAAI General Embedding M3),由北京智源研究院(BAAI)开发。

📖 深入学习:如果你想了解 Embedding 模型的完整工作原理(Tokenization → Transformer → Pooling → 向量输出)、维度选择、模型对比和 L2 归一化,请阅读 附录F:Embedding 模型深入。

BGE-M3 的核心特点

特性 说明
多语言 支持中英双语及 100+ 语言
多粒度 支持短句到长文档(最多 8192 token)的向量化
多功能 同时支持 Dense 检索、Sparse 检索和 Multi-Vector 检索
维度 默认输出 1024 维 Dense 向量
部署 可在本地 GPU/CPU 上运行,不需要调用外部 API

在本项目中,BGE-M3 默认放在项目 ./models/bge-m3/ 目录下;Docker 模式会挂载到容器内 /app/models/bge-m3/,再通过 LangChain 的 Embedding 接口调用:

# qa_core/retrieval/models.py — 获取 Embedding 模型
from langchain_huggingface import HuggingFaceEmbeddings
from qa_core.config.settings import get_settings

def get_embeddings():
    settings = get_settings()
    return HuggingFaceEmbeddings(
        model_name=settings.embedding_model_path,
        model_kwargs={"device": "cpu", "local_files_only": True},  # 或 "cuda"
        encode_kwargs={"normalize_embeddings": True},  # L2 归一化
    )

第二部分:向量数据库在 RAG 中的职责

Embedding 只负责把文本变成向量,向量数据库负责保存和检索这些向量。一次在线查询通常需要完成:

query → embedding → Top-K 向量检索 → metadata 过滤 → 返回候选文本
能力 在 RAG 中的作用
向量存储 保存文档 chunk、FAQ 的向量及原文
ANN 检索 从大量向量中快速召回近似 Top-K
标量过滤 限制场景、版本、租户、角色和 source
集合管理 隔离不同数据结构和业务数据

普通关系查询擅长精确条件,全文检索擅长关键词匹配,向量数据库擅长语义相似度搜索。企业 RAG 往往需要把这些能力组合起来,而不是让一种存储承担所有任务。

本项目选择 Milvus,但本章不展开 Milvus 的 Collection、Schema、索引、部署架构和产品选型。这些内容统一放在第 04 章:Milvus 索引机制与基本操作。


第三部分:Dense 检索 vs Sparse 检索

这是本项目最核心的检索概念。让我们用一个具体的例子来理解。

flowchart TD
    Q["❓ 用户问题<br/>'入职需要准备什么材料'"] --> Dense["🧮 Dense 检索<br/>(BGE-M3 Embedding)"]
    Q --> Sparse["📝 Sparse 检索<br/>(Milvus BM25)"]

    Dense --> D1["语义理解<br/>→ '入职/报到/新员工'"]
    D1 --> D2["同义词扩展<br/>→ '准备/提交/携带'"]
    D2 --> D3["✅ 优势:理解语义<br/>❌ 劣势:精确术语弱"]

    Sparse --> S1["关键词匹配<br/>→ '入职' 命中 3 次"]
    S1 --> S2["精确匹配<br/>→ 'HS编码8471.30' 精确命中"]
    S2 --> S3["✅ 优势:精确匹配<br/>❌ 劣势:不理解同义词"]

    D3 --> Merge["🔀 混合检索结果<br/>Dense分数 + Sparse分数<br/>加权融合排序"]
    S3 --> Merge
    Merge --> Result["📊 互补优势<br/>既有语义覆盖<br/>又有精确匹配"]

    style Dense fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px
    style Sparse fill:#FFFBEB,stroke:#D97706,stroke-width:2px
    style Merge fill:#ECFDF5,stroke:#059669,stroke-width:2px

3.1 场景设定

假设知识库中有以下文档片段:

ID 内容
D1 "忘记密码时,可以通过绑定的邮箱或手机号自助重置"
D2 "管理员可以在后台重置任何用户的密码"
D3 "Webhook 回调地址配置在系统设置-集成管理页面"
D4 "API 密钥在个人设置-安全页面中生成和管理"

3.2 Dense 检索(语义相似度)

用户提问:"我怎么修改自己的登录密码"

Dense 检索使用 Embedding 模型将问题和文档都转成向量,计算余弦相似度:

问题向量   vs  D1 向量 → 相似度 0.91  ← 最高!语义非常接近
问题向量   vs  D2 向量 → 相似度 0.72
问题向量   vs  D3 向量 → 相似度 0.15
问题向量   vs  D4 向量 → 相似度 0.22

D1 排第一,因为"忘记密码→自助重置"与"修改登录密码"语义高度相关。

Dense 检索的优势: - 理解语义,同义词和改写都能识别 - "重置密码"、"修改密码"、"改密码"、"忘记密码怎么办"等表达都能召回

Dense 检索的局限: - 对专业术语、编号、代码等精确匹配较弱 - 例如搜索"API v3.2 变更",Dense 检索可能召回所有和 API 相关的文档,难以精确定位到特定版本

3.3 Sparse 检索(关键词匹配)

Sparse 检索(本项目使用 BM25 算法)基于词频和逆文档频率,对关键词做精确匹配:

问题:"API 密钥在个人设置-安全页面中生成和管理"

D4 包含:API(1次), 密钥(1次), 个人设置(1次), 安全页面(1次), 生成(1次), 管理(1次)
→ BM25 分数最高

D3 包含:Webhook(1次), 回调(1次), API(0次), 密钥(0次)
→ BM25 分数较低

Sparse 检索的优势: - 精确匹配专业术语、编号(如 "API v3.2"、"HS 编码 8471.30") - 对生僻词和专有名词效果极好 - 计算效率高,不需要 GPU

Sparse 检索的局限: - 无法理解语义:搜"修改密码"不会召回"忘记密码怎么办" - 同义词需要手动维护

3.4 Hybrid Search(混合检索)

混合检索 = Dense + Sparse,取长补短:

问题:"HS 编码 8471.30 的进口关税是多少"

Dense 检索贡献:
  → 召回语义相关的文档:关税政策、进口流程、税率表

Sparse 检索贡献:
  → 精确匹配 "8471.30" 的文档:该编码对应的具体税率记录

合并结果 → 既有精确的编码匹配,又有相关的背景信息

在本项目中,Milvus 2.5.x 原生支持混合检索,并通过 BM25BuiltInFunction 提供 Sparse 召回能力:

# 一次查询同时走 Dense 向量和 Sparse 向量
results = milvus_store.similarity_search_with_score(
    query,
    k=top_k,
    expr=filter_expression,  # 同时过滤 source、kb_version、tenant 等
)
# Milvus 内部自动融合 Dense 和 Sparse 的分数

第四部分:Reranker(重排器)

向量检索适合从大规模知识库中快速召回候选,但 Top-K 的顺序不一定足够准确。Reranker 的职责是在候选集已经很小之后,再判断“当前问题与每条候选文本是否真正相关”。

4.1 Bi-Encoder 与 CrossEncoder

阶段 输入方式 优点 代价
Bi-Encoder 召回 query 和文档分别编码,再比较向量 文档向量可预计算,检索快 query 与文档缺少逐词交互
CrossEncoder 重排 (query, document) 成对送入模型 相关性判断更细致 每个候选都要推理,只适合 Top-K

典型链路是:

Milvus 快速召回 20 条
→ CrossEncoder 对 20 个 query-document 对打分
→ 重新排序并保留前 5 条

因此 Reranker 不能代替向量数据库:它负责“少量候选精排”,不负责“全库召回”。

4.2 本文档中的内容边界

  • 本章只建立召回与重排的概念边界。
  • 第 08 章说明 rerank_hits() 如何进入项目检索链路。
  • 附录 D说明 CrossEncoder、BGE Reranker、推理成本和失效场景。

第五部分:在本项目中的体现

回顾第 1 章中的核心架构图,现在你应该能理解每个组件的角色:

flowchart TD
    Q["❓ 用户问题<br/>'入职流程有哪些步骤'"] --> EMB["🔤 BGE-M3 Embedding"]
    EMB --> DV["📐 1024维 Dense 向量"]

    DV --> MH["🔍 Milvus 混合检索"]

    MH --> DENSE["📖 Dense 向量 → 语义相关的文档<br/>(制度、流程、注意事项)"]
    MH --> SPARSE["📌 Sparse BM25 → 精确包含<br/>'入职''流程''步骤'的文档"]

    DENSE --> MERGE["🔄 合并去重"]
    SPARSE --> MERGE

    MERGE --> CAND["📋 Top 30 候选"]
    CAND --> RR["⚖️ BGE Reranker 重排"]
    RR --> TOP5["🎯 Top 5 最相关文档"]
    TOP5 --> CTX["🧩 构建 Prompt Context"]
    CTX --> LLM["🤖 LLM 生成答案"]

    style Q fill:#F3E8FF,stroke:#9333EA,stroke-width:2px
    style EMB fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px
    style DV fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px
    style MH fill:#FEF3C7,stroke:#D97706,stroke-width:2px
    style DENSE fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style SPARSE fill:#FFF7ED,stroke:#EA580C,stroke-width:2px
    style MERGE fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
    style CAND fill:#F9FAFB,stroke:#6B7280,stroke-width:2px
    style RR fill:#FEF3C7,stroke:#D97706,stroke-width:2px
    style TOP5 fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style CTX fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px
    style LLM fill:#F3E8FF,stroke:#9333EA,stroke-width:2px

重点掌握

优先级 内容 原因
★★★ 必会 Embedding 的概念:文本→固定长度浮点数向量,语义相近的文本向量距离近 向量检索的基石
★★★ 必会 Dense 检索(语义相似度)vs Sparse 检索(关键词 BM25)的区别和互补 混合检索策略的核心,决定召回质量
★★★ 必会 Hybrid Search = Dense + Sparse 取长补短 本项目 Milvus 的核心检索方式
★★★ 必会 Reranker(CrossEncoder)解决 Bi-Encoder 精度不足的问题:先粗排后精排 提升检索精度的关键环节
★★ 理解 余弦相似度的取值范围和含义 理解向量比较的基础
★★ 理解 BGE-M3 的多语言、多粒度、多功能特性 了解本项目 Embedding 模型的选择理由
★★ 理解 向量数据库的四项职责:向量存储、ANN 检索、标量过滤、集合管理 建立存储层边界,Milvus 细节留到第 04 章
★ 了解 向量相似度的三种计算方式(余弦/欧氏/内积)的公式 项目中使用默认内积,其他度量用于理解差异

返回笔记开头 ↑