第 2 章:RAG 整体架构
能用一段端到端代码跑通 RAG 的完整骨架,并讲清四大模块各管什么、怎么联动
📊 学习时长:60-75 分钟 🎯 完成后能力:能用一段端到端代码跑通 RAG 的完整骨架,并讲清四大模块各管什么、怎么联动 🔗 关联面试题:5 道(覆盖 5 个不同角度,见 Part 3)
这一章你会学到什么
- ✓ 把第 1 章的"迷你流水线"展开成端到端骨架,亲手跑通"建库 → 提问 → 召回 → 拼上下文 → 回答"
- ✓ 看懂 RAG 的四大模块(解析切分入库 / Query 理解 / 检索召回重排 / 上下文生成)各管什么
- ✓ 理解在线检索为什么是"双路召回 + 两阶段(粗排→精排)"的漏斗
- ✓ 想通一个关键问题:为什么离线要同时建"向量库 + 倒排索引"两套索引
- ✓ 建立"模块联动"的全局视角:一处没做好会顺着链路放大
本章按三段式组织,不同基础的读者各取所需:
段落 给谁看 内容 占比 🚀 Part 1 · 主线实战 RAG 零基础,想先跑起来 概念白话 + 端到端骨架代码 ~60% 🎯 Part 2 · 面试深度 想讲透、备战面试 四模块 + 两阶段 + 双索引 + 联动 ~30% 🏆 Part 3 · 验收串题 检验学到位没 关联面试题 + 自检清单 ~10%
🚀 Part 1 · 主线实战 | ~60% 这部分默认你会写 Python、会用命令行,但 RAG 零基础。不用 API key、不用装库,几十行代码把整套 RAG 的骨架跑通。
在开始之前
你需要:
- ✅ 装好 Python(3.9+),会在命令行跑
python xxx.py - ✅ 能看懂基础 Python(列表、字典、函数、for 循环)
- ✅ 不需要联网 / API key / 任何第三方库:本章的 demo 是纯 Python
- ✅ 最好先读过第 1 章,这章是把第 1 章那条"迷你流水线"展开成完整骨架
如果第 1 章还没看,建议先回去:第 1 章 · 为什么做 RAG。
先看一个教学用的"架构演进"
下面用一个虚构的保险资料问答场景拆解架构,不代表作者或学员的客户项目。假设知识库包含产品条款 PDF、培训 PPT 和录播视频字幕,最小版本常会遇到两类问题:
- 像「公司的报销制度是什么」这种流程型问题,答案明明白白躺在制度文档里,却因为只有几个关键词、纯向量检索抓不准;
- 像「怎么提升销售业绩」这种思考型问题,答案又零散分布在十几份培训材料里,召回的片段东一块西一块。
再加上召回回来没重排、Embedding 还是通用的、不认"现金价值""退保费"这些保险黑话,这些毛病各不相同,却都指向同一件事:
RAG 不是"写个检索"就完事,它得拆成几个各管一摊的模块,一个个治。
这一章,我们先把这套"分工"在代码里跑一遍(Part 1),再逐块讲透每个模块为什么这么设计(Part 2)。
3 个核心概念(先白话,再术语)
概念 1:两条链路,离线建库 + 在线问答
RAG 系统拆开看,就是两条流水线。打个餐厅的比方:
- 离线建库,像后厨提前备菜、把半成品放进冰箱:把文档解析、切分、转成向量、建好索引,存成一个"能快速查"的知识库。这条链路通常在首次建库和文档新增、更新、删除时运行,可采用全量或增量方式。
- 在线问答,像客人点单后现炒一盘:用户提问后,按需执行检索、重排、拼上下文和生成;缓存命中、路由或拒答策略可能跳过部分步骤,因此要按请求路径测量延迟。
两条链路靠"知识库/索引"这一个交汇点连起来:离线辛辛苦苦建好的库,就是在线检索时要查的那个库。
为什么非要拆成"离线 + 在线"两段?因为这两段的诉求正好相反:离线建库追求质量,允许用更长时间做解析、切分和向量化;在线问答追求可控延迟,不能在用户提问时临时处理全部原始文档。把重活挪到离线、提前建好索引,在线只做"查 + 拼 + 答",就是两条链路分开的根本原因。具体耗时必须用自己的数据规模和硬件实测。
顺手记两个词:Embedding(把文字转成一串数字向量,让"意思相近"能用数学距离算出来)、chunk(把大文档剪成的小段,每段几百字)。这两个词后面会反复出现。
概念 2:四大模块
把两条链路再切细,就是四个各管一摊的模块(Part 2 会逐个讲透):
- 解析·切分·入库(离线):把杂乱文档变成干净、带结构的知识块,后厨的"洗菜切配";
- Query 理解与路由(在线):先读懂用户到底问什么:前台的"听懂点单";
- 检索召回 + 重排(在线):从海量知识里捞出最相关的几块,后厨的"取料"。这里的召回(recall),就是"把可能相关的内容从库里捞出来"这个动作;
- 上下文问答(在线):基于捞回的资料生成答案:"装盘上桌"。
概念 3:索引(index)是什么
第 1 章的迷你 RAG,检索时是拿 query 和每一条知识硬比一遍。几条数据无所谓,但真实库里有几十万、上百万条 chunk,逐条比对会慢到没法用。
索引,就是为了"查得快"而预先建好的数据结构。 就像一本书后面的"名词索引页":你想找"等待期",不用从第一页开始翻,翻到索引页就能直接跳到第 88 页。
RAG 常见两类索引,这章先记住名字,Part 2 讲它们何时互补、何时单路已经足够:
- 向量库(向量数据库):专门存 Embedding 向量、能快速找"意思相近"的库;
- 倒排索引:像书后的名词索引页,按"词"建,查关键词又快又准。
🛠️ 跟着做:把架构图变成一段能跑的代码
我们把"两条链路"各写成几个函数,跑通一个端到端的迷你 RAG。整条流水线就 5 个函数:
Step 1:写"离线建库"的两步
新建一个文件 mini_rag_pipeline.py(完整文件见 code-snippets/mini_rag_pipeline.py),先写离线部分:
# ============ 离线:建库(只跑一次 / 定期更新) ============
docs = [
"第1条 责任范围:本保险承保意外伤害导致的身故或残疾,但以下情况除外:战争、核辐射。",
"第2条 等待期:本产品等待期为90天,等待期内出险不予赔付。",
"第3条 现金价值:现金价值等于累计已交保费扣除各项费用与风险保费。",
]
def chunk(text, size=20):
"""把一段长文本剪成多个小段(chunk)。
Args:
text: 一篇文档的全文
size: 每段多少字(真实系统按"语义"切,见第 3 章;这里图省事按定长切)
"""
# 从 0 开始,每隔 size 个字截一段:text[0:20]、text[20:40] ……
return [text[i:i + size] for i in range(0, len(text), size)]
def build_index(docs):
"""把所有文档切成 chunk,存成一个"能查的"列表 = 玩具版知识库。"""
index = []
for doc_id, text in enumerate(docs): # doc_id:记住这段来自第几篇文档(后面溯源用)
for piece in chunk(text): # 把这篇文档切成若干小段
index.append({"doc_id": doc_id, "text": piece})
return index
这就是"剪 → 存"。工程系统可以把 chunk 转成向量、建立倒排或其他索引(第 4、5 章);具体选一类还是多类索引取决于查询与评估结果,共同目标是把文档变成可检索单元。
Step 2:写"在线问答"的三步
接着写在线部分:
# ============ 在线:问答(每次提问都跑一遍) ============
def retrieve(index, query, top_k=2):
"""从知识库里找出最相关的 top_k 个 chunk。
这里用最朴素的相似度:query 里有多少个字在 chunk 文本里出现过。
真实系统换成向量相似度(第 4 章)+ BM25 关键词打分(第 5 章)。
"""
scored = []
for item in index:
# 命中数:query 的每个字,只要在这个 chunk 文本里出现就 +1
hit = sum(1 for ch in query if ch in item["text"])
scored.append((hit, item))
# 按命中数从高到低排序
scored.sort(key=lambda x: x[0], reverse=True)
# 取前 top_k 个,且至少命中 1 个字(命中 0 的直接丢掉)
return [item for hit, item in scored[:top_k] if hit > 0]
def assemble_context(chunks):
"""把召回的 chunk 拼成一段"上下文",准备喂给大模型。"""
# 每条都标上来源 doc_id;真实系统还会做 Prompt 模板 + 引用溯源(第 9 章)
return "\n".join(f"[来源 doc{c['doc_id']}] {c['text']}" for c in chunks)
def answer(index, query):
"""完整走一遍:检索 → 拼上下文 →(交给模型)回答。"""
hits = retrieve(index, query) # ① 先检索:捞出相关的几段
if not hits: # 一条都没召回 → 模型没依据,只能瞎猜
return "没检索到 → 模型只能瞎猜(幻觉)"
context = assemble_context(hits) # ② 拼上下文
# ③ 真实系统这里把 context + query 拼成 prompt 发给 LLM;玩具版只打印模型会看到啥
return f"模型将基于以下上下文作答:\n{context}"
这就是"捞 → 拼 → 答"。
Step 3:跑起来
文件末尾加上入口:
if __name__ == "__main__":
index = build_index(docs) # 离线:建库(只跑一次)
print(f"建库完成,共 {len(index)} 个 chunk\n")
print(answer(index, "核辐射保不保")) # 在线:每次提问跑一遍
在命令行运行:
python mini_rag_pipeline.py
你的终端应该看到这样的输出:
建库完成,共 7 个 chunk
模型将基于以下上下文作答:
[来源 doc0] 第1条 责任范围:本保险承保意外伤害导致
[来源 doc0] 的身故或残疾,但以下情况除外:战争、核辐
如果看到的是别的:
- 没有任何输出 / 报语法错 → 检查文件是不是保存了、Python 是不是 3.9+(
python --version); - 中文显示成乱码 → 终端编码问题,不影响逻辑,换个支持 UTF-8 的终端即可。
🐛 跑不通?看这里
报错 IndentationError 或 SyntaxError
- Python 对缩进敏感,八成是复制时把缩进弄乱了:确保函数体统一用 4 个空格缩进。
报错 NameError: name 'docs' is not defined
docs、几个函数、if __name__入口要在同一个文件里,且docs定义在最前面。
只打印了"建库完成",没有第二段输出
- 你可能把
print(answer(...))漏掉了,或者它的缩进进了别的函数里。
想换成自己的内容
- 把
docs列表里的句子换成你自己的几句话,再跑,你会看到 chunk 数量随之变化。
🛠️ 动手实验:top_k 调大调小,看召回怎么变
把入口那行换成一个会命中多条的问题,再去 retrieve 把默认的 top_k 调一调:
print(answer(index, "等待期 现金价值")) # 这个问题同时和 doc1、doc2 相关
先用默认 top_k=2 跑;再把 def retrieve(index, query, top_k=2) 的默认值改成 1、再改成 3,各跑一次。观察:
top_k=1:只给模型 1 块上下文,省 token,但万一这条不全就答不好;top_k=3:给模型 3 块,信息更全、更容错,但太多会引入噪音、还烧 token。
业务取舍:答案高度集中的精确问答,用小 top_k;答案分散的开放问题,用大 top_k。改一个数字,送进模型的"参考资料"就完全不同,这就是为什么 top_k 是个要反复调的关键参数(Part 2 还会细讲)。
这里的 token,你可以粗略理解成 1 个汉字或 0.5 个英文单词。模型一次能读的 token 有上限,所以不能无限制地多塞 chunk。
这一节你掌握了什么?
- ✅ RAG 的两条链路:离线建库、在线问答,以及它们靠"知识库"交汇
- ✅ 端到端骨架的 5 个函数,以及"剪 → 存 → 捞 → 拼 → 答"这条主线
- ✅ 亲手调了 top_k,感受到"检索给模型几块资料"是个要权衡的事
你刚才做的,对应工程系统里的哪些职责?
- 你写的
build_index= 字节、阿里、各家保险/医疗 RAG 里离线建库环节的入门版(它们会换成向量库 + 倒排索引); - 你写的
retrieve= 工程系统的检索召回职责(可选向量检索、BM25 与重排,是否组合需评估); - 你写的
assemble_context+answer= 上下文拼接 + 生成模块(换成 Prompt 工程 + 引用溯源 + 真·大模型)。
顺便注意:输出里
核辐射那条被切在了"战争、核辐",又撞上第 3 章那个"切分切坏语义"的问题。骨架虽简单,但每一块的工程化(比如切分)都直接影响最终效果,这正是后面章节要逐块补强的。但核心骨架,就是你刚跑的这几十行。
下面 Part 2,我们把这四大模块一个个讲透,并回答两个面试高频问题:为什么检索要分两阶段?为什么索引要建两套?
🎯 Part 2 · 面试深度 | ~30% 这部分把四大模块、两阶段检索、双索引、模块联动讲清楚。学完你能把"RAG 整体架构"画出来、讲明白每一块为什么这么设计。
2.1 四大模块,各管一摊
- ① 解析·切分·入库(离线):把 PDF/Word/PPT/视频变成干净、带结构、可检索的知识块。这一摊的质量是整个系统的天花板:正所谓 Garbage in, garbage out(垃圾进、垃圾出)。对应第 3、4 章。
- ② Query 理解与路由(在线):用户提问进来,先读懂"到底问什么":意图识别、Query 改写/扩写、多轮指代消解。它是系统的**"调度员 + 翻译官"**。对应第 7、8 章。
- ③ 检索召回 + 重排(在线):从海量知识里把最相关的几块捞出来,是系统的**"搜索引擎"**。对应第 5、6 章。
- ④ 上下文问答(在线):把捞回的资料拼成上下文,让大模型基于它作答,并控制回答、做引用溯源、防幻觉。是面向用户的**"答主"**。对应第 9 章。
面试提示:面试官让你"画一下 RAG 架构",别只画一条 query→检索→LLM。先把"离线 / 在线"两条链路分开,再标出四大模块的位置。这样能说明每一环怎样验收:检索不准时,应区分是解析、切分、召回、重排还是 Query 理解出了问题。
2.2 在线检索:为什么是"双路召回 + 两阶段"
很多人以为检索就是"算个相似度取 Top-K",工程系统可以根据查询与风险增加路由、双路召回或重排:
- 双路召回:同时用向量检索(懂语义)和关键词检索 BM25(精确匹配)两路去捞,再融合去重。BM25 是搜索引擎用了 20 年的"关键词打分"经典算法,第 5 章会让你亲手写一遍。
- 两阶段:先用快的方法召回一批较宽松的候选(比如粗召回 Top 50),再用慢但准的 Cross-Encoder(精排用的深度模型,准但慢) 把这 50 条重新排好,最后只取 **Top-K(比如 3–5 条)**交给模型。
这俩为什么非得一起上?还是那个保险项目的两类问题最能说明:用户问「公司的报销制度是什么」,这是个短查询、核心就"报销制度"几个字,BM25 按词面一击即中;但用户问「如何推销保险产品」,文档里写的偏偏是"如何销售保险产品",BM25 卡在"推销≠销售"上,这时候得靠向量检索把"推销"和"销售"在语义上对上。两类问题天天都来,只用一路就总有一半抓瞎,所以双路一起召回,再两阶段排好。
业内行话:面试常问"既然精排更准,为什么不直接拿精排扫全库?"。精排要对每个候选运行模型,候选规模较大时计算开销可能无法满足延迟与成本预算;因此常先用较轻的召回缩小候选,再精排。是否需要两阶段以及候选数取多少,应在同一负载下实测。
2.3 为什么离线要建两套索引
- 向量库(语义):把文字变成数字向量,靠"意思相近"找。擅长口语化、近义("孩子摔伤"能召回"未成年人意外伤害"),但对低频专有名词不准,比如"核辐射",纯向量可能召回到"辐射""核武器"这些沾边但不对的内容。
- 倒排索引 / BM25(关键词):按词建索引,靠"字面命中"找。擅长专有名词、编号、数字("第3条""等待期"又快又准),但不懂近义("推销"匹配不到"销售")。
这里还有个绕不开的词:向量库在较大规模向量中找"最相近",常会使用 ANN(近似最近邻,Approximate Nearest Neighbor),避免逐条精确计算。它用可接受的召回损失换查询加速,常见实现有 HNSW、IVF;具体提升幅度受数据规模、硬件、索引和参数影响,要与精确检索在同一数据与负载下实测。面试里被问"向量检索为什么快",可以答到"ANN 缩小候选搜索范围,再用召回与延迟共同验收"。
落到技术栈选型时,可以把 Milvus 向量检索 + Elasticsearch/BM25 关键词检索 + FastAPI 编排当成一套候选方案。组件名字并不构成结论:是否需要双索引、用哪种实现,应由查询类型、数据规模、延迟预算、运维能力和同一评估集上的对照结果决定。
业内行话:当评估集同时包含精确词匹配与语义改写,且两路错误具有互补性时,可以同时检索再融合。若单路已满足质量、延迟和成本目标,就不必为了架构完整而强行增加一套索引。面试时应说明什么查询触发混合检索、如何做对照实验,而不是把"两套都建"当成固定答案。
2.4 模块联动:别把模块当孤岛
各模块不是孤立的,一处没做好可能会顺着链路放大:切分把语义切碎 → 检索召回残片 → 生成阶段缺少完整证据。所以优化要有全局视角,下面几个因素建议联动验证:
- chunk 大小 ↔ LLM 上下文长度:块太大,一次塞不下几条;块太小,要拼很多条,容易 Lost in the Middle(塞太多内容时,模型容易忽略中间段的现象)。
- 召回 Top-N ↔ 生成质量:召回太少漏信息,太多则噪音干扰、还烧 token。
- 离线存的元数据 ↔ 在线能力:离线给每个 chunk 存了章节/来源,在线才能做过滤和"答案溯源"。
工程提示:RAG 系统需要做全链路埋点:记录每步的输入输出(解析提取了什么、召回了哪些片段、模型最终答了什么),出问题才能定位到具体环节。检索段可用 Recall@k 等指标检查正确证据是否进入候选集;指标异常时,先顺着埋点排查切分、检索和 Query 理解,不要直接把问题归因于模型。
🏆 Part 3 · 验收串题 | ~10% 学到这一步,做几道题验证一下,知道自己学到位没。
关联面试题(5 道,覆盖 5 个角度)
- 【整体架构与流程】 解释 RAG 的整体架构与完整工作流程。
- 【离线索引构建】 描述构建近似最近邻(ANN)索引的完整流程和关键技术选择。
- 【在线检索机制】 详细解释 RAG 中检索模块(Retriever)的工作机制。
- 【为什么混合检索】 为什么要采用混合检索(Sparse + Dense)?分别阐述其作用。
- 【召回链路全局优化】 优化 RAG 召回链路的方法:检索器选择、索引结构、查询重写等。
学完这一章你应该能干嘛
跟着做(Part 1):
- 能跑通端到端的迷你 RAG,看懂"剪 → 存 → 捞 → 拼 → 答"
- 能改 top_k 并说出它对召回的影响
- 能说出你写的每个函数对应真实系统哪个模块
讲深度(Part 2):
- 能分"离线/在线"两条链路画出 RAG 四大模块
- 能讲清在线检索为什么是"双路召回 + 两阶段(粗排→精排)"
- 能讲清为什么离线要同时建向量库 + 倒排索引
- 能举一个"模块联动、一处放大"的例子,并说出 2 个全局权衡
4 项以下 → 回去 Part 1 重做;4–6 项 → Part 2 再看一遍;7 项以上 → 进入下一章。
完整代码 + 数据集
本书每章的完整可运行代码 + 测试样本,都在配套 GitHub 仓库:
🔗 github.com/MisterBooo/rag-from-zero
- 跟着教程 clone 下来就能跑
- 本章的端到端迷你 RAG(
mini_rag_pipeline.py)也在里面- 欢迎 Star ⭐ / Issue 反馈
导航