最近有位读者在准备知识库问答项目,他翻了不少 GitHub 上收藏的 RAG 项目,发现一个很有意思的现象:有的项目就是单个 Python 脚本从头写到尾,文档导入、切分、向量化、检索、生成全挤在一起;有的项目则是一堆 Spring Boot 服务堆叠,看起来模块很多,但真正要换一个 Embedding 模型或者换一种文档解析方式时,改动范围还是很大;还有一些项目把前端交互做得非常炫,可一旦回到“文档能不能被正确检索到”这个基本问题上,反而解释不清。
他问我:这类项目到底该怎么组织?
我的答案不是“去抄哪个框架”,而是先理解一个底层问题:模块化 RAG 项目解决的不是“多拆几个包”,而是让 RAG 流程的每一层都拥有清晰的边界,可以独立测试、独立替换、独立演进。这句话看起来平淡,实际落地时最容易翻车的恰恰就是边界问题。这篇文章我会从工程组织、选型逻辑、落地流程和排查思路四个层面展开,重点讲清楚“怎么把一个 RAG 项目从脚本演变成可维护的工程”。
1. 先搞清楚“模块化 RAG”到底在解决什么问题
1.1 一个 RAG 项目最容易在什么时候开始失控
很多团队开始做 RAG 时,都是从一个 Demo 脚本起步的。这个阶段非常简单:读取几个 PDF,调用一个 Embedding 接口,把文本塞进向量库,然后用大模型回答用户问题。单机脚本从输入到输出全部跑通,体验很惊艳,但这只是“流程没有断”,并不代表项目具备工程化能力。
问题通常会在三个时间点集中爆发。
第一个时间点是换数据源或换文档类型。昨天还在处理 Markdown 文件,今天要接入扫描版 PDF,明天可能还要处理表格型文档。如果解析逻辑和切分逻辑写死在一起,一个格式适配就会牵扯到后面的向量化和检索逻辑,改动范围迅速扩大。
第二个时间点是换模型或换向量库。团队测试了两个 Embedding 模型,发现另一个中文效果更好,或者想从本地向量库迁到生产级向量库。如果索引模块和检索模块之间没有清晰接口,模型换掉之后就要满项目找哪里引了旧的嵌入维度,这种排查会非常消耗耐心。
第三个时间点是业务方开始提需求。要按部门过滤文档、要显示引用来源、要支持增量导入、要控制回答的温度和 prompt。这些需求单独看都不复杂,但项目如果从一开始就没有模块边界,改一个需求就要动到底层数据流,最后演变成谁也不敢改的代码块。
这也是为什么我坚持认为,RAG 项目不能一直停留在“能跑”的状态。它需要的是工程化组织,而模块化就是其中最基本的一种组织方式。
1.2 模块化的本质是给流程画边界,而不是把项目拆成微服务
模块化 RAG 很容易被误解成“拆得越碎越好”。有人甚至一上来就把项目拆成六七个微服务,每个模块独立部署、独立数据库,最后运维成本比功能开发成本还高。这其实走入了另一个极端。
模块化的核心不是“拆分”,而是边界。每个模块要有明确的输入、输出和职责范围。RAG 项目从数据到答案,天然存在一条链路:
- 数据接入模块:读取原始文件或数据源,输出统一格式的文档对象。
- 解析与切分模块:把文档解析成可检索的文本块,输出切分后的片段列表。
- 索引模块:对片段做向量化并写入向量库,输出向量 ID 和元数据。
- 检索模块:处理用户查询,召回相关片段,输出带分数的内容列表。
- 重排模块(可选):对召回结果做更精细的排序。
- 生成模块:组装上下文,调用大模型,输出答案和引用信息。
- 展示与接口模块:暴露查询接口,承接前端或业务系统调用。
如果每个模块只依赖上游的“接口约定”,而不是直接操作上游的内部数据,那么任何一层都可以独立替换。今天换解析器,明天换向量库,后天加一个重排模块,都不会导致整个链路推倒重来。
模块化也不等于微服务。在项目早期,用 Maven 多模块、Python 多包或者单仓库多目录都能实现模块化。真正的标准只有一个:你说要替换其中一个模块时,改动范围能否被限制在这个模块内部。
2. 搭建模块化 RAG 项目的最小骨架,先别急着选框架
2.1 动手前先画模块依赖图,而不是先选框架
我见过很多项目是反着来的——先选定一个 Java 框架或 Python 框架,再根据框架的功能去设计模块。框架先行也不是不行,但容易让项目结构跟着框架走,而不是跟着业务链路走。
更稳妥的顺序是:先画出 RAG 项目的模块依赖图,再决定用哪个技术栈和框架去实现。
一个常见的依赖方向是这样的:
api/server 层 ↓ retrieval 检索模块 ↓ generation 生成模块 ↓ index 索引模块 ↓ ingest 数据接入与解析模块注意,生成模块并不直接依赖原始文档,它只依赖检索模块返回的片段列表。索引模块不关心用户查询,只关心文档如何被切分和向量化。数据接入模块不关心检索逻辑,只负责把各种格式的文件变成统一的文档对象。这种依赖关系,保证了每一层都能单独开发和验证。
如果是用 Spring Boot 作为宿主工程,目录结构可以这样组织:
com.company.rag ├── ingest │ ├── datasource │ ├── parser │ └── splitter ├── index │ ├── embedder │ └── vectorstore ├── retrieval │ ├── retriever │ └── reranker ├── generation │ ├── prompt │ ├── llm │ └── context └── api ├── controller └── dto这个结构不完美,但它给了团队一个很直观的认知地图:文档从接入到回答,每一步都有对应的包位置。新人拿到代码,能很快定位到“我想改解析逻辑应该去哪”。
2.2 四个关键模块的接口边界定义
接口边界是模块化设计里最容易含糊的地方。很多项目把模块拆出来了,但 JSON 对象类型定义得乱七八糟,上游改一个字段,下游全线报错。所以接口定义必须尽量稳定且有语义。
可以先用一个表格把核心接口约定固定下来:
| 模块 | 输入 | 输出 |
|---|---|---|
| ingest | 原始文件、URL、数据库记录 | 统一 Document 对象 |
| index | Document 列表 | Chunk 列表、向量 ID、元数据 |
| retrieval | 用户查询 | 排序后的 Chunk 片段列表 |
| generation | 用户查询 + 检索结果 | 答案 + 引用来源 |
| api/server | HTTP 请求 | JSON 响应 |
这里的 Document 对象建议包含字段:来源标识、标题、正文、原始路径、解析时间、解析方式。Chunk 对象建议包含:片段 ID、来源文档 ID、文本内容、分块序号、元数据字段(如页码、章节名)。
约定这些字段不是为了增加工作量,而是为了让每个模块都能独立做单元测试。比如测试 ingest 模块时,只需要验证“输入一个文件,输出一个 Document 对象”;测试 index 模块时,只需要验证“输入一个 Document,输出若干 Chunk”。如果中间夹了一条“顺便打印日志再顺手存一下数据库”的代码,测试就会变得不可控。
2.3 用一条最小链路先验证工程是否成立
模块化项目最忌讳的是“结构很完整,流程没跑通”。我建议任何时候都先做一条最小可运行链路,再去补全生产级功能。
最小链路可以这样定义:
- 准备 1 到 2 份真实的业务文档,而不是直接用网上随便下载的 PDF。
- 手动触发一次完整流程:文档解析 → 切分 → 向量化 → 写入向量库 → 检索 → 生成回答。
- 验证三个点:切分结果是否可以肉眼阅读、检索返回的片段是否包含用户问题对应的关键信息、生成回答是否引用了正确的来源。
这个阶段不要做批量导入、权限系统、前端页面,也不要做复杂的重排和 Agent 调度。先把最基础的链路跑通,确认每个模块都能独立工作、接口之间没有 hidden assumption。
注意:不要一上来就把批量数和并发数拉满,先用一条样例确认输入、输出和日志都正常,再考虑扩展。
3. 关键环节怎么选型,以及几个容易忽略的坑
3.1 文档解析与切分:RAG 质量的第一道闸门
很多人把注意力放在大模型和 Embedding 模型上,觉得只要模型强,RAG 效果就不会差。但实际项目中,文档解析和切分对效果的影响往往被严重低估。
先看解析。PDF 有文本型和扫描型,文本型 PDF 直接抽取文字即可,但经常混入页眉页脚、页码和乱序文本;扫描型 PDF 需要 OCR,OCR 之后可能丢失版式和表格结构。Word 文档要处理 doc/docx 差异,Markdown 要保留标题层级,HTML 要去掉标签和广告内容。这些解析逻辑看似琐碎,却是整个 RAG 链路的入口。解析如果不干净,后面的所有模块都会把错误放大。
再看切分。固定长度切分实现最简单,按字符数或 token 数硬切,但容易把一段完整的语义断成两半。按标题结构切分更贴近文档本身的逻辑,前提是文档标题层级清晰。重叠窗口可以减少信息断裂,但如果重叠太多,又会造成大量冗余内容。这里没有银弹,更稳妥的做法是:先做一版切分,把结果打印出来肉眼检查几段,看看关键信息有没有被切断。
3.2 Embedding 与向量库的选择逻辑
Embedding 模型的选择要结合业务语言。中文场景如果直接拿英文优化的模型,检索效果大概率不理想。业界已有不少中文表现较好的开源模型,落地前要先用自己的业务语料做一次小规模评测,而不是只看模型介绍。
这里有一个很容易被忽略的坑:换 Embedding 模型意味着全部索引要重建。不同模型的向量维度不同,向量库里的 collection 也要重新创建。所以选型阶段不要急着定,可以多测两三个模型,确定一个再批量生成索引。一旦生成完成,不建议频繁更换。
向量库的选型则要看部署和维护成本。Chroma 很适合学习和小规模验证,数据量上来后要关注性能和持久化问题。Milvus、Qdrant 这类专业向量库适合生产环境,支持集合管理、元数据过滤和批量写入。pgvector 适合团队已经有 PostgreSQL 且数据量可控的场景,可以减少一个中间件。
从工程角度看,向量库本身不是最关键的决策点,真正重要的是它是否满足你的使用方式:能否支持按来源文档删除索引、能否支持元数据过滤、能否和现有部署环境兼容。
3.3 重排模块:什么时候值得加
向量检索的召回结果通常只看语义相似度,而语义相似不代表业务相关。一个用户问“报销流程”,检索出来的片段可能包含“差旅报销”“采购报销”“报销制度”,甚至“预算管理”。如果 TopK 里塞满了这些宽泛的内容,生成模块就会被噪声干扰。
重排模块的作用就是在召回之后做更精细的排序。常见方案是用交叉编码器模型,对候选片段和用户查询逐条打分,取前 5 到 10 条进入生成阶段。这能明显提升最终答案的精确度,但代价是增加一轮模型计算。
实际落地时,我会建议先不加重排,让基础链路跑通。然后准备一个带标准答案的评测集,对比加与不加重排的差异。如果评测显示检索结果中前排片段确实更精准了,再考虑引入。如果基础召回本身就不好,提前加重排只是把错误结果重新排了一遍。
4. 从单条链路到批量任务,模块化 RAG 的落地流程
4.1 先建立一个小型评测集,而不是靠“感觉”判断效果
很多 RAG 项目团队在调优阶段走了弯路,原因很简单:没有评测集。今天问几个问题觉得回答还行,明天换个场景发现不行,但没人知道到底哪里不行。
更务实的做法是,在项目早期就建立一个 20 到 50 条的小型评测集。每条问题配上标准答案或期望命中的关键知识点,然后重点关注几个指标:
| 指标 | 含义 | 何时观察 |
|---|---|---|
| 上下文命中率 | 标准答案中的关键信息是否出现在检索到的片段中 | 每次索引或解析变更后 |
| 检索召回率 | 相关文档片段是否被检索出来 | 调 TopK 和切分策略时 |
| 答案准确率 | 最终回答是否和标准答案一致 | 调 Prompt 或换模型时 |
| 幻觉率 | 回答是否包含原文中没有的信息 | 每次生成链路变更时 |
| 端到端延迟 | 从提问到返回答案的时间 | 每次加模块后 |
评测集规模不用大,但一定要覆盖真实的业务问题。没有评测集,调参就等于盲调。
4.2 批量导入与增量更新的工程处理
从单条链路走向批量任务时,最需要重视的是幂等性。同一份文档重复导入,不应该产生重复的 Chunk。实现幂等通常需要两个字段:文档的唯一来源标识和 Chunk 的来源文档 ID。导入前先检查文档是否已经存在,存在则先删除旧索引再写入新的,避免脏数据堆积。
批量导入还要考虑失败如何重试。文档解析可能因为文件损坏、编码问题、格式不支持而失败;向量化可能因为模型服务超时而失败。不要把失败任务直接丢掉,而是记录到任务表里,标记失败原因,支持手动重新触发。
这里存在一个很多人忽略的问题:增量更新不是“把新文件解析一下再塞进去”就完了。如果新版本文档和旧版本文档内容不一致,需要先移除旧版本对应的全部 Chunk,再写入新版本的 Chunk。这个逻辑写在索引模块里,比写在业务流程里更安全。
4.3 配置管理与模块开关
RAG 项目涉及大量配置项:Embedding 模型名称与维度、向量库连接信息、切分参数、TopK 数量、是否启用重排、大模型 API 地址和 Key、提示词模板路径。这些配置如果散落在代码里,后续排查会非常困难。
更合理的方式是通过配置中心或至少一个集中配置文件管理,并根据环境(开发、测试、生产)做区分。模块开关也很重要,比如用一个配置项控制是否启用重排模块。这样同一套代码在不同环境下可以灵活组合,而不是为了临时关掉某个模块改代码重发。
前端展示不是 RAG 核心模块,但有一个建议值得采纳:返回结果时把引用来源一起返回。这个看起来小的设计,对线上排查却非常关键。用户说回答不对时,你先要看的是检索模块返回了哪些片段。如果引用来源清晰,问题就能快速定位到是检索不好还是生成不好。
5. 全链路排查:回答不对、检索不到、速度变慢分别怎么查
5.1 检索不到相关内容,按这个顺序查
“检索为空”或“检索结果明显不相关”是 RAG 项目最常遇到的问题。排查时不要直接改参数,按下面的顺序走一遍:
- 先确认文档是否真正完成索引。检查文档解析是否有报错,切分结果是否为空,向量库中是否能查到对应 Chunk。常见坑是文档格式不支持,Parser 静默返回空文档。
- 再检查切分结果。如果关键信息恰好被切到两个 Chunk 的边界,检索时可能无法在单个片段中命中完整答案。检查切分后的片段能否独立表达语义。
- 再检查 Embedding 维度是否一致。如果模型换过,旧向量库里的向量维度可能和新模型不一致,查询时会直接报错或返回空。
- 再看检索参数。TopK 值太小、相似度阈值设置过高,都会导致相关片段被过滤掉。调试阶段可以先放开阈值,看看实际排在前面的都是什么内容。
- 最后检查查询预处理。用户输入的问题如果包含大量口语或专有名词,可能需要先做改写或关键词提取,再进入检索。
这个顺序背后的逻辑很简单:先把数据链路查清楚,再查模型和参数,最后查输入。很多人一上来就调 TopK,结果问题其实出在文档根本没有成功向量化。
5.2 答案不准确,问题往往出在上下文组装
检索到了相关内容,但生成答案仍然不正确,这时候问题通常不在检索模块,而在上下文组装和生成模块。
需要依次检查:
- 检索返回的片段是否真的进入了 Prompt。有些框架默认只取分数最高的 3 条,但如果这 3 条里只有 1 条是真正相关的,另外 2 条会稀释模型注意力。
- 上下文是否被截断。如果大模型上下文窗口有限,而 Chunk 太长或太多,后半部分内容可能永远不会被模型看到。需要确认组装后的上下文 token 总数。
- Prompt 是否给了模型行为约束。更稳妥的做法是在 Prompt 中明确告诉模型“只基于给出的上下文回答,不要补充已知知识”。否则模型可能在相关片段缺乏时自行脑补。
- 引用来源是否和答案内容匹配。如果引用的来源本身就没包含答案信息,那说明上下文组装环节可能在逻辑上有缺陷。
答案不准确问题的本质,是“证据链断裂”。生成模块拿到的证据不足,或者证据被上下文顺序和截断策略削弱了。
5.3 响应慢,瓶颈通常不在大模型
RAG 项目上线后,用户最容易感知到的问题是响应慢。排查思路不是直接优化大模型调用,而是先拆时间。
第一步是给全链路加日志,记录每个模块的耗时:文档解析时间、Embedding 调用时间、向量检索时间、重排时间、LLM 生成时间。然后看哪个环节耗时占比最高。
常见慢点有:
- 向量检索前没有索引,导致全量扫描。
- 重排模块对 100 条候选逐条计算,造成额外延迟。
- Embedding 服务和 LLM 调用没有超时控制,服务端卡住后前端一直等待。
- 批量任务同步执行,单个大文档解析阻塞了后续所有请求。
- 前端等待完整生成结果才显示,而不是用流式输出。
从工程经验看,最有效的优化通常是异步化和流式化:索引任务放到后台执行,LLM 生成改为流式返回。这种优化能显著改善体感,同时对原有链路改动最小。
6. 再往前走一步:Agentic RAG、多模态 RAG 和模块化的关系
6.1 Agentic RAG 本质上是把“决策”也模块化
Agentic RAG 是 RAG 的一种进阶形态。传统 RAG 是一条固定链路:用户问题进来,检索一次,生成一次。Agentic RAG 让模型自己决定是否需要检索、用什么查询词检索、是否需要多次检索、是否需要调用其他工具。这意味着原来的“检索模块”不再是一个固定步骤,而是被一个 Planner 模块调度。
从模块化角度看,这是一个很自然的演进:你不需要推翻原来的链路,只需要新增一个决策模块,让它按需调用已有的检索模块。但前提是基础检索链路必须稳定。否则 Agent 会把检索错误放大:第一次查不到、第二次改写查询还是查不到,最后把不相关内容拼进上下文。
6.2 多模态 RAG 给模块化增加了新的数据层
多模态 RAG 也是同样的逻辑。文档里除了文字,还有表格、图片、流程图。传统解析模块只能提取文本,多模态场景需要增加图像解析、表格识别、视觉向量化等能力。
这些新增能力放在模块化架构里,其实只是扩展了 ingest 模块和 index 模块的输出类型。原有检索模块、生成模块不需要大幅改动,只要你把新数据对象的标准定义好了。这也是模块化带来的实际收益:不是所有改动都需要全局重构。
6.3 什么场景不适合模块化 RAG
模块化不是银弹,有一些场景强行模块化反而增加负担。
- 临时性实验:只是想验证一个概念,跑通即可,不用维护长期工程结构。这时候直接看官方 Demo 反而效率更高。
- 没有继续迭代计划的项目:如果知识库内容不会持续增加,不需要多人协作,模块化设计带来的复杂度不值得。
- 团队维护能力不足:模块化需要每个模块都有清晰的接口文档和测试。如果团队没人维护,拆得越细,后续越难接手。
真正适合模块化 RAG 的场景,是知识库会持续增长、检索质量需要持续调优、业务方会不断提新需求的项目。在这种项目里,模块化换来的不是最快速度,而是可维护性和可演进性。
7. 不妨从最小链路开始,把边界画清楚
回到开头那个读者的疑问。面对一堆 RAG 项目,最该学的不是某段代码,而是怎么看待这条链路。
我给他的建议很简单:不要一开始就去搭建一个庞大的框架,也不要满足于一个跑通的脚本。先画模块图,定义好每个模块的输入输出,用两三个真实文档把最小链路跑通,然后逐步加批量、加重排、加评测、加 Agent。每一个环节加进来之前,都要确认它不会破坏模块之间的边界。
模块化 RAG 项目真正值得投入的地方,不是把技术栈换成最新最热门的那一套,而是让项目在复杂度增长的过程中,依然能被理解、被测试、被安全地修改。能做到这一点,项目才具备了持续演进的底子。