# L2 RAG 检索增强生成 · 学习笔记

> **定位**：L2 工程层主线笔记，配套《知识树.md》使用
> **维护者**：扣子 · **主用户**：大扣子
> **维护规则**：每学完一个子主题，主人确认后落档；知识树只放速查索引，详细推导与例子全部留在这
> **开始日期**：2026-06-16

---

## 目录

1. [L2 RAG 全景地图](#l2-rag-全景地图)
2. [检索核心机制](#检索核心机制)
   - 2.1 [BM25 关键词检索](#21-bm25-关键词检索)
   - 2.2 [向量检索](#22-向量检索)
   - 2.3 [混合检索 Hybrid Search](#23-混合检索-hybrid-search)
   - 2.4 [场景对比与选型](#24-场景对比与选型)
3. [Embedding 模型训练](#embedding-模型训练)
   - 3.1 [训练数据怎么来](#31-训练数据怎么来)
   - 3.2 [分布式语义假设](#32-分布式语义假设)
   - 3.3 [对比学习流程骨架](#33-对比学习流程骨架)
   - 3.4 [完整训练流程图](#34-完整训练流程图)
4. [InfoNCE Loss 完整推导](#infonce-loss-完整推导)
   - 4.1 [3 个定义](#41-3-个定义)
   - 4.2 [温度系数 τ 是什么](#42-温度系数-τ-是什么)
   - 4.3 [N=4 数值算例](#43-n4-数值算例)
   - 4.4 [反例：τ 太小会怎样](#44-反例τ-太小会怎样)
   - 4.5 [一句话总结](#45-一句话总结)
5. [公式图与脚本（§4 InfoNCE 产物）](#公式图与脚本§4-infonce-产物)
6. [Chunking 分块策略](#5-chunking-分块策略)
   - 5.1 [Chunk 大小怎么定](#51-chunk-大小怎么定)
   - 5.2 [Overlap 重叠](#52-overlap-重叠)
   - 5.3 [递归切分](#53-递归切分recursive-split)
   - 📄 [Lost in the Middle — 长上下文中间段盲区研究](../学习资料/Lost_in_the_Middle_2307.03172_长上下文中间段盲区研究.pdf)（3,887 引用）
7. [Rerank 重排序](#6-rerank-重排序)
   - 6.1 [为什么需要 Rerank](#61-为什么需要-rerank)
   - 6.2 [Bi-Encoder vs Cross-Encoder](#62-bi-encoder-vs-cross-encoder)
   - 6.3 [两阶段流水线](#63-两阶段流水线完整数据流)
   - 6.4 [MarginRankingLoss 训练](#64-cross-encoder-训练marginrankingloss)
   - 6.5 [LLM-as-Reranker](#65-llm-as-reranker-另一个流派)
   - 📄 [Cross-Encoder vs LLM Reranker — 重排序模型对比研究](../学习资料/Cross-Encoder_vs_LLM_Reranker_2403.10407_重排序模型对比研究.pdf)（28 引用）
8. [RAG 评估与可观测性](#7-rag-评估与可观测性)
   - 7.1 [检索质量五指标](#71-检索质量--五个核心指标)
   - 7.2 [RAGAS 生成质量](#72-生成质量--ragas-框架)
   - 7.3 [端到端与生产实践](#73-端到端与生产实践)
8. [认知卡点 ❓](#认知卡点)
9. [待办与下一步](#待办与下一步)

---

## L2 RAG 全景地图

RAG = **R**etrieval-**A**ugmented **G**eneration。在生成答案前，先从外部知识库"取"到相关内容，再喂给 LLM。

```
[用户问题]
    ↓
[检索器 Retriever]  ←── 知识库（文档库/向量库）
    ↓
[Top-K 相关文档片段]
    ↓
[Prompt 拼装：问题 + 文档]
    ↓
[LLM 生成答案]
```

**L2 RAG 五大子主题**（学完打勾）：

- [x] **检索核心机制**：BM25 vs 向量 vs 混合检索 ✅ 2026-06-16
- [x] **Embedding 训练**：数据构造 + 对比学习 ✅ 2026-06-16
- [x] **InfoNCE Loss 推导**：公式 + 数值算例 ✅ 2026-06-16
- [x] **分块策略 Chunking** ✅ 2026-06-18~24（§5.1 固定大小 + §5.2 Overlap + §5.3 递归切分）
- [x] **重排序 Rerank** ✅ 2026-06-24
- [x] **评估与可观测性** ✅ 2026-06-24

> **节奏**：每周 1-2 子主题，单次 ≤ 30 分钟。今天（6-16）一口气过了 1+2+3，**扎实程度优先于推进速度**——主人在 L1 已明确表达"想搞懂底层机制再前进"。

---

## 检索核心机制

> **本节核心问题**：用户问"温州有什么好吃的"，系统怎么从几万篇文档里找到相关的？

### 2.1 BM25 关键词检索

**全名**：Best Matching 25（信息检索竞赛 25 号算法）

**核心思想**：TF-IDF 的升级版，按词频和稀有度加权打分。

**打分公式（思想层）**：

$$
\text{BM25}(q, d) = \sum_{t \in q} \text{IDF}(t) \times \frac{f(t,d) \times (k_1+1)}{f(t,d) + k_1 \times \left(1 - b + b \times \frac{|d|}{\text{avgdl}}\right)}
$$

- $f(t, d)$ = 词 t 在文档 d 里出现几次
- $\text{IDF}(t)$ = 词 t 越稀有分越高（"温州"比"的"分高）
- $k_1$、$b$ = 调参超参（一般 $k_1=1.5$, $b=0.75$）
- $|d|/\text{avgdl}$ = 文档长度归一化（防长文档天然占便宜）

**特点**：

| 优点 | 缺点 |
|------|------|
| ✅ 精确匹配强（专有名词/罕见实体不丢） | ❌ 不懂语义（搜"水果"找不到"苹果"） |
| ✅ 可解释（哪个词命中看得见） | ❌ 错别字/同义词/英文词干变种扛不住 |
| ✅ 冷门实体强（罕见人名/产品名召回率高） | ❌ 多模态（图片/视频）天然不支持 |
| ✅ 速度快（倒排索引是经典老技术） | ❌ 跨语言/跨模态不行 |

**适合场景**：法律/医疗/学术检索（专有名词密集）、关键词已知的中文文档库。

### 2.2 向量检索

**核心思想**：把文本变成一个数字向量，靠"向量距离"判断相似度。

**关键步骤**：

```
1. 文档/问题 → Embedding 模型 → 一个 N 维向量（通常 384/768/1024/3072 维）
2. 计算两个向量的余弦相似度 cos(q, k) = (q·k) / (|q| × |k|)
3. 相似度 Top-K 的文档作为检索结果
```

**Embedding 模型**：把"语义相近"的文本映射到"几何空间相近"的点。详见第 3 节。

**特点**：

| 优点 | 缺点 |
|------|------|
| ✅ 懂语义（搜"水果"能召回"苹果"） | ❌ 专有名词/罕见词可能漂移 |
| ✅ 多模态（图/文/音频可同空间） | ❌ 黑盒（"为啥召回这条"难解释） |
| ✅ 错别字/同义词鲁棒 | ❌ 需要训练 Embedding（不是开箱即用） |
| ✅ 跨语言（CLIP 类模型） | ❌ 大文档库需要向量数据库（成本） |

**适合场景**：FAQ/客服/闲聊型助手、跨模态检索。

### 2.3 混合检索 Hybrid Search

**为什么混**：BM25 强在精确，向量强在语义——**两者互补**。

**融合算法：RRF（Reciprocal Rank Fusion）**：

$$
\text{RRF\_score}(d) = \sum_{r \in \{\text{BM25, vector}\}} \frac{1}{K + \text{rank}_r(d)}
$$

- $\text{rank}_r(d)$ = 文档 d 在第 r 个检索器里的排名（1, 2, 3...）
- $K$ = 平滑常数（一般 60），防止排名靠后分数过小
- 把两个排名列表的"倒数排名"相加，分数越高的文档最终排序越前

**特点**：
- ✅ 召回率 + 准确率双提升（生产级 RAG 几乎都开）
- ✅ 可调权重（BM25 0.3 + 向量 0.7 之类）
- ⚠️ 工程复杂度上升（要维护两套索引）

### 2.4 场景对比与选型

| 场景 | 首选 | 理由 |
|------|------|------|
| 法律合同检索 | BM25 | 案号/法条/当事人名必须精确 |
| 电商商品搜索 | 混合检索 | 商品名要精确 + 描述要语义 |
| 论文/学术库 | BM25 或 混合 | 公式/引用要精确，摘要要语义 |
| 客服 FAQ | 向量检索 | 用户问法千变万化，要语义 |
| 跨模态（以图搜文） | 向量检索 | BM25 天然不支持 |
| 小型个人知识库（< 1 万文档） | BM25 | 简单够用，省成本 |
| 大型生产级 RAG | 混合检索 | 综合最优 |
| 中文为主+同义词多 | 混合检索 | 单向量模型对中文一词多义有时漂 |

**实战默认配置**（生产级 RAG 起步）：

```
hybrid_search:
  bm25_weight: 0.3
  vector_weight: 0.7
  rerank: bge-reranker-large   # 二次精排，下一节讲
  top_k_retrieve: 20            # 粗排召回数
  top_k_rerank: 5               # 精排后送 LLM 的数
```

---

## Embedding 模型训练

> **本节核心问题**：Embedding 模型怎么学会"语义相近→向量相近"的？

### 3.1 训练数据怎么来

要让模型学会"语义相近"的概念，必须给 (文本 A, 文本 B, 是否相似) 三元组。**4 种主流构造方式**：

#### 方式 ① 自然共现对

**做法**：抓同一篇文章的 (标题, 摘要)、(问题, 答案)、(前句, 后句) 等天然配对。

**来源**：
- Wikipedia（段落配对）
- 论文（标题+摘要、引文+上下文）
- Reddit/StackOverflow（问题+高赞答案）

**优点**：真实、免费。**缺点**：正样本多，负样本需要挖。

#### 方式 ② 搜索日志

**做法**：用搜索引擎的 click-through 数据（用户搜了啥、点了啥）。

**例**：
- 正样本：用户搜"温州美食"点了"江蟹生介绍页"
- 负样本：用户搜"温州美食"但点开又迅速关闭的页面

**优点**：来自真实用户意图，质量极高。**缺点**：只有搜索引擎公司有，外部拿不到。

#### 方式 ③ LLM 合成数据

**做法**：用 GPT-4/Claude 生成"问题-答案-干扰项"三元组。

**例**：

```yaml
原始段落: "雁荡山以奇峰怪石闻名，夜晚灵峰夜景更佳。"
LLM 生成:
  - 相关问题: "雁荡山夜景值得看吗？"
  - 部分相关: "雁荡山门票多少钱？"  # 软负样本
  - 完全无关: "温州有哪些海鲜？"  # 硬负样本候选
```

**优点**：可批量生产，质量可控。**缺点**：成本（GPT-4 调用费），可能学偏 LLM 的偏见。

#### 方式 ④ 主流方案：Cohere 和 OpenAI 路径

- **OpenAI**（text-embedding-3 系列）：用合成数据 + 对比学习 + RLHF 微调
- **Cohere**（embed-v3）：多任务 + 多语料 + 高质量负采样
- **BGE**（智源开源）：大规模中英文 + 硬负样本挖掘

**关键工程技巧**：**硬负样本挖掘**（hard negative mining）

```
定义:
  软负样本: 主题相似但答案无关（"雁荡山门票" vs 雁荡山夜景）
  硬负样本: 主题不同但表层词重叠（"灵峰夜景时间" vs 灵峰夜景价格）

做法: 训练中用向量检索近似找硬负样本，让模型专注难例
```

**为啥有效**：模型学到的不是"区别明显的事"，而是"容易混淆的事"，对难例边界更敏感。

### 3.2 分布式语义假设

> 词义 = 上下文分布（"你从哪来，你就是谁"）

**J.R. Firth 1957 经典一句话**：

> "You shall know a word by the company it keeps."

**核心思想**：

```
"灯盏糕"出现在 [温州, 小吃, 油炸, 早餐, 麻糍] 的上下文里
→ Embedding 时它会自然被映射到 [温州小吃集群] 的位置
→ 即使训练时没见过"灯盏糕 vs 锅边糊"的配对
→ 模型也能推断"灯盏糕"和"麻糍"语义相近
```

**类比**：
- 一个陌生朋友从朋友圈看：常发温州小吃 + 常去江心寺 + 常用温州话
- 你就能推断他大概率是温州本地人，即使他从没在朋友圈说自己是温州人
- Embedding 模型学"语境集群"就是这种推断

**这也是为啥**：
- Embedding 模型能"举一反三"（zero-shot 泛化）
- 不需要为每对相似词单独标注
- 数据越多，分布越准，Embedding 越准

### 3.3 对比学习流程骨架

**目标**：让 (q, k⁺) 距离近，(q, k⁻) 距离远。

**伪代码**：

```python
for batch in dataloader:
    # 1. 编码
    q = encoder(question)            # query 向量
    k_pos = encoder(positive_doc)   # 正样本向量
    k_neg = encoder_batch(neg_docs) # 负样本向量们

    # 2. 计算相似度（除以温度 τ）
    scores = matmul(q, [k_pos, k_neg].T) / tau

    # 3. InfoNCE Loss：让正样本分数最高
    loss = cross_entropy(scores, label=0)  # 0 位置 = 正样本

    # 4. 反向传播
    loss.backward()
    optimizer.step()
```

**直觉理解**：
- 训练初期：所有分数差不多，Loss 大
- 训练中期：模型开始把正样本推到 k⁺ 集群
- 训练后期：正样本分数 >> 负样本分数，Loss 趋近 0

### 3.4 完整训练流程图

```
┌─────────────────────────────────────────────────────────────┐
│                  Embedding 模型训练完整流程                    │
└─────────────────────────────────────────────────────────────┘

   ① 数据准备
   ┌────────────────────────────────────────────┐
   │  原始语料（爬取/搜索日志/LLM合成）           │
   │       ↓                                     │
   │  构造三元组 (q, k⁺, k⁻₁, k⁻₂, ..., k⁻ₙ)  │
   │       ↓                                     │
   │  硬负样本挖掘（用旧模型检索难例）            │
   └────────────────────────────────────────────┘
                       ↓
   ② 模型初始化
   ┌────────────────────────────────────────────┐
   │  选 backbone：BERT-base / BGE / E5          │
   │  输出维度：768 / 1024 / 3072                │
   │  温度 τ 初始化：0.05 ~ 0.1                   │
   └────────────────────────────────────────────┘
                       ↓
   ③ 对比学习循环
   ┌────────────────────────────────────────────┐
   │  for epoch in 1..N:                          │
   │    for batch in dataloader:                  │
   │      编码 q, k⁺, k⁻                         │
   │      计算相似度 scores = (q·k)/(τ·|q|·|k|)   │
   │      InfoNCE Loss = -log(softmax(正样本))   │
   │      反向传播 + 更新参数                     │
   │      每 K 步硬负样本再挖掘                    │
   └────────────────────────────────────────────┘
                       ↓
   ④ 评估
   ┌────────────────────────────────────────────┐
   │  检索评测集：BEIR / MTEB                     │
   │  指标：NDCG@10 / MRR@10 / Recall@K          │
   │  对比基线：BM25 / 上一版模型                 │
   └────────────────────────────────────────────┘
                       ↓
   ⑤ 部署
   ┌────────────────────────────────────────────┐
   │  模型导出 ONNX / TorchScript                 │
   │  向量库索引（FAISS / Milvus / Qdrant）        │
   │  对外提供 embed(text) → vector API            │
   └────────────────────────────────────────────┘
```

---

## InfoNCE Loss 完整推导

> **本节核心问题**：InfoNCE Loss 公式每一部分都是啥意思？为什么这么设计？

### 4.1 3 个定义

#### 定义 1 · 相似度函数 $s(q, k)$

把两个向量的余弦相似度除以温度 $\tau$：

$$
s(q, k) = \frac{q \cdot k}{\tau \cdot |q| \cdot |k|}
$$

- $q$ = query 向量（用户问题的 Embedding）
- $k$ = key 向量（候选文档的 Embedding）
- $q \cdot k$ = 点积（向量化之后就是乘加）
- $|q|$, $|k|$ = L2 范数（向量的"长度"）
- $\tau$ = 温度系数（详见下一节）

> 注：L2 归一化后，$q \cdot k / (|q| \cdot |k|) = \cos(q, k) \in [-1, 1]$

#### 定义 2 · 正样本概率 $P(k^+|q)$

正样本分数 = $\exp(s(q, k^+))$，分母是正样本 + 所有负样本的 exp 分数：

$$
P(k^+ \mid q) = \frac{\exp(s(q, k^+))}{\exp(s(q, k^+)) + \sum_i \exp(s(q, k_i^-))}
$$

- $k^+$ = 与 q 配对的正样本（应该被召回的那个）
- $k_i^-$ = 第 i 个负样本（不该被召回的 N-1 个）
- 这个公式就是 Softmax —— 正样本分数越高，概率越大

#### 定义 3 · InfoNCE Loss

Loss = 负对数似然，我们想让 $P(k^+|q)$ 越大越好，所以 Loss 越小越好：

$$
\mathcal{L}_{\text{InfoNCE}} = -\log P(k^+ \mid q)
$$

> 为什么是负 log？把"乘积最大化"变成"求和最小化"，梯度更稳。这也是 Softmax + CrossEntropy 的经典套路。

### 4.2 温度系数 τ 是什么

**τ 的角色**：控制概率分布的"尖锐度"。

| τ 值 | 效果 | 比喻 |
|------|------|------|
| τ 大（如 1.0） | 概率分布平坦，所有负样本都被平均惩罚 | 老师打分宽松，"有进步就给分" |
| τ 小（如 0.05） | 概率分布尖锐，只有分数最高的负样本被严惩 | 老师打分严格，"一点点差别都看得出来" |

**经验范围**：
- `τ = 0.05` 是 CLIP 原论文默认
- 训练后期可逐步降低 τ（从 0.1 → 0.05）让模型更挑剔
- τ 太大会学不动，τ 太小会梯度爆炸

### 4.3 N=4 数值算例

> 4 个候选：1 个正样本 k⁺，3 个负样本 k⁻₁, k⁻₂, k⁻₃。τ = 0.1。

**Step 1：算相似度**（假设 L2 已归一化）：

| 配对 | cos(q, k) | s = cos/τ | 解释 |
|------|-----------|-----------|------|
| q × k⁺ | 0.80 | 8.0 | 正样本，应该最高 |
| q × k⁻₁ | 0.30 | 3.0 | 软负样本（主题沾边） |
| q × k⁻₂ | 0.20 | 2.0 | 硬负样本（有点混淆） |
| q × k⁻₃ | 0.10 | 1.0 | 简单负样本（明显不相关） |

**Step 2：exp 一下**：

| 项 | exp 值 |
|----|--------|
| exp(s(q, k⁺)) | exp(8.0) ≈ 2981 |
| exp(s(q, k⁻₁)) | exp(3.0) ≈ 20.1 |
| exp(s(q, k⁻₂)) | exp(2.0) ≈ 7.4 |
| exp(s(q, k⁻₃)) | exp(1.0) ≈ 2.7 |

**Step 3：算 $P(k^+|q)$**：

$$
P = \frac{2981}{2981 + 20.1 + 7.4 + 2.7} \approx 0.992
$$

模型学得不错，正样本概率 99.2%。

**Step 4：算 Loss**：

$$
\mathcal{L}_{\text{InfoNCE}} = -\log(0.992) \approx 0.008
$$

Loss 很小，模型已经学到位了。

### 4.4 反例：τ 太小会怎样

> 还是上面 4 个 cos 值，把 τ 从 0.1 换成 0.5。

**新相似度 s = cos/0.5**：

| 配对 | s' = cos/0.5 |
|------|--------------|
| q × k⁺ | 1.6 |
| q × k⁻₁ | 0.6 |
| q × k⁻₂ | 0.4 |
| q × k⁻₃ | 0.2 |

**exp 之后**：

| 项 | exp 值 |
|----|--------|
| exp(s'(q, k⁺)) | exp(1.6) ≈ 4.95 |
| exp(s'(q, k⁻₁)) | exp(0.6) ≈ 1.82 |
| exp(s'(q, k⁻₂)) | exp(0.4) ≈ 1.49 |
| exp(s'(q, k⁻₃)) | exp(0.2) ≈ 1.22 |

**新概率**：

$$
P' = \frac{4.95}{4.95 + 1.82 + 1.49 + 1.22} \approx 0.521
$$

**新 Loss**：

$$
\mathcal{L}' = -\log(0.521) \approx 0.652
$$

**对比**：

| τ | P(k⁺) | Loss | 模型对负样本的"严厉度" |
|---|-------|------|------------------------|
| 0.1 | 99.2% | 0.008 | 极其严苛（k⁻₁ 分数 3.0 直接被压成 ~0.7%） |
| 0.5 | 52.1% | 0.652 | 宽松（k⁻₁ 分数 0.6 还能分到 ~18% 的概率） |

**直觉**：
- τ 小 → 严师 → Loss 大 → 梯度大 → 模型专注难例
- τ 大 → 慈师 → Loss 小 → 梯度小 → 模型放轻松

### 4.5 一句话总结

> **InfoNCE Loss = 把"让正样本分数高"这件事，用 Softmax 转成"正样本概率最大"，再取负对数变成 Loss，让模型通过反向传播学会把所有负样本压到概率最低。**

### 4.6 信息论含义：InfoNCE = 互信息下界估计器

**核心结论**（Oord et al. 2018, CPC 论文）：

\[
I(x;y) \geq \log N - \mathcal{L}_{\text{InfoNCE}}
\]

**人话**：你最小化 InfoNCE Loss 时，实际上在**最大化互信息的一个下界**。

**为什么叫"Info"NCE**：
- **NCE**（Noise Contrastive Estimation）：统计学习方法，目标是"从噪声中识别真实数据"
- **InfoNCE** 继承了 NCE 的对比思想，但目标从"估计密度"变成了**最大化表征与上下文之间的互信息**
- `Info` = Information = 互信息

**NCE → InfoNCE 进化对比**：

| 维度 | NCE | InfoNCE |
|------|-----|---------|
| 本质 | 密度估计 → 二分类（正 vs 噪声） | 表征学习 → 多分类（1 正 vs N-1 负） |
| 信息论视角 | 无直接 MI 联系 | 直接是 MI 下界 |
| 负样本 | 1 个噪声分布 | N-1 个具体负样本 |
| 输出 | 学习数据分布 p(x) | 学习表征 f(x)，让正样本互信息最大化 |

**N 为什么越大越好**：

互信息下界的紧度取决于 N：

- N=2 → 下界很松，估计值远低于真实 MI
- N=65536（MoCo）→ 下界很紧，几乎等于真实 MI

这也是为什么 InfoNCE 类方法都拼大 batch——**信息论本身要求负样本量够大，下界才紧**。

---

## 5. Chunking 分块策略

> **核心思想**：chunk 是 RAG 检索的最小单位。切多大、要不要重叠、按什么边界切——这三条决定了 RAG 召回率的天花板。

---

### 5.1 Chunk 大小怎么定

**核心问题**：chunk 是 RAG 检索的最小单位——多大都行，但大了小了都完蛋。

#### 双轴 Trade-off

| 维度 | 小 chunk（128） | 大 chunk（1024+） |
|---|---|---|
| 召回精度 | ✅ 高（精确命中一句话） | ❌ 低（一大段里 1 句相关） |
| 上下文完整 | ❌ 缺前因后果 | ✅ 一次给齐 |
| Embedding 主题聚焦 | ✅ 主题清晰 | ❌ 主题稀释 |
| 召回数膨胀 | ❌ 同样内容切成 N 倍 | ✅ 切片少 |
| LLM 上下文压力 | ✅ 宽松 | ❌ 紧张（Lost in the Middle） |

**关键反直觉**：很多人以为"chunk 越大召回越好"——**错**。

大 chunk 的 embedding 是全段平均，多个主题平均后 = **主题稀释**，跟 query 的相似度反而降低。

#### 数字经验：512 是怎么来的

| Chunk 大小 | 适用场景 | 典型用法 |
|---|---|---|
| 64-128 | FAQ / Q-A pair | 单条问答，精确匹配 |
| 256 | 句子级 | QA、客服话术 |
| **512** | **段落级**（**默认**） | **通用技术文档、知识库** |
| 1024 | 长文档 / 论文 | 多段融合、综述 |
| 2048+ | 几乎不用 | 触发 LLM 上下文窗口限制 |

**512 = 一个标准文档章节**（200-500 tokens）+ 一些边界 token。

#### 关键数学关系

**单次检索总 token 消耗**：

> $T_{total} = K \times C_{chunk} + T_{prompt}$

**硬约束**：

> $K \times C_{chunk} \leq W_{ctx} - T_{prompt}$

- $W_{ctx}$ = LLM 上下文窗口（GPT-4 = 8K，Claude = 200K）
- $T_{prompt}$ 通常 500-1000

举例：8K 上下文、prompt 1K → 剩余 7K 给检索：
- 选 512 chunk → K ≤ 13（实际取 5-10）
- 选 256 chunk → K ≤ 27（实际取 10-20）
- 选 1024 chunk → K ≤ 6（实际取 3-5）

#### 实测决策树

```
你的文档是什么类型？
   │
   ├─ FAQ / 短问答 ────────→ 128 tokens
   │
   ├─ 技术文档 / 手册 ─────→ 256-512 tokens
   │
   ├─ 论文 / 长文 ─────────→ 512-1024 tokens
   │
   └─ 多轮对话上下文 ──────→ 1024 tokens
```

**配套 K 值**：
- chunk 小 → K 大（10-20）
- chunk 大 → K 小（3-5）

#### 跟"工作日志颗粒度"的对照

| 系统 | 颗粒度单位 | 数字量级 |
|---|---|---|
| **工作日志** | 1 段连续工作 / 1 个窗口 | 1 次压缩 = 1 chunk |
| **RAG** | 1 段语义完整文本 | 128-1024 tokens |
| Git | 1 次 commit | 1 个变更集 |
| 人脑长期记忆 | 1 段经历 | 1 次事件 |

**共同原则**：都是"语义完整的最小叙事单元"——不按字符切，不按时间切，按**事件/语义**切。

#### 关键认知卡点

> "为什么 512 是经验最优？跟 LLM 注意力机制有啥关系？"

→ 5.4 会展开：LLM 对长上下文的中段（Lost in the Middle）注意力衰减。**核心矛盾**：人想"一次给够上下文"，但 LLM 想"一次只给最相关的几段"。

→ 解决思路：**小 chunk + 精排**两阶段方案（第 6 章 Rerank 引子）。

---

### 5.2 Overlap 重叠

**核心问题**：关键句子被切到两个 chunk 的边界 → 单个 chunk 都不完整。

#### 问题引入

切分点刚好在"关键信息 + 上下文"之间：

```
Chunk 1: "....市盈率是 30 倍，盈利能力强"
Chunk 2: "...能力强，主要受益于新业务增长"
Chunk 3: "...新业务增长点在于 AI 芯片"
```

用户问"AI 芯片增长点是什么"：
- 召回 Chunk 3 → 提到"AI 芯片"但没"增长点"前因
- 召回 Chunk 2 → 提到"新业务增长"但没"AI 芯片"

→ **关键信息横跨边界**。

#### 解决思路：Overlap = 共享边界区

```
Chunk 1: [────────A────]──B
Chunk 2:                B──[────C────]
                          ↑
                       overlap 区
```

Chunk 1 包含 B 的完整信息，Chunk 2 也包含 → 切分点不丢信息。

#### Overlap 多大合适？黄金比例 10-20%

| Chunk 大小 | 推荐 Overlap | 比例 |
|---|---|---|
| 128 | 13-25 | 10-20% |
| **512** | **50-100** | **10-20%** |
| 1024 | 100-200 | 10-20% |

**为啥是 10-20%？**
- 太小（< 5%）：边界信息仍可能丢失
- 太大（> 30%）：召回重复，浪费 token
- 10-20%：覆盖典型句子边界（一句话 20-30 tokens）又不浪费

#### 关键数学关系

**chunk 总数公式**：

> $N_{chunk} = \left\lceil \dfrac{L_{doc} - O}{C - O} \right\rceil$

- $L_{doc}$ = 文档总长度（tokens）
- $C$ = chunk 大小
- $O$ = overlap 大小

举例：10000 tokens 文档，chunk=512，overlap=50：
- $N_{chunk} = \lceil (10000-50)/(512-50) \rceil = \lceil 21.5 \rceil = 22$ 个
- 不带 overlap：$\lceil 10000/512 \rceil = 20$ 个
- **多出 2 个 = 10% 存储开销**

**Token 放大率**（重复部分占比）：

> $R_{amp} = \dfrac{C}{C - O}$

| Overlap | 放大率 | 额外 token |
|---|---|---|
| 0 | 1.000 | 0% |
| 50 / 512 | 1.108 | +10.8% |
| 100 / 512 | 1.243 | +24.3% |
| 200 / 512 | 1.641 | +64.1% |

→ overlap 越大，成本**非线性**上涨。**10-20% 是性价比甜区**。

#### Overlap 不是银弹（三个反直觉）

1. **重叠太多反而干扰检索**——同一段信息在两个 chunk 里，embedding 接近，会双倍召回相同内容
2. **对"语义连贯长文本"无意义**——切分点恰好在自然段中才需要 overlap；如果按段切，本来就不需要
3. **结构化文档（Markdown）通常不用**——h1/h2 边界天然是分块点

#### 实测建议

| 文档类型 | 推荐 overlap | 理由 |
|---|---|---|
| 连续散文 / 论文 | 10-20% | 句子边界可能跨 chunk |
| 客服话术 / FAQ | 5-10% | 单条本身完整 |
| 结构化文档（Markdown） | 0-5% | 标题是天然边界 |
| 代码 | 0% | 函数/类边界天然分隔 |

#### 跟工作日志的对照

工作日志其实**就是用了 overlap 思路**：

| 角色 | 对应关系 |
|---|---|
| 窗口日志（W1, W2, W3） | chunk |
| `index.json` 的 summary | overlap 区 |
| 跨窗口信息保留 | 靠 summary 重叠 |

每次归档时，summary 既写到当前窗口日志，也作为下一行的索引 → 形成"软 overlap"。

→ **RAG 的 overlap 跟工作日志的 index.json 是同一个设计哲学**：用摘要当"软胶水"，把分散的记忆粘起来。

#### 关键认知卡点

> "为什么 FAQ / 结构化文档几乎不用 overlap？"
>
> → 因为它们的"信息单元"本身完整，不存在跨边界问题。overlap 主要解决"连续散文"场景。

---

### 5.3 递归切分（Recursive Split）

**核心问题**：固定大小切分不管文档结构——一句话被腰斩、代码块被拆散、表格被截断。递归切分按优先级逐级 fallback，先按大边界切，切不了再降级。

#### 工作原理：多级 fallback

RecursiveCharacterTextSplitter 内置一组分隔符优先级，从大到小尝试：

```
["\n\n", "\n", "。", ".", "！", "？", " ", ""]
  段落    句子   中文句号 英文句号                         字符级
```

**执行流程**：

1. 先尝试用 `\n\n`（段落分隔）切——尽量保持段落完整
2. 如果切出来的 chunk 还是太大，降级到 `\n`（行分隔）
3. 再不行降级到 `。` `.`（句子分隔）
4. 最后兜底：按字符硬切（`""`）

**直观例子**：

```
原文（1200 tokens，目标 512）:
[段落1: 300t]\n\n[段落2: 400t]\n\n[段落3: 500t]

第1轮（\n\n）:
  → Chunk A: 段落1 = 300t ✅ 合格
  → Chunk B: 段落2+3 = 900t ❌ 超标 → 降级

第2轮（\n）:
  → B 内部再切：段落2=400t ✅ / 段落3=500t ✅

最终: [300t] [400t] [500t] 三个完整段落
```

#### 为什么递归切分比固定大小好

| 场景 | 固定大小（512） | 递归切分 |
|------|----------------|---------|
| 代码块 | 可能从函数中间截断 | 优先在 `\n\n`（函数间空行）切 |
| 表格 | 可能从第3行截断 | 在表格前后的 `\n\n` 切 |
| 对话记录 | 可能把一句话劈成两半 | 降级到 `\n` 或 `。` 保持句子完整 |
| 技术文档 | 可能在列表项中间截断 | 优先在标题/段落边界切 |

#### LangChain 默认分隔符序列

```python
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", ".", "！", "？", " ", ""]
    #           段落      行    中文句号  英文  感叹  问号  空格  字符兜底
)
```

**关键认知**：
- 分隔符列表**有顺序**——`["\n\n", "\n", " ", ""]` ≠ `[" ", "\n\n"]`
- 前面的优先级高，后面的只是兜底
- 最后一项必须是 `""`（空字符串 = 字符级硬切），否则递归可能永远切不完

#### 三个常见坑

1. **分隔符列表对了，但 chunk_size 太小**：设 chunk_size=100，段落本身 300 tokens → 最终还是会降级到字符硬切，失去递归切分意义
2. **中文分隔符缺失**：只写 `["\n\n", "\n", ".", " "]` → 中文没有 `.` 作为句号 → 中文段落只能降级到空格或字符硬切
3. **overlap + 递归的交互**：递归切分后 overlap 机制仍然生效——但 overlap 在"段落边界"处可能跨段落重复，语义不如段落内 overlap 自然

#### 与其他切分方式的对比

| 切分方式 | 原理 | 优点 | 缺点 |
|---------|------|------|------|
| **固定大小** | 每 N tokens 一刀 | 简单可控 | 破坏语义边界 |
| **递归切分** | 分隔符逐级 fallback | 自动尊重文档结构 | 分隔符列表需针对语种调整 |
| **语义切分** | 按 Embedding 距离断句 | 最智能 | 需要额外模型，慢且贵 |
| **结构切分** | 按 h1/h2/code block | 最精确 | 只适用结构化文档 |

**生产实践**：递归切分是当前 RAG 的**默认起点**——90% 的场景够用，不需要上语义切分。剩下的 10%（论文公式区、多语言混合文档）才需要定制。

#### 关键认知卡点

> "递归切分的 `separators` 列表为什么必须有顺序？"
>
> → 顺序 = 优先级。`\n\n` 在前意味着"优先保段落"，只有段落太大才降级到句子。如果反过来 `"。"` 在前，会把每个句子都切成独立 chunk，丢失段落级上下文。

---

## 公式图与脚本（§4 InfoNCE 产物）

**论文风格公式图**（PNG + PDF）：

- PNG：`/app/data/所有对话/主对话/AI学习/InfoNCE_Loss_论文公式.png`（377KB）
- PDF：`/app/data/所有对话/主对话/AI学习/InfoNCE_Loss_论文公式.pdf`（62KB）
- 生成脚本：`/app/data/所有对话/主对话/AI学习/渲染InfoNCE公式.py`

**图里包含 4 块**：
1. 定义 1 相似度函数
2. 定义 2 正样本概率
3. 定义 3 InfoNCE Loss
4. 完整公式组合（含 τ 作用 + 几何解释）

**脚本可复用**：以后学新公式（注意力机制 QKV / KL 散度 / Softmax 推导 / Transformer 位置编码）都可以用同一个脚本改 prompt 直接出图。`matplotlib.mathtext` 支持的字符有限，规避清单：

- ❌ `\limits`（会报 ParseSyntaxException）
- ❌ `⟨⟩` `‖‖` 字符（会方块化）
- ✅ 用 ASCII：`q·k` 代替 `⟨q,k⟩`，`|q|` 代替 `‖q‖`

---

## 认知卡点 ❓

> 学习中遇到的不太懂的地方，先记下来

- [ ] **batch size 对 InfoNCE 的影响**：为啥大 batch 通常效果更好？是因为负样本多还是别的？
- [x] **InfoNCE 名字信息论含义**：已回答 → 见 §4.6，InfoNCE 是互信息下界估计器，N越大下界越紧
- [ ] **τ 的取值有什么理论指导**：除了经验值，有没有办法算出最优 τ？
- [ ] **蒸馏 Embedding 模型**：能不能用大模型蒸馏小模型？loss 怎么设计？
- [ ] **多模态 Embedding**（CLIP 类）：图像和文本怎么共享一个向量空间？

---

## 待办与下一步

- [x] L2 RAG §5.3 **递归切分**（RecursiveCharacterTextSplitter 多级 fallback：段落→句子→字符）
- [ ] L2 RAG §5.4 **语义切分**（按 embedding 距离断句、按主题切，前沿方向）
- [ ] L2 RAG §5.5 **小节级切分**（按文档结构 h1/h2/code block 切）
- [x] L2 RAG 第 6 子主题：**重排序 Rerank** ✅ 2026-06-24
- [x] L2 RAG 第 7 子主题：**评估与可观测性** ✅ 2026-06-24
- [ ] L2 RAG 第 2 篇正式阅读：Anthropic《Introducing Contextual Retrieval》
- [ ] 实战：用旅游搭子 SKILL.md 跑一次"分块 + 检索 + 召回率"小实验
- [ ] 把 `渲染InfoNCE公式.py` 抽成通用模板（在脚本头部加 docstring 说明）

---

## 6. Rerank 重排序

> **核心问题**：混合检索召回 20-100 条候选，Bi-Encoder 打分不够准，怎么从中精挑出最相关的 3-5 条送 LLM？

---

### 6.1 为什么需要 Rerank

**Bi-Encoder 的硬伤**：Query 和 Document 在编码时**互相没看见**。

```
Bi-Encoder（检索阶段）:
  Query → Encoder → q 向量    ─┐
  Doc1  → Encoder → d1 向量   ─┤ 各自独立编码
  Doc2  → Encoder → d2 向量   ─┤ 然后算 cos(q, d)
  Doc3  → Encoder → d3 向量   ─┘
```

这意味着：
- Query 编码时不知道 Doc 长什么样
- Doc 编码时不知道 Query 在问什么
- 「Query 和这个 Doc 到底多相关」——Bi-Encoder **没机会判断**

**Cross-Encoder 的解决方式**：把 Query 和 Document **拼在一起**送进模型。

```
Cross-Encoder（Rerank 阶段）:
  [Query, Document] → Encoder（完整 Self-Attention）→ 相关性分数
```

Query 的每个 token 都能和 Document 的每个 token 交互——这才是真正的"阅读理解式打分"。

---

### 6.2 Bi-Encoder vs Cross-Encoder

| 维度 | Bi-Encoder | Cross-Encoder |
|------|-----------|---------------|
| 编码方式 | Query 和 Doc 独立编码 | Query+Doc 联合编码 |
| 交互方式 | 只在最后的 cos 计算相遇 | 全程 Self-Attention 交互 |
| 速度 | 极快（向量库毫秒级检索） | 慢 50-100 倍 |
| 精度 | 中等（60-70%） | 高（85-92%） |
| 适用阶段 | **粗排**（海选 Top-100） | **精排**（精选 Top-5） |
| 代表模型 | BGE / E5 / text-embedding-3 | bge-reranker / Cohere Rerank |

**关键数字对比**：

| 方案 | 耗时 | 精度 |
|------|------|------|
| 纯 Bi-Encoder | ~10ms | 一般 |
| 纯 Cross-Encoder（全库） | ~3小时 | 最高（但不现实） |
| **Bi + Cross 两阶段** | **~210ms** | **接近 Cross** |

→ 两阶段方案用 20× 时间换接近 Cross-Encoder 的精度，是生产级 RAG 的标准配置。

---

### 6.3 两阶段流水线（完整数据流）

```
[用户问题]
    ↓
① 混合检索（BM25 + 向量 + RRF）
    ↓ Top-100
② Cross-Encoder Rerank（逐条精打分）
    ↓ Top-5
③ 送入 LLM 生成答案
```

**为什么不是 Top-100 直接送 LLM？**
- 100 条 × 512 tokens = 51,200 tokens → 超出 8K 窗口 + 触发 Lost in the Middle
- LLM 对排序不敏感——关键信息即使在中后段也能生成，但质量依赖"重要信息在前几条"

**Rerank 的本质**：**把最重要的排前面，对抗 Lost in the Middle**。

---

### 6.4 Cross-Encoder 训练：MarginRankingLoss

Cross-Encoder 不是天生就有的，需要专门训练。主流 Loss 是 **MarginRankingLoss**：

$$
\mathcal{L} = \frac{1}{N}\sum_{i=1}^{N} \max\left(0, \text{margin} - s(q, c^+) + s(q, c^-)\right)
$$

- $s(q, c^+)$ = query 与正样本（相关文档）的分数
- $s(q, c^-)$ = query 与负样本（不相关文档）的分数
- $\text{margin}$ = 要求正样本至少比负样本高多少（通常 1.0）

**目标不是「正样本分数越高越好」，而是「正样本排在负样本前面」**。

**直觉**：
```
正样本分=0.85，负样本分=0.80 → margin=1 → Loss = max(0, 1-0.85+0.80) = 0.95 ❌
正样本分=0.85，负样本分=0.20 → margin=1 → Loss = max(0, 1-0.85+0.20) = 0.35 ✅
```

正负样本拉开足够差距时 Loss=0，不再更新——"够好了，别优化了"。

---

### 6.5 LLM-as-Reranker：另一个流派

除了训练专用 Cross-Encoder，还有一种思路：**直接用 LLM 做 Rerank**。

| 维度 | Cross-Encoder | LLM-as-Reranker |
|------|--------------|-----------------|
| 精度 | 85-92% | 90-97% |
| 速度 | 快（专用小模型） | 慢（大模型逐条推理） |
| 成本 | 低（本地 GPU） | 高（API 按 token 计费） |
| 上手门槛 | 需微调（需要标注数据） | 零样本开箱即用 |
| 适用场景 | 高吞吐线上系统 | 离线评测 / 冷启动 / 小流量 |

**当前共识**：生产环境用 Cross-Encoder 做 Rerank（快+便宜），LLM-as-Reranker 更适合离线评估和标注数据生成。

---

### 6.6 管线全景串联

回头看 RAG 五大子主题，它们是一条链：

```
Chunking（决定召回上限）
    ↓
混合检索（BM25 + 向量 + RRF）
    ↓
Rerank（Cross-Encoder 对抗 Lost in the Middle）
    ↓
评估（量化上面的每一步效果）
```

- **Chunk 太小** → Rerank 救不回来（关键信息可能根本不在任何 chunk 里）
- **Chunk 太大** → Rerank 打分的粒度太粗（一大段里 1 句相关 → 整段高分）
- **不 Rerank** → 混合检索的 Top-K 是粗糙排序，LLM 收到 5 条中最关键的可能是第 5 条
- **没有评估** → 上面三个问题你永远不知道

**生产级 RAG 三件套**：混合检索 + RRF + Rerank。

---

## 7. RAG 评估与可观测性

> **核心问题**：RAG 搭好了，怎么证明它"好"？检索准不准、答案对不对、幻觉多不多——这些都需要量化。

---

### 7.1 检索质量 — 五个核心指标

#### Recall@K（找全了吗？）

$$
\text{Recall@K} = \frac{\text{Top-K 中相关文档数}}{\text{全部相关文档总数}}
$$

- 侧重"别漏"——用户问的东西，检索结果里有没有
- 例：库里有 5 篇相关文档，Top-5 召回 3 篇 → Recall@5 = 3/5 = 0.6

#### Precision@K（找对了吗？）

$$
\text{Precision@K} = \frac{\text{Top-K 中相关文档数}}{K}
$$

- 侧重"别乱塞"——召回的 K 条里有多少真正相关
- 例：Top-5 里 3 条相关、2 条无关 → Precision@5 = 3/5 = 0.6

#### Hit Rate（至少找到一条吗？）

$$
\text{Hit Rate} = \frac{\text{至少召回 1 条相关的查询数}}{\text{总查询数}}
$$

- 最宽容的指标——"有一条算赢"
- 适合 FAQ 场景：用户只想要一个答案，有一条相关就够了

#### MRR（第一条正确答案排第几？）

$$
\text{MRR} = \frac{1}{|Q|}\sum_{i=1}^{|Q|} \frac{1}{\text{rank}_i}
$$

- $\text{rank}_i$ = 第 i 个查询的第一个正确答案的排名
- 排第 1 → 贡献 1.0 / 排第 3 → 贡献 0.33 / 排第 10 → 贡献 0.1
- **RR 的倒数设计让排名越靠后惩罚越重**

#### NDCG（整体排序质量好吗？）

三步走：

**① CG（Cumulative Gain）**：
$$\text{CG@K} = \sum_{i=1}^{K} \text{rel}_i$$
rel 是相关性评分（如 0/1/2/3），简单的累加。

**② DCG（Discounted CG）**——位置折损：
$$\text{DCG@K} = \sum_{i=1}^{K} \frac{\text{rel}_i}{\log_2(i+1)}$$

位置越靠后，分母越大，贡献越小。

**③ NDCG**：
$$\text{NDCG@K} = \frac{\text{DCG@K}}{\text{IDCG@K}}$$

IDCG 是"理想排序下的 DCG"（最相关的全排前面）。NDCG ∈ [0, 1]，1 代表完美排序。

#### 五指标互补关系

| 指标 | 测什么 | 短板 |
|------|--------|------|
| Recall@K | 别漏 | 不关心排序 |
| Precision@K | 别乱塞 | 不关心召回全不全 |
| Hit Rate | 够用就行 | 太宽容 |
| MRR | 第一个正确的排哪 | 只看第一条，忽略后面的 |
| NDCG | 整体排序质量 | 需要相关性分级（不只 0/1） |

**关键洞察**：Recall 和 MRR 互补——
- Recall 满分 + MRR 低 = 答案全在但排在末尾
- MRR 满分 + Recall 低 = 第一条很准但漏了很多

---

### 7.2 生成质量 — RAGAS 框架

[RAGAS](https://arxiv.org/abs/2309.15217) 是当前最主流的 RAG 评估框架，三个核心指标：

#### Faithfulness（忠实度）— RAG 的命脉

> 答案的每个声明，都能在检索上下文里找到依据吗？

$$
\text{Faithfulness} = \frac{\text{有依据的声明数}}{\text{总声明数}}
$$

- 检测幻觉的核心指标。Faithfulness = 0.8 → 20% 的内容是编的
- **幻觉率 = 1 - Faithfulness**

#### Answer Relevance（答案相关性）

> 答案扣题了吗？有没有答非所问？

- 做法：用 LLM 根据答案反生成问题，算反生成的问题与原问题的相似度
- 反生成的问题和原问题越像 → 答案越扣题

#### Answer Correctness（答案正确性）

> 有标准答案时直接对比；没有标准答案时 LLM 综合评判。

- 有 ground truth → 精确匹配 / F1 / BLEU
- 无 ground truth → LLM-as-Judge（给答案打分 1-5）

**三者关系**：Faithfulness（基石）→ Relevance（方向）→ Correctness（终点）

---

### 7.3 端到端与生产实践

#### 端到端指标

| 指标 | 目标 | 含义 |
|------|------|------|
| 幻觉率 | ≤ 5% | 答案中有多少是无依据的 |
| 响应时间 P95 | ≤ 3s | 95% 的请求在 3 秒内返回 |
| 任务完成率 | ≥ 80% | 用户问题真正被解决的比例 |
| 重问率 | 越低越好 | 用户不满意又追问同一问题的比例 |

#### 生产落地三步

1. **造 Golden Dataset**（50-100 条标注）：人工标注 query + ideal_docs + ground_truth_answer
2. **CI 集成**：每次 PR 自动跑评估 → Recall@5 下降 > 5% 就报警
3. **线上采样**：1-5% 流量用 LLM-as-Judge 实时评估 Faithfulness + Relevance

#### 与已学知识的全部串联

| 调什么 | 看哪个指标 |
|--------|-----------|
| Chunk 大小 | Recall@K + Precision@K |
| Overlap | 重复召回率 |
| 混合检索 + RRF | MRR + NDCG |
| Rerank | Precision@5 提升 |
| Lost in the Middle | MRR + NDCG（天然验证排序） |
| Faithfulness | 幻觉检测（与 Chunk 质量直接相关） |

> **一句话**：**评估不是 RAG 的最后一步——它是 RAG 的每一步的影子。每调一个参数，都应该有指标告诉你变好了还是变差了。**

---

**最近一次更新**：2026-06-24 23:11 · §5.3 递归切分 + §6 Rerank + §7 RAG 评估全部落档；RAG 五大子主题标记全部完成
