本文按"总---分---总"的结构,拆解一个实际 RAG 项目的整体流程:离线部分负责文档加载、父子块切分与向量入库;在线部分由 FAQ 缓存和 RAG 检索两条链路组成,最终把检索结果交给大模型整理返回。文中也会说明关键参数、设计权衡和面试表达思路。
一、总览:RAG 项目分为离线与在线两大部分
一个完整的 RAG 系统,通常可以拆成两条链路:
-
离线链路:加载文档 → 文本切分 → 向量化 → 存入向量库;
-
在线链路:接收用户问题 → FAQ 匹配或 RAG 检索 → 大模型生成 → 返回答案。
在我们的项目中,在线部分又进一步拆成两条路径:
-
FAQ 链路:缓存约 500 个高频问答对,优先用 Redis 精准匹配,匹配不到再用 BM25 做相关问题检索;
-
RAG 链路:对用户问题做分类、查询改写、混合检索、重排,最后把父块 TopK 交给大模型生成。
整体架构可以概括为:

下面按离线、在线两部分展开。
二、离线部分:数据读取、父子块切分、向量存储
离线部分主要做三件事:数据读取、文本父子块切分、向量存储。
1. 数据读取:不同格式用不同加载器
项目中的文档格式包括 PDF、DOC、PNG。不同格式采用不同的加载方式:
-
DOC:直接读取文本;
-
PDF、PNG :使用 PaddleOCR 读取文本。
除了正文内容,读取阶段还会记录文档元数据,例如:
-
文档名;
-
作者;
-
发布时间;
-
是否有效。
这些元数据后续会一起存入向量库,用于过滤、追溯和结果展示。比如"是否有效"可以在检索阶段过滤掉失效文档,避免脏数据进入生成环节。
2. 文本切分:父子块切分,父块 1500 token,子块 300 token
文本切分尝试过多种方式,最终选择父子块切分:
-
父块:约 1500 token;
-
子块:约 300 token。
这样设置的原因很直接:测试下来召回率效果比较好 。
父子块的核心思想是:
-
用子块做向量化和检索,粒度更细,召回更精准;
-
命中子块后,取它对应的父块作为生成上下文,保证大模型拿到的信息更完整。
如果只用小碎块,召回准但上下文容易断裂;只用大块,上下文完整但向量表达容易稀释。父子块是在"召回精度"和"上下文完整性"之间做平衡。
3. 向量存储:bge-m3 向量化,稀疏与稠密一起入库
使用 bge-m3 模型对子块做向量化,得到:
-
稀疏向量;
-
稠密向量。
然后将以下内容一起存入 Milvus:
-
子块的稀疏向量和稠密向量;
-
子块对应的父块内容;
-
文档元数据。
这样在线检索时,可以同时利用稀疏和稠密两路召回,命中子块后直接取父块内容,不需要再回表拼装。
离线部分可以总结为一张表:
| 模块 | 做法 | 目的 |
|---|---|---|
| 数据读取 | DOC 直接读;PDF/PNG 用 PaddleOCR | 兼容多格式文档 |
| 元数据记录 | 文档名、作者、发布时间、是否有效 | 过滤、追溯、展示 |
| 文本切分 | 父块 1500 token,子块 300 token | 平衡召回精度与上下文完整性 |
| 向量化 | bge-m3 生成稀疏+稠密向量 | 支持混合检索 |
| 向量存储 | Milvus 存子块向量、父块内容、元数据 | 在线检索与生成一体化 |
三、在线部分:FAQ 链路 + RAG 链路
在线部分的目标是:尽量用低成本方式解决高频问题,把复杂问题交给 RAG 检索和大模型生成。
1. FAQ 链路:Redis 精准匹配 + BM25 相关性匹配
FAQ 部分缓存了约 500 个高频问答对,分成两步:
第一步:Redis 精准匹配
用户问题先到 Redis 做精准匹配:
-
如果问题完全一致,直接返回答案;
-
如果不一致,进入第二步。
这一步成本低、延迟低,适合处理大量重复提问。
第二步:BM25 相关问题检索
使用 BM25 将用户问题与 FAQ 中的所有问题进行相关性计算,然后把相关性得分通过 softmax 转成概率分布,取最高概率对应的问题。
这里设置了一个概率阈值:0.90。
-
最高概率超过 0.90:认为找到了用户问题的相关问题,返回对应答案;
-
最高概率低于 0.90:认为 FAQ 无法覆盖,转入 RAG 检索 Milvus。
FAQ 链路大约覆盖了整体用户问题的 50%,也就是说,一半左右的问题可以通过缓存和相关性匹配低成本解决,不需要走完整 RAG 链路。
FAQ 链路可以概括为:
用户问题
→ Redis 精准匹配
→ 命中:返回答案
→ 未命中:BM25 相关性计算
→ softmax 概率分布
→ 最高概率 ≥ 0.90:返回答案
→ 最高概率 < 0.90:进入 RAG 链路
2. RAG 链路:问题分类、查询改写、混合检索
RAG 在线部分分成三步:问题分类、查询改写、混合检索。
第一步:问题分类,微调 BERT 做二分类
使用微调后的 BERT 模型做二分类:
-
与 RAG 内容无关的问题:直接交给大模型回答,不进入检索;
-
与 RAG 内容相关的问题:继续进入查询改写和检索。
这一步的价值是减少无效检索。不是所有用户问题都需要查知识库,先分类可以避免把无关问题硬塞进向量库检索,降低噪声和延迟。
第二步:查询改写,用大模型 + 提示词完成
对需要检索的问题做查询改写,常见方式包括:
-
复杂问题拆解:拆成多个子问题;
-
口语化问题简化:去掉冗余表达,保留核心意图;
-
用大模型先输出一版答案,再用答案去检索。
改写使用大模型 + 提示词完成。目的是让检索 query 更接近知识库中的表达,提高召回质量。
第三步:混合检索 + 排序 + 父块去重 + Reranker 重排
改写后的问题同时使用稠密向量和稀疏向量 做混合检索,结果用 WeightedRanker 排序。
因为离线阶段使用的是父子块切分,所以检索命中子块后,需要:
-
取子块对应的父块;
-
对父块去重;
-
使用 Reranker 模型重排;
-
取父块 TopK 结果,交给大模型做生成参考。
这一步的关键是:子块负责召回,父块负责生成。去重避免同一父块被多个子块重复带入,Reranker 则进一步修正混合检索的排序结果。
RAG 链路可以概括为:
用户问题
→ BERT 二分类
→ 与 RAG 无关:直接交给大模型
→ 与 RAG 相关:查询改写
→ 稠密+稀疏混合检索
→ WeightedRanker 排序
→ 子块对应父块去重
→ Reranker 重排
→ 父块 TopK
→ 大模型生成返回
四、关键设计权衡
这个项目中有几个值得注意的设计点:
| 设计点 | 做法 | 权衡 |
|---|---|---|
| 离线多格式加载 | DOC 直读,PDF/PNG 用 PaddleOCR | 兼容性更好,但 OCR 有成本 |
| 父子块切分 | 父块 1500 token,子块 300 token | 子块召回准,父块上下文完整 |
| 稀疏+稠密双向量 | bge-m3 同时生成 | 兼顾语义召回和关键词召回 |
| FAQ 优先 | Redis + BM25 + 0.90 阈值 | 低成本覆盖约 50% 高频问题 |
| 问题分类 | 微调 BERT 二分类 | 减少无关问题进入检索 |
| 查询改写 | 大模型+提示词 | 提升复杂/口语问题的召回 |
| 混合检索+重排 | WeightedRanker + Reranker | 先粗排再精排,提升最终相关性 |
| 父块去重 | 子块命中后取父块去重 | 避免重复上下文,控制 Token 成本 |
五、总结
这个 RAG 项目可以概括为:
离线做三件事:多格式文档读取、父子块切分、bge-m3 稀疏+稠密向量入库;在线分两条路:FAQ 用 Redis 精准匹配和 BM25 相关性匹配覆盖高频问题,RAG 用 BERT 分类、查询改写、混合检索、父块去重和 Reranker 重排,最后把父块 TopK 交给大模型生成。
核心思路是:
-
高频问题走 FAQ,低成本快速返回;
-
无关问题先分类,避免无效检索;
-
复杂问题先改写,再混合检索;
-
子块召回、父块生成,兼顾精度和上下文完整性;
-
粗排加精排,提升最终送给大模型的质量。
六、面试口述建议
如果要在面试中讲这个项目,建议保持"总---分---总":
-
先总览:RAG 分在线和离线,在线又分 FAQ 和 RAG 两条链路;
-
再分述:离线三块、在线 FAQ 两块、在线 RAG 三块;
-
最后总结:高频走 FAQ、无关先分类、复杂先改写、混合检索加 Reranker、父块 TopK 给大模型。
每讲完一个模块,可以停顿一下,主动问面试官是否想深入某一部分。如果面试官对在线更感兴趣,就优先展开 FAQ 和 RAG 在线链路;如果对离线感兴趣,再展开数据读取、父子块切分和向量存储。这样既能把控节奏,也能把面试官的注意力引到你最熟悉的设计点上。