Interview Prep · RAG + Embedding / Retrieval

RAG + 文本嵌入 面试 Cheat Sheet

检索增强生成 + 文本嵌入/重排 · InfoNCE / 难负例 / Matryoshka / BM25 / RRF / ColBERT / GraphRAG + 公式推导 + From-Scratch PyTorch + 25 高频题(L1 必会 · L2 进阶 · L3 顶级 lab)

Source: docs/tutorials/rag_embedding_retrieval_tutorial.md SHA256: 9534fd905207 Rendered: 2026-06-16 06:15 UTC

§0 TL;DR Cheat Sheet

9 句话搞定 RAG + 嵌入

一页拿下面试核心要点(详见后文 §2–§11 推导)。

  1. RAG 是什么:检索增强生成 = 先用 query 从外部知识库检索相关片段,再把片段拼进 prompt 让 LLM 生成答案。应对三件事:知识过期、幻觉、私有数据(Lewis et al. 2020)。
  2. 两半互补:左半「训嵌入」是对比学习(双塔 + InfoNCE + 难负例),数学密;右半「用嵌入」是 RAG 管线(chunk → 检索 → 重排 → 拼 prompt),系统密。
  3. 嵌入主公式 InfoNCE:$\mathcal{L} = -\log \frac{\exp(\text{sim}(q,d^+)/\tau)}{\sum_i \exp(\text{sim}(q,d_i)/\tau)}$ —— 本质是「正样本 vs 一堆负样本」的 softmax 交叉熵,温度 $\tau$ 调锐度。
  4. 双塔 vs 交叉编码器:双塔(bi-encoder)query/doc 分开编码,可预计算、快、用于召回;交叉编码器(cross-encoder)拼一起编码,不可预计算、慢但准,用于重排 top-k
  5. 难负例(hard negative):从检索 top-k 里挑「像但不相关」的当负例,比随机负例信息量大;但有 false negative 陷阱——挑到的「难负例」可能其实相关,反成噪声。
  6. 稀疏 vs 稠密 vs 混合:BM25(词项精确匹配,强在罕见词/实体)+ 稠密向量(语义匹配)互补,用 RRF(Reciprocal Rank Fusion)按排名融合:$\text{RRF}(d)=\sum_r \frac{1}{k+\text{rank}_r(d)}$,$k\approx60$。
  7. Matryoshka 表征:一次训练让嵌入的前 $m$ 维也是好嵌入,部署时按需截断维度(省存储/加速召回),靠多粒度损失 $\sum_m \mathcal{L}(\text{emb}[:m])$。
  8. RAG vs 长上下文 vs 微调:RAG 注入可更新的外部事实、可溯源;长上下文塞全文但贵且有「lost in the middle」;微调改能力/风格不擅长注入海量新事实。三者常组合。
  9. 评测:别只看生成,要分层评 检索质量(recall@k / nDCG)+ 生成忠实度(faithfulness / context relevance / answer relevance,如 RAGAS)。

§1 直觉:为什么需要 RAG

LLM 有三个结构性短板:知识截止(训练后的事不知道)、幻觉(不知道时也会一本正经编)、私有数据(你公司的文档它没见过)。直接微调来灌知识又贵又慢、还容易过时。

RAG(Retrieval-Augmented Generation)把「知识」从参数里搬到外部知识库:把权重当「推理引擎」,把向量库当「可随时更新的外部记忆」。回答时先检索相关片段、再让 LLM 基于片段作答——知识更新只需更新库(不重训)、答案能溯源(引用来源)、私有数据不进训练集。

这条链路天然分成互补的两半

一句话心智模型

LLM 是「闭卷考试只能靠脑子」,RAG 是「开卷考试,先用 query 去书里翻到相关页(检索),再照着页面作答(生成)」。翻得准不准 = 嵌入 + 检索质量;答得好不好 = 重排 + prompt + LLM。

§2 文本嵌入:双塔 + 对比学习

2.1 双塔(bi-encoder)结构

把 query 和 document 各自过一个编码器(通常共享或孪生 BERT-like 模型),取句向量([CLS] 或 mean pooling),L2 归一化:

$$\mathbf{q} = \text{normalize}(\text{Enc}(\text{query})), \qquad \mathbf{d} = \text{normalize}(\text{Enc}(\text{doc}))$$

相似度用点积(归一化后等于余弦):$\text{sim}(q,d) = \mathbf{q}^\top \mathbf{d}$。

关键优势:doc 向量可离线预计算并建索引,线上只编码 query 一次、做近邻搜索。这是双塔能扛海量库的根本原因(对比 §6 的交叉编码器)。

2.2 InfoNCE / 对比损失(必考,要会推)

目标:让 query 和它的正例 doc $d^+$ 相近、和负例 $d^-$ 远离。把它写成「在一堆候选里选出正例」的分类问题——对每个 query,候选集 $\{d^+, d_1^-, \dots, d_{N}^-\}$ 上做 softmax 交叉熵:

$$\boxed{\;\mathcal{L}_{\text{InfoNCE}} = -\log \frac{\exp(\text{sim}(q, d^+)/\tau)}{\exp(\text{sim}(q, d^+)/\tau) + \sum_{i} \exp(\text{sim}(q, d_i^-)/\tau)}\;}$$

推导视角:这就是把「相似度 $/\tau$」当 logits、正例索引当 label 的 cross-entropy。最小化它 = 最大化 $q$ 与 $d^+$ 的相似度、压低与所有负例的相似度(InfoNCE,van den Oord 2018;用于检索即 DPR,Karpukhin 2020)。

温度 $\tau$ 的作用:$\tau$ 小 → logits 被放大 → softmax 更尖 → 模型更「在意」最难的负例(梯度更偏向 hardest negative;但正例已压倒性最大时小 $\tau$ 也会使梯度饱和变小);$\tau$ 大 → 更平滑、各负例权重更均匀。$\tau$ 太小会对噪声/false negative 过敏,太大则区分度不足,是关键超参(常见 $0.01\sim0.07$)。

2.3 In-batch 负例与难负例

In-batch negatives:一个 batch 有 $B$ 对 $(q_i, d_i^+)$,对 $q_i$ 而言,同 batch 里其它 $B-1$ 个正例 $d_{j\ne i}^+$ 就当负例。一次前向就拿到 $B-1$ 个负例,几乎零额外开销(DPR 的关键 trick)。batch 越大、负例越多、对比越强(大 batch 对对比学习很重要)。

难负例(hard negative):随机负例往往太容易(一眼就不相关),梯度小。从 BM25/上一版模型的检索 top-k 里挑「字面/语义像、但其实不相关」的当负例,信息量大得多,是刷榜关键。

False negative 陷阱

从 top-k 挖的「难负例」可能其实是相关的(只是没标注),把它当负例会给错误梯度。缓解:去掉与正例太像的候选(阈值过滤)、用 cross-encoder 给负例打分剔除高分项、或 teacher 蒸馏软标签。难负例是双刃剑,不是越难越好。

§3 难负例的对偶 + Matryoshka 表征

3.1 余弦 / 点积 / 归一化

L2 归一化后 $\lVert \mathbf{q} \rVert = \lVert \mathbf{d} \rVert = 1$,于是 $\mathbf{q}^\top\mathbf{d} = \cos\theta \in [-1,1]$。归一化的好处:相似度只看方向不看模长,训练更稳(绝对阈值仍需按模型/语料校准)。多数检索嵌入都归一化后用点积(等价余弦)。

3.2 Matryoshka 表征学习(MRL)

普通嵌入是定长(如 768 维),想省存储/加速只能事后降维(PCA)损失大。Matryoshka(Kusupati 2022)让一次训练就得到「嵌套」嵌入:前 $m$ 维($m \in \{64,128,256,\dots,768\}$)各自都是合格嵌入。做法是对多个粒度同时算损失:

$$\mathcal{L}_{\text{MRL}} = \sum_{m \in \mathcal{M}} w_m \, \mathcal{L}_{\text{InfoNCE}}\big(\mathbf{q}[:m],\ \mathbf{d}[:m]\big)$$

部署时按预算截断:粗排用前 64/128 维(快、省)、精排用全维。OpenAI text-embedding-3、Nomic、BGE 等都支持 MRL 维度可调。

MRL 为何能成立

多粒度损失迫使模型把「最重要的语义」压进前几维(类似按重要性排序的主成分),所以截断前缀仍可用。注意:用于余弦/点积检索时,每个前缀 $\mathbf{q}[:m]$ 通常要重新 L2 归一化(否则不同前缀范数不同会改变 logit 尺度)。代价:训练目标更复杂、满维质量可能略降一点点,换来部署弹性。

§4 从零实现:双塔 + InfoNCE 训练

下面是一个可跑的双塔 + in-batch InfoNCE 训练步(用 toy 编码器,重点是对比损失与难负例逻辑)。

import torch
import torch.nn as nn
import torch.nn.functional as F


class DualEncoder(nn.Module):
    """Toy 双塔:query/doc 共享一个编码器;真实场景换成 BERT-like。"""

    def __init__(self, vocab, dim=128):
        super().__init__()
        self.emb = nn.EmbeddingBag(vocab, dim, mode="mean")  # 词袋均值当句向量
        self.proj = nn.Linear(dim, dim)

    def encode(self, ids, offsets):
        h = self.proj(self.emb(ids, offsets))
        return F.normalize(h, dim=-1)                        # L2 归一化 -> 点积=余弦


def info_nce_in_batch(q, d_pos, tau=0.05):
    """q, d_pos: [B, dim] 已归一化。同 batch 其它正例当负例。"""
    logits = (q @ d_pos.t()) / tau          # [B, B],第 i 行第 i 列是正例
    labels = torch.arange(q.size(0), device=q.device)
    return F.cross_entropy(logits, labels)  # 对角线为正 -> 多分类交叉熵


def info_nce_with_hard(q, d_pos, d_hard, tau=0.05):
    """额外拼接每个 query 自己的难负例 d_hard: [B, dim]。"""
    pos = (q * d_pos).sum(-1, keepdim=True)         # [B,1] 正例相似度
    in_batch = q @ d_pos.t()                        # [B,B] in-batch 负例
    in_batch.fill_diagonal_(float("-inf"))          # 去掉自身正例避免重复计入
    hard = (q * d_hard).sum(-1, keepdim=True)       # [B,1] 难负例相似度
    logits = torch.cat([pos, in_batch, hard], dim=1) / tau   # 正例放第 0 列
    labels = torch.zeros(q.size(0), dtype=torch.long, device=q.device)
    return F.cross_entropy(logits, labels)

要点:

§5 检索:稀疏 / 稠密 / 混合

5.1 稀疏检索:BM25

BM25 是词项精确匹配的经典基线,对罕见词、实体名、代码/ID 这类「必须字面命中」的 query 仍很强:

$$\text{BM25}(q,d) = \sum_{t \in q} \text{IDF}(t) \cdot \frac{f(t,d)\,(k_1+1)}{f(t,d) + k_1\big(1 - b + b\,\frac{\lvert d\rvert}{\text{avgdl}}\big)}$$

其中 $f(t,d)$ 是词频,$\lvert d\rvert$ 是文档长度,$\text{avgdl}$ 是平均长度,$k_1$(≈1.2–2.0)控词频饱和、$b$(≈0.75)控长度归一。词频饱和:出现 10 次不等于比 1 次重要 10 倍。

5.2 稠密检索 + ANN(HNSW)

稠密 = 把 query 编码成向量、在向量库里找最近邻。库一大就不能暴力算全部,用近似最近邻(ANN)。主流是 HNSW(Malkov & Yashunin 2016):建多层「可导航小世界」图,高层稀疏跳远、低层密集精搜,查询经验上近似对数 / 亚线性(非严格保证,依赖 efSearch、图度数、召回目标)。

稠密不是万能

稠密向量擅长语义/同义/改写,但对「精确实体、罕见专名、序列号」可能不如 BM25(这些词在语义空间里没区分度)。所以工业界很少纯稠密,多走混合。

5.3 混合检索 + RRF

把 BM25 和稠密各自的排名用 Reciprocal Rank Fusion 融合(只用排名、不用分数,免去量纲对齐):

$$\boxed{\;\text{RRF}(d) = \sum_{r \in \mathcal{R}} \frac{1}{k + \text{rank}_r(d)}\;}, \qquad k \approx 60$$

$\text{rank}_r(d)$ 是文档 $d$ 在检索器 $r$ 里的名次(1 起)。常数 $k$ 压低头部名次的过度主导。RRF 简单、稳、无需训练,是混合检索的默认融合法(Cormack 2009)。

learned sparse:SPLADE

在 BM25(词项)与稠密(语义)之外还有一条中间路线:SPLADE(Formal 2021)用 MLM 头把 query/doc 展开成带学习权重的稀疏词项向量(含同义/扩展词),既能进倒排索引(快、可解释),又有语义扩展能力,常作为强 baseline 或混合的一路。

§6 重排(Rerank):交叉编码器

召回(双塔 + ANN)追求高 recall、快,但 top-k 里精排不够。重排器把召回的 top-k(如 100)重新精确打分、取前几个

双塔 bi-encoder交叉编码器 cross-encoderColBERT(late interaction)
编码方式query、doc 分开编码[query; doc] 拼一起编码doc 存逐 token 向量
交互仅最后点积(无 token 级交互)全程 attention(early,强)token 级 late interaction(MaxSim)
可预计算 doc✅(建索引)❌(每对都要重算)✅(存 token 向量,较大)
速度 / 规模快 / 百万–十亿慢 / 只能重排 top-k中 / 较大存储
用途召回重排召回+重排折中

交叉编码器:$\text{score}(q,d) = \text{MLP}(\text{BERT}([q;d])_{\texttt{[CLS]}})$,query 和 doc 的 token 全程互相 attend,所以准;代价是无法预计算(doc 表示依赖 query),只能对召回的少量候选跑。

ColBERT 的 late interaction:doc 存每个 token 的向量,打分用 MaxSim——query 每个 token 找 doc 里最相似的 token 再求和:

$$\text{score}(q,d) = \sum_{i \in q} \max_{j \in d} \mathbf{E}_{q_i}^\top \mathbf{E}_{d_j}$$

比双塔的单向量更细粒度、又能预计算 doc 端,是召回与重排之间的折中(Khattab & Zaharia 2020)。代价是存储/延迟:每个 doc 存逐 token 向量,索引远大于单向量;ColBERTv2(量化 + 残差压缩)与 PLAID(中心点剪枝加速)是把多向量 late interaction 真正落地到可接受存储/延迟的关键工程(Santhanam 2021/2022)。单向量 vs 多向量是「省存储 vs 高精度」的经典权衡。

§7 RAG 管线:从 query 到答案

query
  │  ① query 改写 / 扩展 (HyDE / multi-query)
  ▼
[稀疏 BM25]  +  [稠密 ANN]
  │                │
  └──── RRF 融合 ───┘   ② 召回 top-100
  ▼
cross-encoder 重排  ③ 取 top-5
  ▼
拼 prompt (context + question)  ④ 含来源标注
  ▼
LLM 生成 + 引用   ⑤ 可选自检: Self-RAG(生成反思) · CRAG(检索质量→纠错回路)

§8 RAG 进阶:GraphRAG · 对比 · 评测

8.1 GraphRAG

朴素 RAG 按片段独立召回,回答「跨多文档的全局性问题」(如「整个报告的主题脉络」)很弱。GraphRAG(微软,Edge 2024)先用 LLM 把语料抽成实体-关系知识图 + 社区摘要,全局问题走「社区摘要」而非散片段。强在 query-focused summarization,代价是建图贵。

8.2 RAG vs 长上下文 vs 微调

RAG长上下文微调
注入什么可更新的外部事实当次塞进的全文能力 / 风格 / 格式
知识更新改库即可、可溯源每次重塞、无持久重训、易过时
成本检索 + 短 prompt长 prompt 贵、慢训练贵、推理便宜
短板检索错则答错lost-in-the-middle不擅长灌海量新事实

实务里三者常组合:微调调风格 + RAG 注事实 + 适度长上下文容纳召回片段。

8.3 评测(别只看生成)

分两层评:

经典翻车

只看「答案读起来对不对」,不分层。检索召回了错片段、LLM 据此「忠实地」给出错答案,端到端看像幻觉,根因却在检索。必须把检索和生成分开归因。

§9 从零实现:mini-RAG 管线

import re, math
import torch, torch.nn.functional as F

def chunk(text, size=40, overlap=10):
    words = text.split()
    out, i = [], 0
    while i < len(words):
        out.append(" ".join(words[i:i+size]))
        i += size - overlap                      # 滑窗重叠,避免上下文断裂
    return out

def embed(texts, encoder):                       # encoder: str -> [dim] 归一化向量
    return F.normalize(torch.stack([encoder(t) for t in texts]), dim=-1)

def dense_topk(qv, dv, k):                        # 余弦 top-k
    sims = dv @ qv                                # [N]
    return sims.topk(min(k, len(dv))).indices.tolist()

def bm25_scores(query, docs, k1=1.5, b=0.75):
    toks = [d.lower().split() for d in docs]
    avgdl = sum(len(t) for t in toks) / len(toks)
    N = len(docs)
    df = {}
    for t in toks:
        for w in set(t):
            df[w] = df.get(w, 0) + 1
    scores = []
    for d in toks:
        s, dl = 0.0, len(d)
        for w in set(query.lower().split()):
            if w not in df:
                continue
            idf = math.log(1 + (N - df[w] + 0.5) / (df[w] + 0.5))
            f = d.count(w)
            s += idf * f * (k1 + 1) / (f + k1 * (1 - b + b * dl / avgdl))
        scores.append(s)
    return scores

def rrf(rank_lists, k=60):                        # rank_lists: [[doc_idx 按名次]...]
    score = {}
    for ranks in rank_lists:
        for rank, idx in enumerate(ranks, start=1):
            score[idx] = score.get(idx, 0.0) + 1.0 / (k + rank)
    return sorted(score, key=score.get, reverse=True)

def mini_rag(query, corpus, encoder, top_recall=10, top_final=3):
    chunks = [c for doc in corpus for c in chunk(doc)]
    dv = embed(chunks, encoder)
    qv = F.normalize(encoder(query), dim=-1)
    dense_rank = dense_topk(qv, dv, top_recall)                       # 稠密名次
    bm25 = bm25_scores(query, chunks)                                 # 先算一次,避免 O(N^2)
    bm25_rank = sorted(range(len(chunks)), key=lambda i: bm25[i], reverse=True)[:top_recall]
    fused = rrf([dense_rank, bm25_rank])[:top_final]                  # 混合 + 截断
    context = "\n".join(f"[{r}] {chunks[i]}" for r, i in enumerate(fused, 1))
    return f"基于以下片段回答(标注出处编号):\n{context}\n\n问题:{query}"

这段把 §5–§7 串起来:切块 → 稠密 + BM25 双路召回 → RRF 融合 → 拼成带出处的 prompt(真实场景在融合后再加 cross-encoder 重排)。

§10 工程实践与常见 bug

「加了 RAG 还是幻觉」

多半不是 LLM 的锅:检索召回了无关/错误片段,LLM「忠实地」据此作答。先查检索(recall@k、片段对不对),再怀疑生成。

§11 复杂度与资源

阶段计算备注
嵌入 doc(离线)$O(N \cdot C_{\text{enc}})$一次性,可批量;MRL 可省存储
建 ANN 索引(HNSW)$O(N \log N)$ 建图内存换查询速度
稠密召回(线上)经验近似对数 / 亚线性 per queryANN 近似、非精确、非严格保证
BM25 召回倒排索引 $O(\lvert q\rvert \cdot \text{postings})$词项级
RRF 融合$O(R \cdot k_{\text{recall}})$只排名,极廉价
cross-encoder 重排$O(k_{\text{recall}} \cdot C_{\text{enc}})$每个候选一次前向,只对 top-k
LLM 生成$O(L_{\text{ctx}}^2)$ prefillcontext 越长越贵(见 attention 篇)

双塔的核心红利:doc 编码 $O(N)$ 但离线一次,线上每 query 只编码 1 次 + ANN 查询;交叉编码器无法预计算,所以只能用在 top-k 重排,绝不能拿来扫全库。

§12 25 高频面试题

按难度分三档,点开看答案要点 + 易踩坑。

L1必会题(任何用过 RAG 的岗位)

Q1. 什么是 RAG?解决什么问题?
  • 检索增强生成:先检索相关片段、再让 LLM 基于片段生成
  • 缓解:知识截止 / 幻觉 / 私有数据(缓解事实性幻觉 + 提升可溯源,非保证消除幻觉)
  • 知识更新只需更新库,可溯源

只说「让 LLM 查资料」,说不清「检索→拼 prompt→生成」三步和它应对的具体问题。

Q2. RAG 的基本流程有哪些步骤?
  • 离线:文档切块 → 嵌入 → 建索引
  • 线上:query 嵌入 → 召回 top-k → (重排)→ 拼 prompt → LLM 生成
  • 可选:query 改写、来源标注、自检

漏掉切块/重排,或把召回和重排混为一谈。

Q3. 双塔(bi-encoder)和交叉编码器(cross-encoder)区别?
  • 双塔:query/doc 分开编码,可预计算 doc 向量,快,用于召回
  • 交叉编码器:拼一起编码,全程 attention,准但不可预计算,用于重排
  • 召回用双塔、重排用交叉编码器是标准搭配

说交叉编码器也能建索引扫全库(错,它不可预计算)。

Q4. 为什么需要重排(rerank)?
  • 召回追求高 recall + 快,精度不够
  • 交叉编码器对 top-k 精确打分,提升精度
  • 只对召回的少量候选跑,代价可控

以为召回就够了,不知道重排常是高 ROI 的一步。

Q5. 稀疏(BM25)和稠密检索各自强在哪?
  • BM25:词项精确匹配,强在罕见词/实体/序列号
  • 稠密:语义/同义/改写匹配
  • 互补,工业界多用混合

说稠密全面碾压 BM25(错,精确匹配场景 BM25 常更好)。

Q6. 什么是 chunking?块大小怎么影响效果?
  • 把文档切成片段再嵌入
  • 太大:检索粗、稀释相关信息
  • 太小:上下文断裂
  • 常 256/512 token + 重叠

把 chunk 当无关紧要的预处理,不知道它是头号调参旋钮。

Q7. 相似度为什么常用余弦 / 归一化点积?
  • L2 归一化后点积 = 余弦
  • 只看方向不看模长,更稳(阈值仍需校准)
  • 多数嵌入归一化后用点积

不知道归一化后点积就是余弦,或答不出归一化的好处。

Q8. RAG 和直接把文档塞进长上下文有何不同?
  • RAG:先检索相关片段,prompt 短、可溯源、库可更新
  • 长上下文:塞全文,贵、慢、有 lost-in-the-middle
  • 大规模 / 动态知识下 RAG 通常更经济可控(全文也塞不下)

以为有长上下文就不需要 RAG(库一大就塞不下、也贵)。

Q9. 向量库为什么用近似最近邻(ANN)?
  • 库大时暴力算全部 $O(N)$ 太慢
  • HNSW 等 ANN 把查询降到经验上近似对数 / 亚线性(非严格保证)
  • 用一点召回率换巨大速度

说向量检索是精确最近邻(大库下基本都是近似)。

Q10. 「加了 RAG 还是幻觉」可能的原因?
  • 多半是检索召回了无关/错误片段,LLM 据此「忠实」作答
  • 也可能 chunk 太碎、prompt 没强约束「只依据 context」
  • 先查检索质量,再怀疑生成

直接归咎 LLM,不去查检索那一层。

L2进阶题(research / 工程深入)

Q11. 写出 InfoNCE 损失并解释温度 $\tau$。
  • $\mathcal{L} = -\log \frac{\exp(\text{sim}(q,d^+)/\tau)}{\sum_i \exp(\text{sim}(q,d_i)/\tau)}$
  • 本质是相似度当 logits、正例当 label 的交叉熵
  • $\tau$ 小 → softmax 更尖 → 更看重最难负例;太小对噪声敏感

只会写公式,讲不出「它就是 softmax 交叉熵」和 $\tau$ 的锐度作用。

Q12. In-batch negatives 是什么?为什么高效?
  • batch 内每个 query 用其它样本的正例当负例
  • 一次前向拿到 $B-1$ 个负例,几乎零额外成本
  • 相似度矩阵对角线是正例,label = arange(B)

说要单独前向算负例(错,in-batch 复用同 batch)。

Q13. 难负例为什么重要?false negative 陷阱是什么?
  • 随机负例太易、梯度小;难负例(像但不相关)信息量大
  • false negative:挖到的「难负例」可能其实相关 → 错误梯度
  • 缓解:阈值过滤、cross-encoder 剔除、软标签蒸馏

只说「难负例好」,不知道它可能引入 false negative 噪声。

Q14. RRF 怎么融合多路检索?为什么用排名不用分数?
  • $\text{RRF}(d)=\sum_r \frac{1}{k+\text{rank}_r(d)}$,$k\approx60$
  • 只用名次,免去不同检索器分数量纲对齐
  • 简单、稳、无需训练

试图直接加 BM25 分数和余弦分数(量纲不可比)。

Q15. Matryoshka 表征解决什么?怎么训?
  • 一次训练得到嵌套嵌入,前 $m$ 维也是好嵌入
  • 多粒度损失 $\sum_m \mathcal{L}(\text{emb}[:m])$
  • 部署按预算截断维度(截断后需重新 L2 归一化;省存储/加速粗排)

以为是事后 PCA 降维(MRL 是训练时就把重要语义压进前缀)。

Q16. BM25 里 $k_1$ 和 $b$ 分别控制什么?
  • $k_1$:词频饱和(出现多次的边际收益递减)
  • $b$:文档长度归一化强度($b=1$ 全归一、$b=0$ 不归一)
  • 典型 $k_1\approx1.2$–$2$,$b\approx0.75$

说不清词频饱和与长度归一这两个机制。

Q17. ColBERT 的 late interaction 与双塔、交叉编码器的关系?
  • 双塔:单向量、仅最后点积(无 token 级交互)、可预计算
  • 交叉编码器:early、全 attention、最强、不可预计算
  • ColBERT:token 级 late interaction,存 token 向量、MaxSim $\sum_i\max_j E_{q_i}^\top E_{d_j}$,介于两者、可预计算 doc

把 ColBERT 当普通双塔,不知道它是 token 级 MaxSim。

Q18. HyDE 是什么思路?为什么有用?
  • 先让 LLM 生成一个「假设文档 / 段落」,用它的嵌入去检索
  • 让查询向量分布对齐真实文档(伪文档比原始问题更像语料里的文档)
  • 零样本稠密检索常因此提升

以为 HyDE 是改写 query 关键词(其实是生成假设文档再嵌入)。

Q19. 怎么评测一个 RAG 系统?
  • 检索层:recall@k、nDCG、MRR
  • 生成层:faithfulness、context relevance、answer relevance(如 RAGAS)
  • 分层归因:先看检索对不对,再看生成

只看端到端答案对错,不分检索/生成两层。

Q20. lost in the middle 是什么?怎么缓解?
  • 长 context 里放中间的关键信息易被模型忽略(Liu 2023)
  • 缓解:重排后把最相关片段放头尾、压缩 context、减少无关片段
  • 也是「召回越多越好」不成立的原因之一

以为 context 越长越全越好,忽略中间信息被弱化。

L3高级题(顶级 lab / 深水区)

Q21. 为什么交叉编码器不能拿来扫全库?复杂度差在哪?
  • 交叉编码器 doc 表示依赖 query,无法离线预计算
  • 每个 (query, doc) 对都要一次完整前向,全库 = $O(N \cdot C_{\text{enc}})$/query
  • 双塔 doc 离线编码、线上只 ANN 查询(经验近似对数 / 亚线性);所以交叉编码器只配重排 top-k

说交叉编码器也能建索引(它的表示和 query 耦合,建不了静态索引)。

Q22. 对比学习里 batch size 为什么影响很大?和负例的关系?
  • in-batch 负例数 = batch-1,batch 越大负例越多、对比越强
  • 大 batch 让 InfoNCE 的分母覆盖更多负例,估计更准
  • 小 batch 负例少、易过拟合;可用 cross-batch memory / 难负例补偿

不知道 batch 大小直接决定 in-batch 负例数量。

Q23. GraphRAG 相比朴素 RAG 强在哪?代价?
  • 朴素 RAG 按片段独立召回,弱在「跨文档全局问题」
  • GraphRAG 用 LLM 抽实体-关系图 + 社区摘要,全局问题走摘要
  • 代价:建图/摘要的离线 LLM 成本高、维护复杂

把 GraphRAG 当「换个向量库」,不知道它建的是知识图 + 社区摘要。

Q24. Self-RAG / CRAG 这类「自检式 RAG」在做什么?
  • Self-RAG:训模型输出反思 token(要不要检索 / 片段有没有用 / 答案有没有依据)
  • CRAG:在检索层用 evaluator 给已检索文档的相关性/置信度打分(推理时无标注全集、测不了真 recall),低置信时触发纠正检索(web 搜索、重写)
  • 目标:让 RAG 在「检索没用/错」时不盲目据此作答

以为是更好的检索器;其实是加自我评估/纠正回路——Self-RAG 侧重生成时反思,CRAG 侧重评估检索质量并触发纠正检索。

Q25. 同一套知识,什么时候选 RAG、什么时候选微调、什么时候靠长上下文?
  • 注入大量可更新外部事实 + 要溯源 → RAG
  • 能力/风格/格式/输出结构 → 微调
  • 单次少量文档、要全局推理且放得下 → 长上下文
  • 实务常组合:微调调风格 + RAG 注事实 + 长上下文容纳召回

绝对化地说「RAG 全面优于微调」或反之,不看「注入事实 vs 改能力」的本质区分。

§A 附录:sanity check

info_nce_in_batch 与 mini-RAG 的关键不变量(可写脚本验证):

下面是 code/rag_embedding.pyPyTorch 2.10 / CPU 上的真实运行输出(每行带 assert,全过才打印汇总):

[a] in-batch InfoNCE: argmax==idx, loss aligned 0.0000 < shuffled 22.9564  OK
[b] DualEncoder ||v|| = 1.0000 (~1), max|dot| = 1.0000 (<=1)  OK
[c] entropy(tau=0.05) = 0.0000 < entropy(tau=0.5) = 1.2945  OK
[d] with-hard loss = 0.0000 finite, logits (6, 8) == (B, B+2)  OK
[e] RRF(doc7): base = 0.03226 -> better = 0.03252 (non-decreasing); rrf top = 7  OK
[f] BM25 top = 1 (entity chunk 1); fused top2 = [1, 2]  OK

all RAG / embedding sanity checks passed ✓

其中 [a] 对齐时 InfoNCE loss≈0、把正例打乱后骤升到 ~23,证明损失确实在「对齐」上响应(不只是构造了对角阵);[c] 同一组 logits 下 $\tau=0.05$ 的 softmax 熵(≈0)远低于 $\tau=0.5$(≈1.29),验证小温度更锐;[f] 含罕见实体 Zephyrnaut9000 的片段被 BM25 排到第 1、混合(RRF)结果 top-2 保留,演示混合召回对精确实体的稳健(§5.3;注:此 toy 例里稠密恰好也召回了它,真实场景罕见实体/序列号稠密更易漏)。


📜 Runnable Code

本 tutorial 的嵌入 / 检索核心在 docs/tutorials/code/rag_embedding.py 有最小可跑版本:

纯 PyTorch,CPU 几秒跑完、无需 GPU:python docs/tutorials/code/rag_embedding.py。上方 §A 的输出即该脚本的真实运行结果。


📚 参考文献