📑 本页目录(点开跳转)
11 · 检索与 RAG
⏱ 30 分钟 | ⭐ 核心 | 学过《推荐算法》的话这节会很快
🎯 一句话
模型不知道你的私有数据。RAG = 提问时临时去查资料,把相关片段塞进上下文再让它回答。 它是第 4 章渐进式披露在"知识"上的应用。
🔄 一、基本流程
【离线:建索引】
文档 → 切分成块 → 每块转成向量 → 存进向量库
↑ 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 · 框架这层皮。
✅ 检查点
- RAG 的两个阶段各做什么?
- 为什么必须混合检索而不能只用向量?举个具体例子。
- 上下文检索(Contextual Retrieval)做了什么?效果如何?
- 为什么说重排是性价比最高的一步?
- RAG 答案不对,第一步该查什么?
- HyDE 的原理是什么?
- 上下文能装 100 万 token 了,为什么还需要 RAG?
👀 答案
- 离线建索引(切分→向量化→入库);在线回答(问题向量化→检索 Top-K→塞进提示词→生成)。
- 向量懂语义但对精确的型号/专有名词不准——查「XR-2000」可能返回「XR-3000」,语义极近但答案是错的。BM25 能精确匹配。用 RRF 融合两路。
- 给每个分块补一句「它在讲什么」的上下文说明再向量化,让孤立的块也能被检索到。官方数据检索失败率降约 49%。
- 双塔检索快但不能深度交互;Cross-Encoder 把问题和文档拼一起过模型,准得多。只在已召回的几十条上跑,成本可控。加重排往往比换更好的 embedding 收益大。
- 先查检索——正确文档有没有被召回。没召回的话调提示词毫无意义。
- 先让模型假装回答生成"假答案",用假答案去检索。因为假答案和真文档的表述风格更接近,比原始问题更容易匹配。
- 成本(差几百倍)、上下文腐烂(塞满≠用得好)、时效(知识库随时更新)、可溯源(能指出答案出处)。
🛑 可以停在这里
⚡ 走神救援
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