📑 本页目录(点开跳转)
11 · 检索与 RAG
⏱ 30 分钟 | ⭐ 核心 | 学过《推荐算法》的话这节会很快
🔗 可运行实践: 入库、检索调优与引用诊断 。
🎯 一句话
模型不知道你的私有数据。RAG = 提问时临时去查资料,把相关片段塞进上下文再让它回答。 它是第 4 章渐进式披露在"知识"上的应用。
🔄 一、基本流程
信息关系
实现结构(下面是骨架;完整启动、数据与运行命令见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 分
生产版:能做三个月
差距不在"向量检索"这四个字,而在下面五个环节。
🧱 三、环节一:切分(最被低估)
关键信息
- ❌ 固定长度切(每 500 字一刀)
- → 一句话被拦腰砍断、表格被切碎、代码块被切开
- ✅ 常用策略
- 按结构切:Markdown 标题、章节、段落 优先
- 语义切分:相邻句子语义突变处下刀
- 重叠窗口:块之间重叠 10–20%,防边界丢失
- 父子块:检索用小块(精准),喂模型用大块(完整)⭐ 很好用
⭐ 上下文检索(Contextual Retrieval)
一个简单但效果显著的技巧:给每个块补一句「它在讲什么」再做向量化。
结果对照
在 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 分数量纲不同"的问题。
🎯 五、环节三:重排(性价比最高的一步)
结果对照
🔗 完全是推荐算法的「双塔召回 + 精排交叉」,同一套思想。
🔑 实践判断:正确证据已进入候选但排序错误时,先做固定候选的重排对照;漏召回或模型语种不符时,优先修对应环节, 而且成本可控(只在几十条上跑)。正确证据已经进入候选集时再测它;没召回的文档不能靠重排找回。
🔄 六、环节四:查询侧处理
| 技巧 | 做什么 | 解决什么 |
|---|---|---|
| 查询改写 | 口语化 → 检索友好 | 「上次说的那个咋样了」无法检索 |
| 多查询扩展 | 一个问题生成 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 里混着不相关的 | 固定候选测重排,核对语种、截断与代价 |
| 不评估检索 | 只调提示词,怎么调都不好 | 先测 Recall@K |
| 块太大 | 一块里混了多个主题,向量被稀释 | 切小 + 父子块 |
| 没有拒答机制 | 资料里没有也硬编 | 提示词明确「没有就说没有」+ 检测低相似度时拒答 |
📚 延伸阅读
👉 这一节对应的框架:LlamaIndex 封装的就是这条管线(切分 → 嵌入 → 检索 → 重排)。它替你做了什么、又没替你做什么,见 16b · 框架这层皮。
✅ 检查点
- RAG 的两个阶段各做什么?
- 为什么必须混合检索而不能只用向量?举个具体例子。
- 上下文检索(Contextual Retrieval)做了什么?效果如何?
- 为什么说重排是性价比最高的一步?
- RAG 答案不对,第一步该查什么?
- HyDE 的原理是什么?
- 上下文能装 100 万 token 了,为什么还需要 RAG?
👀 答案
- 离线建索引(切分→向量化→入库);在线回答(问题向量化→检索 Top-K→塞进提示词→生成)。
- 向量懂语义但对精确的型号/专有名词不准——查「XR-2000」可能返回「XR-3000」,语义极近但答案是错的。BM25 能精确匹配。用 RRF 融合两路。
- 给每个分块补一句「它在讲什么」的上下文说明再向量化,让孤立的块也能被检索到。原实验上下文向量降 35%,再加上下文 BM25 降 49%,再加重排降 67%;都是相对于特定基线的 top-20 失败率变化。
- 双塔检索快但不能深度交互;Cross-Encoder 把问题和文档拼一起过模型,可以深度比较相关性,但不保证更准。固定候选与题集,比较排序、答案质量和增加的延迟,再决定是否采用。
- 先查检索——正确文档有没有被召回。没召回的话调提示词毫无意义。
- 先让模型假装回答生成"假答案",用假答案去检索。因为假答案和真文档的表述风格更接近,比原始问题更容易匹配。
- 成本(差几百倍)、上下文腐烂(塞满≠用得好)、时效(知识库随时更新)、可溯源(能指出答案出处)。
🛑 可以停在这里
⚡ 走神救援
先记住这几件事
- RAG 在提问时检索资料,再把相关证据提供给模型。
- 先检查切分与召回,再考虑重排和生成;未检索到证据不是提示词能补救的。
- 专有名词和语义相似各有需求,可用混合检索互补。
- 分别评估检索质量与回答忠实度,没有证据时保留拒答出口。
下一节 👉 12-Harness-长时任务的骨架.md