chapter 10 / rag · 预计学习时间 120 分钟

RAG 与向量检索
给模型一个开卷考场

AUDIO // 本章语音导读
本章目录
  1. 参数知识的三宗罪
  2. Embedding:把语义变成几何
  3. 交互实验室:语义检索 vs 关键词检索
  4. ANN 索引:HNSW 如何在十亿向量里毫秒寻邻
  5. 分块:RAG 质量的隐形上限
  6. 生产级检索:混合召回与重排序
  7. RAG 评估与失败模式清单
  8. 长上下文 vs RAG 与 Agentic RAG
  9. 代码实战:60 行最小 RAG
  10. 章节测验

参数知识的三宗罪

第 6 章说过:模型的事实知识主要存在 FFN 权重里。这种参数化知识有三个结构性缺陷:

RAG(检索增强生成)的解法是把闭卷考试改成开卷:回答前先从外部知识库检索相关材料塞进上下文,让模型「引用」而非「回忆」。管线一句话:文档 → 分块 → 嵌入 → 入库;查询 → 嵌入 → 近邻检索 →(重排)→ 拼 prompt → 生成。每一环都有技术细节,逐个拆。

Embedding:把语义变成几何

核心抽象在第 5 章已经铺好:语义相似 = 向量空间中距离近。embedding 模型把任意文本映射为稠密向量(768-3072 维),用对比学习训练:拉近相关对(问题↔答案、标题↔正文),推开无关对(同 batch 其他样本当负例),损失是 InfoNCE(第 2 章交叉熵的孪生兄弟——「在 batch 里找出正例」的分类):

$$L = -\log \frac{e^{\text{sim}(q, d^+)/\tau}}{\sum_{d \in \text{batch}} e^{\text{sim}(q, d)/\tau}}$$

相似度用余弦(normalize 后等价于点积)。两个工程要点:① 双塔结构——查询和文档各自独立编码,文档向量可离线预计算,查询时只算一次 query 嵌入 + 近邻查找(这是 RAG 能快的原因);② 它的代价是查询和文档没有交互,精度天花板低于把两者拼起来过一遍模型的 cross-encoder——这个权衡在 §6 的重排序里回收。

交互实验室:语义检索 vs 关键词检索

18 条玩具文档摆在 2D「语义地图」上(嵌入空间的卡通版),4 个预设查询。必做实验:① 查「钱能退吗」——关键词模式零结果(语料里没有「钱」「退」整词),语义模式精准命中退款簇:这就是 embedding 解决的「词汇鸿沟」;② 查「VIP 有什么特权」——「VIP」从未在语料出现,语义模式照样落进会员簇中央;③ 查「怎么导出像素作品」后切换两种模式——都能命中:关键词检索没死,所以生产系统是混合检索。

retrieval.compare(semantic, keyword)

青色 Q = 查询 · 绿色编号点 = Top-k 命中 · 灰点 = 未命中文档 · 注意中央的菜谱/天气干扰簇

ANN 索引:HNSW 如何在十亿向量里毫秒寻邻

暴力扫描十亿个 1024 维向量找最近邻:每查询 ~4TB 内存读取,不可行。生产用近似最近邻(ANN),主流是 HNSW(分层可导航小世界图):

VIDEO 01
RAG Explained: Embedding, Sentence BERT, Vector Database (HNSW)
Umar Jamil 49:24
观看指南 · 全管线技术深潜,与本章 §2-§4 对应
  • 12:00 Sentence-BERT 与对比学习——为什么不能拿普通 BERT 的 CLS 向量直接用。
  • 28:00 向量数据库在 RAG 管线中的位置。
  • 35:00 HNSW 逐层图解——「先飞机后骑车」的完整算法版,本章 §4 的影像版。

分块:RAG 质量的隐形上限

文档怎么切,决定了检索单元的语义完整性——分块做坏了,后面全部白搭

生产级检索:混合召回与重排序

实验室实验③的工程化:语义检索懂同义改写但对精确字符串(型号、报错码、人名)反而不如 BM25 关键词检索。生产标准是两路召回 + 融合 + 重排

  1. 混合召回:BM25 取 top-50 ∪ 向量取 top-50。
  2. RRF 融合:不比较两路异构的分数,只用排名——$\text{RRF}(d) = \sum_r \frac{1}{60 + \text{rank}_r(d)}$,简单到不像话但稳健有效。
  3. Cross-encoder 重排:把 query 和每个候选拼在一起过一遍小模型打分(§2 说的精度天花板更高的那种),对融合后的 top-20 精排出 top-5。双塔召回(快而糙)+ 交叉重排(慢而准)的两级漏斗,和推荐系统的召回/精排完全同构。

RAG 评估与失败模式清单

RAG 是管线,评估必须分段归因(第 8 章评测先行方法论的 RAG 版):

维度问题度量
检索质量该找的找到了吗Recall@k / MRR(需要标注的 query→相关块对)
忠实度答案是否只基于检索内容LLM-as-judge 逐句核对引用(RAGAS 思路)
答案相关性是否回答了问题本身LLM-as-judge / 人工抽检

失败模式清单(按管线位置排查,附最常见解药):

长上下文 vs RAG 与 Agentic RAG

「上下文都 1M token 了,把全部文档塞进去不就行了?」三个理由让 RAG 死不了:成本(第 9 章刚算过:每次请求付全库 token 的 prefill 钱,KV cache 吃显存;检索只付 top-5 块);精度(lost in the middle 在百万 token 下更严重——大海捞针评测好看,多针推理仍掉链子);权限与时效(检索层天然支持逐用户 ACL 过滤和秒级增量更新,塞上下文做不到)。实践是光谱:文档 <50K token 直接塞(缓存还便宜,第 9 章),大库用 RAG,两者混用。

演进方向是 Agentic RAG:检索不再是「一次查询一次生成」的固定管线,而是把搜索变成 Agent 的工具——模型自己决定查不查、查什么、查几轮,对结果不满意就改写查询再查,跨文档多跳推理。这正是下一章的主题:当 LLM 拿到工具和循环,一切管线都变成了它的决策。

代码实战:60 行最小 RAG

python · minimal_rag.py(sentence-transformers + numpy,无需向量库)
import numpy as np
from sentence_transformers import SentenceTransformer, CrossEncoder

# ---- 离线:建库(双塔的文档侧,预计算一次) ----
docs = open('knowledge.md').read().split('\n## ')        # 结构感知分块:按二级标题
embedder = SentenceTransformer('BAAI/bge-m3')             # 中英双语 embedding
doc_vecs = embedder.encode(docs, normalize_embeddings=True)  # (n, 1024),归一化后点积=余弦

# ---- 在线:检索 + 重排 + 生成 ----
def retrieve(query, k_recall=20, k_final=5):
    q = embedder.encode([query], normalize_embeddings=True)
    sims = doc_vecs @ q.T                                 # 一行矩阵乘=全库余弦相似度
    cand = np.argsort(-sims[:, 0])[:k_recall]             # 召回 top-20(玩具规模暴力扫;上量换 HNSW)
    reranker = CrossEncoder('BAAI/bge-reranker-v2-m3')    # 交叉编码器精排
    scores = reranker.predict([(query, docs[i]) for i in cand])
    return [docs[cand[i]] for i in np.argsort(-scores)[:k_final]]

def answer(query):
    chunks = retrieve(query)
    context = '\n\n'.join(f'[资料{i+1}] {c}' for i, c in enumerate(chunks))
    prompt = (f"仅依据下列资料回答,资料中没有的信息明确说「资料中未提及」,"
              f"并标注引用的资料编号。\n\n{context}\n\n问题:{query}")
    return llm(prompt)   # 任意 LLM API;注意:context 置前可吃 prompt cache(第 9 章)

print(answer("退款多久到账?"))   # → 依据[资料2],3-5 个工作日原路退回。
把第 9 章的成本工程用在 RAG 上:这个 prompt 的结构哪里还能优化?
固定不变的指令部分置于最前(已做对),但 context 每次都变、又排在 query 前——意味着每次请求的缓存命中止步于指令结尾。两个改进:①若知识库小且查询分布集中,热门 chunk 组合会重复出现,可按「常用块固定顺序拼接」提高前缀重合度;②把指令+few-shot 示例做厚(反正全缓存),把可变部分压薄。更激进的方案:文档库整体塞进上下文吃长缓存(适合 <50K 的库,第 9 章光谱的左端)。成本视角会改变你的 prompt 结构设计——这是把两章知识拧在一起用。

章节测验