L6.1 RAG(检索增强生成)
三维坐标
layer: L6(应用架构)|level: Engineer / Senior|pillar: 训推框架本文不把 RAG 当成「调一下 LangChain 就完事」的黑盒,而是沿着一次问答请求穿过 chunking→检索→重排→生成的真实数据流,逐段拆开每一环的工程取舍:切块策略如何影响召回、向量检索 topk 与 reranker 二阶排序的分工、混合检索如何弥补稠密向量的语义盲区,以及最关键的——怎么用可 量化的指标证明你的 RAG 真的变好了,而不是靠玄学拍脑袋。
学习目标
- 前置知识:读过 L2.2 向量库与近似最近邻检索(知道 HNSW / IVF-PQ 的取舍、什么是近似召回);了解 L4 推理服务(知道一次 LLM 调用的延迟构成、prompt 长度如何影响吞吐与成本);最好扫过 L0.1 企业 RAG 的业务动机(为什么企业要把知识「外挂」而非「训进去」)。会写基础 Python、用过任意一个
embedding模型即可,无需训练经验。 - 学完产出:① 能画出 RAG 的离线索引 + 在线问答双流水线,并说清「召回定上限、重排定精度、prompt 组装定约束」三段职责;② 能针对一份真实文档选对 chunking 策略,并讲清 chunk size 与查询粒度的匹配关系;③ 能解释稠密检索的两大盲区(精确关键词、训练外新词),并用混合检索 + RRF 融合补上;④ 能讲清 bi-encoder 召回与 cross-encoder 重排的「先广撒网、再精筛选」分工,算出 reranker 的延迟代价;⑤ 亲手跑一个纯 CPU 的最小 RAG,并用 RAGAS 风格的 context precision / answer relevancy / faithfulness 三指标量化它,建立「RAG 好坏靠指标不靠肉眼」的认知。
- 阅读姿势:盯住一条主线——「RAG 的每一环都在为最后送进 LLM 的那几百 token 上下文负责」。切块、召回、重排、改写,所有工程动作的唯一目的,都是让 LLM 在生成那一刻,手里握着的是「最相关、最干净、可溯源」的证据,而不是噪声。
背景与现状
RAG(Retrieval-Augmented Generation,检索增强生成) 的本质,是给一个参数知识被冻结在某个时间点的大模型,外挂一套可实时更新、可溯源、可审计的外部记忆。它把「让模型记住一切」这个昂贵且不可控的问题,转化为「让模型在回答前先去检索相关证据」这个可工程化、可治理的问题。
为什么它在 2023 年后成为企业落地大模型的第一选择?三个无法回避的现实矛盾:
- 幻觉(Hallucination)不可接受:纯参数生成会一本正经地编造事实。RAG 通过把检索到的原文证据塞进 prompt,把生成从「凭记忆作答」约束为「依据材料作答」,并天然提供引用溯源。
- 私域知识训不进去:企业内部文档、最新政策、实时数据无法靠重新预训练注入。RAG 让知识更新退化成重建索引,分钟级生效,无需动模型权重。
- 长上下文不是银弹:即便模型支持百万 token,把整个知识库全塞进上下文既贵又慢,且存在 "lost in the middle"(中间信息被忽略)效应。RAG 用检索做精准的上下文裁剪,只喂最相关的 top-k 片段。
业界信号:从 LangChain / LlamaIndex 的爆发式增长,到向量数据库(Milvus、Qdrant、pgvector)成为 AI 应用标配,再到 2024 年 RAG 与 Agent 的融合(Agentic RAG、多跳检索)成为主流——这说明 RAG 已经从「Demo 技巧」沉淀为生产级应用架构的地基。但同时,业界也清醒地认识到:朴素 RAG(naive RAG)的天花板很低,召回不准、噪声干扰、评估缺失是绝大多数项目卡在 60 分的根因。
原理与架构
理解 RAG,最有效的方式是把它拆成**离线索引(Indexing)与在线问答(Query)**两条流水线,看一个 query 是如何穿过 chunking、embedding、检索、重排,最终组装成 prompt 送进 LLM 的。
2.1 RAG 检索生成全链路
读这张图的关键:离线侧把文档切碎、编码、建索引(一次性、可批处理);在线侧把 query 改写、编码、混合召回、重排、组装、生成(毫秒级、对延迟敏感)。整条链路里,「召回」决定上限(没召回到的证据永远生成不出来),「重排」决定精度(把噪声压下去),「prompt 组装」决定 LLM 能否被证据约束住。
2.2 Chunking:被低估的第一道生死线
切块是整条链路里最廉价却最致命的一环——切得太大,单个 chunk 混入大量无关内容稀释向量语义;切得太小,一个完整论点被 拆散,检索到的片段缺乏上下文。主流策略:
| 策略 | 做法 | 适用场景 | 代价 |
|---|---|---|---|
| 固定窗口 + overlap | 按 token 数(如 512)切,相邻块重叠 10–20% | 通用、实现简单 | 可能切断句子 / 语义边界 |
| 语义分块(Semantic) | 按句子 embedding 相似度断点切分 | 长文档、逻辑密集 | 计算开销大 |
| 父子 / 小块检索大块 | 用小块(精准)检索,喂给 LLM 大块(完整上下文) | 问答需完整上下文时 | 索引结构复杂 |
| 基于结构 | 按 Markdown 标题 / 代码块 / 表格边界切 | 结构化文档 | 依赖文档质量 |
值得注意的是:没有最优 chunk size,只有匹配你查询粒度的 chunk size。如果用户问的是「某 API 的某个参数」,就该用小块;如果问的是「这章讲了什么」,就该用大块或父子分块。先看 query 分布,再定切块策略。
2.3 检索优化:从稠密向量到混合检索
朴素的稠密向量检索(dense retrieval)擅长语义相似,但有两个硬伤:对精确关键词、专有名词、ID/编号不敏感;对训练数据外的新词召回差。工程上的三板斧:
- Hybrid Search(混合检索):把 BM25 稀疏关键词检索 与 dense 向量检索 的结果用 RRF(Reciprocal Rank Fusion,倒数排名融合) 或加权融合。BM25 兜住精确匹配,dense 兜住语义泛化,互补。
- HyDE(Hypothetical Document Embeddings,假设文档嵌入):先让 LLM 根据 query 生成一个"假想的理想答案",再用这个假想答案的 embedding 去检索。原理是「答案与答案的向量距离,比问题与答案的向量距离更近」。
- 查询改写 / 多查询扩展:把一个模糊 query 用 LLM 改写成多个清晰子查询并行检索,再合并去重,提升召回覆盖面(对应多跳、复杂问题)。
2.4 Reranking:召回与精排的两段式分工
为什么需要 reranker?因为召回阶段用的是 bi-encoder(双塔,query 和 doc 各自独立编码),速度快但精度有限——它无法建模 query 与 doc 的细粒度交互。reranker 用的是 cross-encoder(交叉编码器,把 query 和 doc 拼起来一起过模型),精度高但慢,只能对召回的 top-k(如 50)做二阶重排,选出最终 top-n(如 5)送给 LLM。
要点在于:召回追求高 recall(宁可多召回,不能漏),重排追求高 precision(把对的排到前面)。这是一个经典的「先广撒网,再精筛选」的两段式工程范式,和搜索引擎的「召回-粗排-精排」一脉相承。
动手实践:极简代码实操
实验目标:用纯 CPU、零外部服务的方式,亲手搭一个最小但完整的 RAG(文档 → 切块 → embedding → 内存余弦检索 → 组装 prompt → mock LLM 生成),然后用 RAGAS 风格的离线指标(context precision / answer relevancy / faithfulness)量化评估它。产出物:一个可运行的脚本 + 一份指标报告,建立「RAG 不能靠肉眼看好坏,必须靠指标」的第一手认知。
3.1 环境准备
# 推荐 Python 3.11;用 uv 或 venv 隔离
python3 -m venv .venv && source .venv/bin/activate
pip install sentence-transformers numpy scikit-learn
# 可选:要跑官方 RAGAS(需联网下载模型 + LLM 评判器)再装
# pip install ragas datasets
3.2 代码:最小 RAG + 手写离线评估
下面这段代码完全离线、纯 CPU 可跑:用 sentence-transformers 生成 embedding,用 numpy 做内存余弦检索,用规则函数 mock 一个 LLM,最后手写三个 RAGAS 风格指标。
import numpy as np
from sentence_transformers import SentenceTransformer
# ---------- 0. 知识库(实际中来自切块后的文档) ----------
corpus = [
"RAG 通过检索外部文档来约束 LLM 生成,缓解幻觉问题。",
"向量检索用 embedding 的余弦相似度找语义相近的片段。",
"Reranker 是 cross-encoder,对召回结果做二阶精排。",
"BM25 是基于词频的稀疏检索,擅长精确关键词匹配。",
"Chunking 切块策略直接影响检索召回的上限。",
"HBM 显存带宽是 GPU 推理的主要瓶颈之一。", # 干扰项:与 RAG 无关
]
model = SentenceTransformer("all-MiniLM-L6-v2") # 小模型,CPU 友好
doc_emb = model.encode(corpus, normalize_embeddings=True) # 已归一化
# ---------- 1. 检索:内存余弦 top-k ----------
def retrieve(query: str, k: int = 3):
q = model.encode([query], normalize_embeddings=True)[0]
scores = doc_emb @ q # 归一化后点积 = 余弦相似度
idx = np.argsort(-scores)[:k]
return [(corpus[i], float(scores[i])) for i in idx]
# ---------- 2. 组装 prompt + mock LLM ----------
def mock_llm(query: str, contexts: list[str]) -> str:
# 真实场景换成 ollama / OpenAI;这里抽取式生成,保证答案"忠于上下文"
joined = " ".join(contexts)
if "幻觉" in query or "约束" in query:
return "RAG 通过检索外部文档约束 LLM 生成,从而缓解幻觉。"
return joined[:80] # 退化:直接返回上下文摘要
def rag_pipeline(query: str, k: int = 3):
hits = retrieve(query, k)
contexts = [c for c, _ in hits]
answer = mock_llm(query, contexts)
return answer, contexts, hits
# ---------- 3. RAGAS 风格离线指标(手写简化版) ----------
def cos(a: str, b: str) -> float:
e = model.encode([a, b], normalize_embeddings=True)
return float(e[0] @ e[1])
def context_precision(contexts, ground_truth):
"""召回上下文中与标准答案相关的比例(相关性 > 阈值算命中)"""
rel = [1 if cos(c, ground_truth) > 0.4 else 0 for c in contexts]
return sum(rel) / len(rel) if rel else 0.0
def answer_relevancy(query, answer):
"""生成答案与问题的语义相关度"""
return cos(query, answer)
def faithfulness(answer, contexts):
"""答案是否"忠于"检索到的上下文(防幻觉的核心指标)"""
return max(cos(answer, c) for c in contexts) if contexts else 0.0
# ---------- 4. 跑离线评估 ----------
eval_set = [
{"q": "RAG 如何缓解幻觉?", "gt": "RAG 检索外部文档约束生成,缓解幻觉。"},
{"q": "什么是 reranker?", "gt": "Reranker 是 cross-encoder,做二阶精排。"},
]
print(f"{'问题':<20}{'ctx_prec':>10}{'ans_rel':>10}{'faith':>10}")
for s in eval_set:
ans, ctxs, _ = rag_pipeline(s["q"])
cp = context_precision(ctxs, s["gt"])
ar = answer_relevancy(s["q"], ans)
fa = faithfulness(ans, ctxs)
print(f"{s['q']:<20}{cp:>10.3f}{ar:>10.3f}{fa:>10.3f}")
3.3 运行与观察
python min_rag_eval.py
- 你会看到每条评估样本输出三个指标:context precision(检索到的上下文有多少是真相关的)、answer relevancy(答案多大程度回应了问题)、faithfulness(答案多忠于检索证据,越高幻觉越少)。
- 故意埋的干扰项「HBM 显存带宽…」会拉低 context precision——这模拟了真实场景中召回噪声对 RAG 质量的伤害。试着把
k调大,观察精度如何被噪声稀释。 - 升级到官方 RAGAS:把 mock LLM 换成 ollama(
ollama run qwen2.5:3b),把手写指标换成ragas.evaluate(...),即可得到 LLM-as-judge 评判的工业级指标。本实验的价值在于先用透明的手写版理解每个指标在算什么,再上黑盒框架。
踩坑预警 (Gotchas)
- embedding 不归一化就点积 = 算错相似度:余弦相似度要求向量归一化,
normalize_embeddings=True不能省,否则点积量纲混乱、排序失真。 - 只看 answer relevancy 会被"答非所问但很流畅"骗过:必须同时盯 faithfulness,否则模型编一段语义相关但脱离证据的话,单看相关度反而高分——这正是幻觉的伪装。
- context precision 阈值是玄学陷阱:
> 0.4这个硬阈值高度依赖 embedding 模型与语料,换模型必须重新校准。生产中优先用 RAGAS 的 LLM 评判替代固定阈值。 - 小模型 + 短语料会"过拟合"出虚高指标:实验里语料只有 6 条,指标会偏乐观。真实评估集应有 ≥50 条覆盖不同问题类型的样本,并包含无法回答的 query 来测试模型「拒答」能力。
- 离线指标好 ≠ 线上效果好:离线评估只能筛掉明显的烂方案,最终必须靠在线 A/B(对比点击率、用户采纳率、人工标注满意度)做裁决。
深入思考
下面三题每题先给题干,再用
<details>折叠一份图文并茂的参考答案。建议先合上答案自己想 3 分钟,再展开对照。
思考题 1:召回与精排的成本权衡
你的 RAG 线上 P99 延迟超标,profile 发现 cross-encoder reranker 占了 60% 耗时。结合 2.4 节 bi-encoder 召回与 cross-encoder 重排的两段式分工,你会如何在「砍掉 reranker(省延迟但掉精度)」「减小召回 top-k(reranker 输入变少)」「换更小的 reranker 模型」「reranker 异步/缓存」之间做取舍?请结合你的**查询分布(高频 query 占比)**给出分级策略。
展开参考答案(含 reranker 延迟分级决策图 + 算一遍)
结论:reranker 慢的根因是 cross-encoder 要把 query 和每个候选 doc 拼起来逐对前向,候选越多越慢;所以正确做法不是一刀切砍掉它,而是按「查询是否高频、是否对精度敏感」分级——高频 query 走缓存直接命中,长尾低价值 query 砍 top-k 或降级小模型,只有真正需要精度的查询才付全量 reranker 的延迟。
用具体数字算一遍(说明为什么砍 top-k 比换模型更立竿见影):
- cross-encoder 的耗时近似正比于候选对数。假设单对前向 ≈ 4 毫秒,召回 top-50 → 重排耗时 ≈ 50 × 4 = 200 毫秒。
- 若把召回缩到 top-20:耗时 ≈ 20 × 4 = 80 毫秒,直接砍掉 60% 的 reranker 时间;由于 profile 里 reranker 占总耗时 60%,整体延迟约降 0.6 × 60% ≈ 36%,足以把 P99 拉回安全线。
- 换更小的 reranker(单对 4 毫秒 → 1.5 毫秒):top-50 → 50 × 1.5 = 75 毫秒,省得更多,但精度会掉,需用离线指标(context precision)确认掉幅可接受。
- 缓存的威力:若高频 query 占流量 30%,且其重排结果可缓存,则这 30% 流量的 reranker 开销直接归零——reranker 期望开销降 30%(折算到整体延迟约降 30% × 60% ≈ 18%),且零精度损失,是性价比最高的一招。
分级策略落地:① 先上 query 归一化 + 重排结果缓存(吃掉高频流量,零精度代价);② 对未命中的查询按价值分流——精度敏感的走全量 reranker,长尾低价值的缩 top-k 或降级小模型;③ 把 reranker 做成可异步/可超时降级——超时则回退到召回分数排序,保证 P99 不被尾部拖垮。对应正文 2.4 节「召回追高 recall、重排追高 precision」的两段式,这里本质是在 precision 与 latency 之间按查询价值动态调档。
思考题 2:离线 + 在线双层评估闭环设计
产品要求「上线新的 chunking 策略前必须有量化证据」。结合 2.2 节切块策略与动手实践 里的 RAGAS 风格指标,请设计一套离线 + 在线双层评估闭环:离线用什么指标集做 gatekeeper?在线 A/B 用什么业务指标做最终裁决?当离线指标涨、线上指标跌时,你会怀疑哪几个环节?
展开参考答案(含双层评估闭环流程图 + 对比表)
结论:离线指标只是「快速廉价的 gatekeeper」,能筛掉明显烂方案但不能拍板;最终裁决必须交给在线 A/B 的业务指标。当离线涨而线上跌时,几乎一定是「评估集分布偏移」或「指标与真实满意度脱节」——离线评估集没覆盖真实查询分布,或 faithfulness 高但用户要的是「有用」而非「忠于证据」。
两层评估的分工对比:
| 维度 | 离线评估(gatekeeper) | 在线 A/B(最终裁决) |
|---|---|---|
| 指标集 | RAGAS 三件套:context precision(召回是否相关)、faithfulness(答案是否忠于证据)、answer relevancy(是否回应问题) | 采纳率 / 点击率 / 人工标注满意度 / 拒答正确率 |
| 角色 | 快速、廉价、可回归——拦截明显劣化 | 慢、贵、有统计功效——决定能否放量 |
| 能回答的问题 | 「这个改动有没有把检索质量搞砸」 | 「真实用户是否更满意」 |
| 盲区 | 评估集分布若偏离真实流量,指标全失真 | 需要足够流量与时间积累显著性 |
离线涨、线上跌时的归因顺序:① 评估集分布偏移——离线评估集是历史/人造样本,新 chunking 在它上面涨,但真实线上 query 分 布不同(如更多短问、更多 ID 检索),切块改大反而切散了精确匹配;② faithfulness 与满意度的 gap——faithfulness 高只说明「忠于检索证据」,若召回本身偏了,模型很「忠诚」地复述了不相关内容,用户反而更不满意;③ 上下文完整性被破坏——新切块虽提升了单块的 context precision,却把一个完整论点拆散,LLM 拿到碎片生成质量下降。对策:把线上真实查询持续回流进离线评估集(对应 2.2 节「先看 query 分布再定切块策略」),让 gatekeeper 始终对齐真实分布。
思考题 3:RAG 的边界——会被长上下文取代吗
有人主张「长上下文模型(1M token)会让 RAG 过时,直接把知识库全塞进上下文即可」。结合 背景与现状里的三个现实矛盾与正文对 lost-in-the-middle 的讨论,请用成本、延迟、lost-in-the-middle 效应、知识更新成本、可溯源性五个维度,论证 RAG 在哪些场景仍不可替代,又在哪些场景确实可以被长上下文取代。
展开参考答案(含五维度边界对比图 + 算一遍)
结论:长上下文与 RAG 不是替代关系而是互补关系——当知识库小、更新慢、不要求溯源时,长上下文更省事;但只要知识库大、更新频繁、需要审计溯源、或对成本延迟敏感,RAG 的「精准裁剪 + 增量更新 + 引用溯源」就不可替代。把整库塞进上下文,是在每一次请求里为「99% 用不到的内容」反复付费。
用具体数字算一遍(为什么「全塞进上下文」在大知识库下成本失控):
- 假设知识库 50 万 token,模型支持 1M 上下文,技术上塞得下。
- 按某长上下文模型输入约 3 美元 / 百万 token 估算,每一次请求若把整库塞进去,光输入就 ≈ 0.5M × 3 / 1M = 1.5 美元 / 次。
- 日均 1 万次查询,则每天输入成本 ≈ 1 万 × 1.5 = 1.5 万美元 / 天——一个月约 45 万美元,且 99% 内容与当前 query 无关。
- RAG 路线:每次只召回 top-5 片段 ≈ 2500 token,输入成本 ≈ 2500 × 3 / 1M ≈ 0.0075 美元 / 次,日成本 ≈ 75 美元——两个数量级的差距,还顺带规避了 lost-in-the-middle(塞 50 万 token,中间证据极易被忽略)。
五维度边界对比:
| 维度 | 全塞长上下文 | RAG |
|---|---|---|
| 成本 | 每次请求为整库 token 付费,随库规模线性爆炸 | 只为 top-k 片段付费,与库规模解耦 |
| 延迟 | 上下文越长,prefill 越慢,TTFT 显著上升 | 上下文短,prefill 快 |
| lost-in-the-middle | 长上下文中间证据易被忽略,准确率下降 | 只喂最相关片段,规避中间盲区 |
| 知识更新成本 | 知识变化要重新拼装超长 prompt | 重建/增量更新索引,分钟级生效 |
| 可溯源性 | 难以指 明答案出自哪段 | 每条证据可回链到原文,天然可审计 |
边界结论:单份合同审阅、一篇长论文问答、需要跨全文做全局推理的一次性任务——知识库小且稳定、无溯源要求,长上下文更省事;而企业知识库问答、实时政策检索、需要引用审计的合规场景——库大、更新频繁、要溯源、对成本延迟敏感,RAG 不可替代。这正好回应背景与现状里的三个现实矛盾:幻觉要溯源、私域要更新、长上下文不是银弹。二者最终走向融合——用长上下文承载 RAG 召回的更大、更完整的片段,而非用它消灭检索。
延伸阅读
1. 核心 Paper
- Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks(RAG 原始论文,2020)— 理解 RAG 的范式起点。
- Precise Zero-Shot Dense Retrieval without Relevance Labels(HyDE,2022)— 理解假设文档嵌入如何提升零样本检索。
- Lost in the Middle: How Language Models Use Long Contexts(2023)— 理解为什么「全塞进上下文」不是银弹。
2. 相关高 Star 仓库与源码必读路径
run-llama/llama_index— 看llama-index-core/llama_index/core/node_parser/(切块策略)与.../indices/(索引抽象)。explodinggradients/ragas— 看src/ragas/metrics/下 faithfulness / context_precision / answer_relevancy 的具体实现,理解 LLM-as-judge 如何打分。langchain-ai/langchain— 看 retrievers 与EnsembleRetriever(混合检索 + RRF 融合)的实现。
3. 优质博客 / 视频
- Pinecone / Qdrant 官方博客的「Hybrid Search」「Reranking」系列实战文章。
- LlamaIndex 官方文档的「Advanced RAG」与「Evaluation」章节,覆盖从朴素 RAG 到 Agentic RAG 的演进路径。
下一篇 → L6.2 Agent 与多步推理:把本文的「单轮检索-生成」放大成「多步规划-工具调用-反思」,讲清 Agent 如何把 RAG 升级为可自主决策的推理系统。