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

11 · 检索与 RAG

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


🎯 一句话

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


🔄 一、基本流程

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

 【在线:回答】
   问题 → 转成向量 → 找最相似的 K 块 → 塞进提示词 → 模型回答
                          ↑ 向量检索(ANN)

最小可用实现(约 20 行):

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 分
   生产版:能做三个月

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

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

   ❌ 固定长度切(每 500 字一刀)
      → 一句话被拦腰砍断、表格被切碎、代码块被切开

   ✅ 常用策略
   ├─ 按结构切:Markdown 标题、章节、段落   ← 优先
   ├─ 语义切分:相邻句子语义突变处下刀
   ├─ 重叠窗口:块之间重叠 10–20%,防边界丢失
   └─ 父子块:检索用小块(精准),喂模型用大块(完整)⭐ 很好用

⭐ 上下文检索(Contextual Retrieval)

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

   原始块:「该季度收入增长 3%。」
           ↑ 单独看:哪个公司?哪个季度?→ 检索不到

   补充后:「本段出自 ACME 公司 2024 Q2 财报,讨论季度业绩。
            该季度收入增长 3%。」
           ↑ 现在能被「ACME 2024 收入」这样的问题命中 ✅

📌 官方数据:这个方法能让检索失败率下降约 49%(配合重排更多)。 成本是用小模型给每个块生成一句上下文——一次性投入,长期收益


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

   向量检索(语义)            关键词检索 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 条

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

🔑 实践经验加一个重排模型,往往比换更好的 embedding 模型收益大, 而且成本可控(只在几十条上跑)。如果只做一项优化,做这个。


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

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

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


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

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

 ① 检索质量:正确答案【在召回结果里】吗?
    指标:Recall@K、MRR、NDCG    ← 推荐算法的老朋友

 ② 生成质量:
    · 忠实度 Faithfulness —— 答案是否只基于检索到的内容(防幻觉)⭐
    · 答案相关性 —— 是否真的回答了问题
    · 上下文相关性 —— 召回的内容是否真有用

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

# 最小可用的检索评估
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 里混着不相关的 加 Cross-Encoder(性价比最高)
不评估检索 只调提示词,怎么调都不好 先测 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. 给每个分块补一句「它在讲什么」的上下文说明再向量化,让孤立的块也能被检索到。官方数据检索失败率降约 49%。
  4. 双塔检索快但不能深度交互;Cross-Encoder 把问题和文档拼一起过模型,准得多。只在已召回的几十条上跑,成本可控。加重排往往比换更好的 embedding 收益大
  5. 先查检索——正确文档有没有被召回。没召回的话调提示词毫无意义。
  6. 先让模型假装回答生成"假答案",用假答案去检索。因为假答案和真文档的表述风格更接近,比原始问题更容易匹配。
  7. 成本(差几百倍)、上下文腐烂(塞满≠用得好)、时效(知识库随时更新)、可溯源(能指出答案出处)。

🛑 可以停在这里

走神救援

RAG=提问时临时查资料塞进上下文,是渐进式披露在"知识"上的应用;和推荐算法的召回完全同构(向量库=ANN,Embedding=双塔物品向量)。⚠️Demo一小时60分,生产版能做三个月,差距在五个环节。①切分(最被低估):固定长度500字一刀会砍断句子、切碎表格;要按结构切+重叠10–20%+父子块(检索用小块保精准、喂模型用大块保完整)。⭐上下文检索:给每块补一句「它在讲什么」再向量化(「该季度收入增长3%」→「本段出自ACME 2024 Q2财报…」),检索失败率降约49%。②混合检索:向量懂同义但专有名词反而不准——💀查「XR-2000的功率」可能返回「XR-3000」,语义极近答案却是错的,BM25才能锁定;两路用RRF融合只用排名、不需要分数可比。③⭐重排性价比最高:先召回100条,再用Cross-Encoder精排出10条;加重排往往比换更好的embedding收益还大,只做一项优化就做它。④查询侧:改写、多查询扩展3–5个变体、HyDE(用假答案去检索)、意图路由、多跳。⑤评估分开评检索(Recall@K/MRR/NDCG)和生成(忠实度/相关性);⚠️最常见的诊断错误是答案不对就调提示词——先查检索,没召回时怎么调都没用;评估集最省力的造法:让模型给每块生成「只有这块能回答的问题」,人工抽查10%。⚠️另两个坑:块太大会混多个主题稀释向量没有拒答机制(写明「没有就说没有」+低相似度拒答)。长上下文不能取代RAG:成本(100万token vs 5个块差几百倍)、腐烂、时效、可溯源

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

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