🏠 总目录📚 本教程 11 · 检索与 RAG ← →
📑 本页目录(点开跳转)

11 · 检索与 RAG

⏱ 30 分钟 | ⭐ 核心 | 学过《推荐算法》的话这节会很快


🔗 可运行实践: 入库、检索调优与引用诊断 。

🎯 一句话

模型不知道你的私有数据。RAG = 提问时临时去查资料,把相关片段塞进上下文再让它回答。 它是第 4 章渐进式披露在"知识"上的应用。


🔄 一、基本流程

① 离线:建索引文档切分嵌入向量库 + 倒排两个都要查② 在线:回答查询混合检索向量 + 关键词重排生成回答⚠️ 只用向量会漏掉专有名词、编号、报错码 —— 关键词那一路不能省⚠️ 切分是最被低估的一环:切错了,后面做得再好都白搭
⭐ Demo 和生产的差距,几乎全在虚线左右那两个环节:切分决定了能不能检到,混合检索决定了专有名词和编号会不会被漏掉。中间的嵌入模型反而是最不需要纠结的一环。

信息关系

【离线:建索引】
文档→切分成块→每块转成向量→存进向量库
↑ Embedding 模型
【在线:回答】
问题→转成向量→找最相似的 K 块→塞进提示词→模型回答
↑ 向量检索(ANN)

实现结构(下面是骨架;完整启动、数据与运行命令见11b和11c):

# 🧩 骨架:docs、split_documents、llm 与模型名称须由自己的工程提供。
from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("...")            # 任选一个 embedding 模型
chunks = split_documents(docs)                # 切分
emb = model.encode(chunks, normalize_embeddings=True)   # ⭐ 归一化后点积=余弦

def answer(question, k=5):
    q = model.encode([question], normalize_embeddings=True)
    scores = emb @ q.T                        # 相似度
    top = np.argsort(-scores.ravel())[:k]
    context = "\n\n---\n\n".join(chunks[i] for i in top)
    prompt = (f"只根据以下资料回答。资料里没有的,直接说没有。\n\n"
              f"<资料>\n{context}\n</资料>\n\n问题:{question}")
    return llm(prompt)

🔗 如果你学过推荐算法:这就是召回。向量库 = ANN 索引, Embedding = 双塔模型的物品向量。整套基础设施完全相同。


⚠️ 二、Demo 和生产的鸿沟

对照

Demo 版:一小时搞定,效果 60 分

生产版:能做三个月

差距不在"向量检索"这四个字,而在下面五个环节。


🧱 三、环节一:切分(最被低估)

关键信息

⭐ 上下文检索(Contextual Retrieval)

一个简单但效果显著的技巧:给每个块补一句「它在讲什么」再做向量化。

结果对照

原始块:「该季度收入增长 3%。」
↑ 单独看:哪个公司?哪个季度?→检索不到
补充后:「本段出自 ACME 公司 2024 Q2 财报,讨论季度业绩。
该季度收入增长 3%。」
↑ 现在能被「ACME 2024 收入」这样的问题命中 ✅

在 Anthropic 的原实验中,Contextual Embeddings 使 top-20 检索失败率相对下降 35%;加 Contextual BM25 后为 49%,再加重排为 67%。这是特定数据与配置下的相对变化,不是业务保证。原始实验与条件。

成本是用小模型给每个块生成一句上下文——一次性投入,长期收益。


🔍 四、环节二:混合检索(别只用向量)

先分清各自擅长什么
对照项向量检索(语义)关键词检索 BM25(字面)
优势✅ 懂同义、懂意图✅ 精确匹配术语、型号、人名、代码
限制❌ 专有名词反而不准❌ 不懂同义词

同一个查询的结果

  • 例:查「XR-2000 的功率」
  • 向量可能返回「XR-3000 的功率」(语义极近,答案却是错的)💀
  • BM25 能精确锁定 XR-2000 ✅

结论:两路都要,然后融合。 常用 RRF(倒数排名融合):

$$\text{RRF}(d) = \sum_{r}\frac{1}{k + \text{rank}_r(d)}$$

💡 人话:每个文档在各路检索里排名越靠前得分越高,加起来排序。 好处是不需要分数可比——只用排名,天然解决了"向量分数和 BM25 分数量纲不同"的问题。


🎯 五、环节三:重排(性价比最高的一步)

结果对照

Embedding 检索(双塔) Cross-Encoder 重排
问题和文档分别编码,点积 问题和文档【拼在一起】过模型
✅ 快,能扫全库 ✅ 能做深度交互,效果需实测
❌ 无法深度交互 ❌ 慢,只能处理几十上百条
先召回 100 条,再用 Cross-Encoder 精排出 10 条

🔗 完全是推荐算法的「双塔召回 + 精排交叉」,同一套思想。

🔑 实践判断:正确证据已进入候选但排序错误时,先做固定候选的重排对照;漏召回或模型语种不符时,优先修对应环节, 而且成本可控(只在几十条上跑)。正确证据已经进入候选集时再测它;没召回的文档不能靠重排找回。


🔄 六、环节四:查询侧处理

技巧 做什么 解决什么
查询改写 口语化 → 检索友好 「上次说的那个咋样了」无法检索
多查询扩展 一个问题生成 3–5 个变体分别检索 单一表述漏召回
HyDE ⭐ 先让模型假装回答,用假答案去检索 问题和答案的表述差异大
意图路由 判断该查文档、查数据库、还是直接答 不是所有问题都需要 RAG
多跳检索 检索→发现缺信息→再检索 需要串联多个事实

💡 HyDE 的巧妙之处:问题「怎么申请报销」和文档「报销流程:第一步…」 在向量空间里未必近;但模型编的假答案「报销需要先填单据…」和真文档就很近了。


📊 七、环节五:评估(最容易跳过,也最致命)

必须分开评估两段,否则你不知道问题出在哪:

操作步骤

  1. 检索质量:正确答案【在召回结果里】吗?
  2. 指标:Recall@K、MRR、NDCG ← 推荐算法的老朋友
  3. 生成质量:
  4. · 忠实度 Faithfulness —— 答案是否只基于检索到的内容(防幻觉)⭐
  5. · 答案相关性 —— 是否真的回答了问题
  6. · 上下文相关性 —— 召回的内容是否真有用

⚠️ 最常见的诊断错误:答案不对就去调提示词。 先查检索——如果正确文档根本没被召回,提示词再怎么调都没用。

# 最小可用的检索评估
def eval_retrieval(qa_pairs, k=10):
    hits = 0
    for q, gold_chunk_id in qa_pairs:
        retrieved = retrieve(q, k)
        if gold_chunk_id in retrieved:
            hits += 1
    print(f"Recall@{k} = {hits/len(qa_pairs):.1%}")

造评估集的最省力办法:让模型读每个块,生成一个「只有这块能回答的问题」, 就得到了 (问题, 正确块) 对。人工抽查 10% 即可。


🕸️ 八、进阶方向

方向 解决什么
GraphRAG 构建实体关系图,回答"跨多文档的全局问题"(如"这些报告的共同主题")
Agentic RAG ⭐ 让 Agent 自己决定检索几次、检索什么(第 1 章的循环 + RAG)
多模态 RAG 检索图片、表格、图表

🤔 上下文变长了,还需要 RAG 吗

需要。四个理由:

理由 说明
成本 每次都塞 100 万 token vs 检索 5 个块,差几百倍
腐烂 塞满 ≠ 用得好(第 4 章)
时效 知识库随时更新,不用重新塞
可溯源 ⭐ 能指出答案来自哪份文档哪一段

🔗 九、和站内其他章的关系

相关的地方 这里的对应
第 4 章渐进式披露 RAG 是它在"知识"上的应用
推荐算法:召回→粗排→精排 RAG 的漏斗完全同构
推荐算法:双塔 + ANN Embedding 检索的基础设施
推荐算法:Recall@K / NDCG 检索侧评估直接复用
第 7 章工具设计 search_docs 就是一个工具,同样要控制返回体积

⭐ 一个很多人会在这一章之后立刻掉进去的坑:把 Memory 当成「RAG 换个名字」。

检索那一半确实是同一套——04b · Agent 记忆里的长期记忆照搬这一章的切分、混合检索、重排,一行不用改。 但另一半完全不同,而且这一章一个字都没讲:

RAG(这一章) Memory(04b)
谁写进去的 你,离线批量灌 ⭐ Agent 自己,在线随时写
写什么 已有文档,原样切块 要先判断「这条值不值得记」 —— 判据是什么?
旧内容怎么办 文档更新就重灌 ⭐ 新旧冲突谁赢?过期的怎么忘掉?
写错了 重建索引即可 错误会一直被检索出来,自我强化 💀

写入和治理是 Memory 独有的那一半,也是它真正难的地方。 🔗 那一章还会把「短期 / 工作 / 长期 / 情景」四层记忆分开——这一章的 RAG 只对应其中的「长期」一层, 而你在第 12 章会用到的 progress.json 是另一层(工作记忆)。先分清是哪一层,再决定用什么技术。


🕳️ 十、六个常见坑

坑 症状 修
只用向量检索 专有名词/型号查不准 加 BM25,用 RRF 融合
固定长度切分 答案被切成两半,哪块都不完整 按结构切 + 重叠 + 父子块
没有重排 Top-5 里混着不相关的 固定候选测重排,核对语种、截断与代价
不评估检索 只调提示词,怎么调都不好 先测 Recall@K
块太大 一块里混了多个主题,向量被稀释 切小 + 父子块
没有拒答机制 资料里没有也硬编 提示词明确「没有就说没有」+ 检测低相似度时拒答

📚 延伸阅读

👉 这一节对应的框架:LlamaIndex 封装的就是这条管线(切分 → 嵌入 → 检索 → 重排)。它替你做了什么、又没替你做什么,见 16b · 框架这层皮。


✅ 检查点

  1. RAG 的两个阶段各做什么?
  2. 为什么必须混合检索而不能只用向量?举个具体例子。
  3. 上下文检索(Contextual Retrieval)做了什么?效果如何?
  4. 为什么说重排是性价比最高的一步?
  5. RAG 答案不对,第一步该查什么?
  6. HyDE 的原理是什么?
  7. 上下文能装 100 万 token 了,为什么还需要 RAG?
👀 答案
  1. 离线建索引(切分→向量化→入库);在线回答(问题向量化→检索 Top-K→塞进提示词→生成)。
  2. 向量懂语义但对精确的型号/专有名词不准——查「XR-2000」可能返回「XR-3000」,语义极近但答案是错的。BM25 能精确匹配。用 RRF 融合两路。
  3. 给每个分块补一句「它在讲什么」的上下文说明再向量化,让孤立的块也能被检索到。原实验上下文向量降 35%,再加上下文 BM25 降 49%,再加重排降 67%;都是相对于特定基线的 top-20 失败率变化。
  4. 双塔检索快但不能深度交互;Cross-Encoder 把问题和文档拼一起过模型,可以深度比较相关性,但不保证更准。固定候选与题集,比较排序、答案质量和增加的延迟,再决定是否采用。
  5. 先查检索——正确文档有没有被召回。没召回的话调提示词毫无意义。
  6. 先让模型假装回答生成"假答案",用假答案去检索。因为假答案和真文档的表述风格更接近,比原始问题更容易匹配。
  7. 成本(差几百倍)、上下文腐烂(塞满≠用得好)、时效(知识库随时更新)、可溯源(能指出答案出处)。

🛑 可以停在这里

⚡ 走神救援

先记住这几件事

下一节 👉 12-Harness-长时任务的骨架.md

打卡记录保存在你的浏览器里,首页能看到总进度