
做了几个 RAG 项目之后你会慢慢发现一个规律第一版 Demo 几乎都能跑通但真正放到业务里问题永远不在“能不能回答”而在“为什么检索不准、为什么答非所问、为什么换了场景就崩”。很多团队把这些问题归因于模型不行、知识库数据太脏而我更愿意先看架构——如果你的 RAG 项目从一开始就把文档加载、切分、向量化、检索、重排、生成这些环节全部揉在一个 Service 类里那后续所有优化都会变成在意大利面代码里打补丁。这篇文章要展示的是一个模块化 RAG 项目的完整设计和落地过程。这里的“模块化”有两层含义第一层是 RAG 流程本身的分层拆分把加载、切分、索引、检索、重排、生成拆成可独立替换的模块第二层是工程结构上的 Maven 多模块划分让每一层都能独立开发、独立测试、独立部署。项目展示会围绕这两条线展开包含工程结构、核心接口、代码示例、效果验证和典型问题排查。如果你已经用 LangChain、Dify 或者自己写的脚本跑通过一个 RAG Demo但感觉继续优化越来越吃力那么这篇文章应该能给你一些新的思路。读完你至少能获得三样东西一是模块化 RAG 的完整架构视图二是可以参考的 Java 多模块工程骨架三是一套可执行的 RAG 效果评估和排错方法。1. 模块化 RAG 项目展示它到底在解决什么问题1.1 先看一个典型的失败场景假设你负责的产品要做一个内部知识库问答助手。第一版实现大概率是这样的把所有文档统一转成纯文本按固定长度切块用同一个 Embedding 模型生成向量写入向量库检索时直接取 Top-K 相似片段拼进 Prompt让大模型回答。Demo 阶段确实能跑通演示的时候效果也不错。但是上线之后问题一个接一个浮出来PDF 里的表格被切碎召回结果完全不可用不同部门的文档格式差异很大统一的切分规则导致长文本被截断、小标题被拆散模型偶尔引用了一份没有权限的旧文档合规立刻提出质疑用户问了一个跨文档的问题检索结果东拼西凑答案是错的但模型表述得非常流畅几乎无法一眼识破。这其实就是很多 RAG 项目从“能跑”到“能用”之间那条巨大的鸿沟。1.2 问题出在哪一环逐一排查之后你会发现问题几乎遍布每个环节。文档加载器没有按文件类型做差异化处理切分策略没有考虑语义边界向量检索只做相似度排序没有重排模型做二次精排Prompt 缺少对知识库来源的约束中间还缺少可观测日志出了问题根本定位不到是哪一环。最要命的是这些环节如果都被写在一个 Service 类里想优化检索就必须动生成代码想替换 Embedding 模型还要担心影响其他逻辑想给某类文档单独配置切分规则只能在代码里加一堆 if-else。这就是 RAG 项目必须模块化的根本原因——不是代码洁癖而是你迟早要在某个环节做替换、调优和问题定位没有一个清晰的边界这些操作全部会变得不可控。1.3 模块化解决的四个核心问题模块化 RAG 至少能解决四个非常实际的问题独立替换与扩展Embedding 模型、向量库、大模型厂商、重排模型都可以在配置层面切换代码不用大改。独立测试与评估每一层都可以单独出指标。检索效果差就先测召回不需要把生成环节一起跑起来。灵活组合策略同一个项目里可以同时存在“纯向量检索”“关键字向量混合检索”“重排后生成”等多条链路。问题可定位每层都有统一的输入输出和日志用户反馈回答不准时可以快速判断是召回问题、重排问题还是生成问题。1.4 适用场景与边界模块化 RAG 也不是银弹。如果你的场景只是临时做一个内部小工具只处理几十篇 Markdown 文档不分模块可能更省事直接一个脚本反而高效。但如果你的项目要考虑知识库规模超过几万份文档、支持多种文件格式、需要接入多个大模型供应商、要按团队配置权限策略、要持续迭代效果模块化基本上就是必经之路。还有一类场景是 Agentic RAG也就是由 Agent 来决定何时检索、检索哪类知识、要不要调用外部工具。这类系统更是建立在模块化之上的——没有清晰的模块边界Agent 的调度逻辑根本没有可落地的抓手。2. 模块化 RAG 的核心概念与整体架构2.1 RAG检索增强生成到底是什么RAGRetrieval-Augmented Generation检索增强生成字面意思是“检索 生成”。通俗理解就是让大模型在回答问题之前先去一个外部知识库里翻资料找到相关内容作为参考再生成答案。类比一下就是考试允许带小抄模型的知识是通用的但对你的私有业务并不了解RAG 就是那张“针对性小抄”。为什么需要 RAG因为大模型的知识有截止时间、不包含企业私有数据、并且在专业领域容易一本正经地胡说八道。RAG 的解决思路是把知识库内容预先切分成片段、做向量化用户提问时先从片段库里检索最相关的内容把这些内容连同问题一起交给大模型让模型基于检索结果回答。这样知识可以随时更新不重新训练模型回答还能给出出处。2.2 模块化 RAG 与传统 RAG 的差异模块化 RAG 并不是一个新的算法而是把 RAG 的实现方式从“一条流水线写到底”变成“分层组件自由组装”。理解这个差异是项目展示的关键可以用一个表格看清楚对比维度传统 RAG 实现模块化 RAG 实现流程实现一个 Service 类串联所有逻辑每个环节独立成模块替换组件改代码风险大容易引入问题替换实现类配置切换即可效果评测整体效果黑盒难以定位瓶颈可以按模块分别评测扩展 Agent基本无法接入路由、工具调用可插拔问题排查全链路日志难追踪每层有清晰边界和日志团队协作所有改动互相影响模块之间通过接口协作并行开发从工程角度讲模块化 RAG 更像是一个组件化框架而不是一个固定算法。它的核心价值在于让“检索增强”这件事变得可配置、可观测、可演进。2.3 整体架构分层一个典型的模块化 RAG 项目可以分成六层每层都有自己的职责数据接入层从本地文件、OSS、对象存储、网页、数据库等来源获取文档。文档处理层负责格式解析、去重、切分、元数据提取。索引存储层包括向量库索引、关键字索引必要时增加图索引。检索与重排层负责召回候选片段再用重排模型做精排。生成与编排层负责 Prompt 模板组装、LLM 调用、知识来源引用。评估与观测层负责效果指标、链路日志、监控告警。模块化 RAG 项目展示的核心就是这六层的落地。每一层向下依赖接口向上提供接口内部具体实现可以随时替换。2.4 模块化 RAG 与 Agentic RAG、低代码平台的关系很多新概念容易让人混淆这里做一个简单的区分。模块化 RAG 描述的是“工程结构”Agentic RAG 描述的是“检索策略升级”。Agentic RAG 是在检索层之上增加决策模块让 Agent 判断是否检索、检索哪个库、需不需要多轮检索但它底层的加载、切分、向量化、重排、生成仍然是模块化组件。至于 Dify 这类低代码平台优势是上手快几分钟就能搭出一个 RAG 应用。但深度定制时仍然需要理解模块化边界而且企业内部做私有化交付、做精细权限控制、做特殊格式解析时自研模块化项目往往比低代码平台更可控。3. 环境准备与工程结构3.1 技术选型模块化 RAG 项目展示的技术栈并不需要特别新重点是结构清晰。以下是一个常见的 Java 技术选型语言与框架Java 8 或 11 以上Spring MVC 或 Spring Boot本文展示以 Spring MVC 多模块工程为例。构建工具Maven适合多模块管理和依赖统一。文档解析PDFBox、Apache Tika、Apache POI按文件类型选用。Embedding常见的开源 Embedding 模型或云厂商 API具体版本以实际项目为准。向量库Milvus、Elasticsearch、PostgreSQL pgvector 都可以项目展示中抽象成接口。大模型OpenAI 兼容接口或国内各大模型平台统一走 HTTP 调用。3.2 Maven 多模块工程结构模块化 RAG 项目的 Maven 工程结构可以直接反映架构分层。一个典型的工程可以拆成下面这些模块rag-parent ├── rag-common // 公共工具、统一返回、异常定义 ├── rag-ingestion // 文档接入与切分 ├── rag-index // 向量化与索引写入 ├── rag-retrieval // 检索与重排 ├── rag-llm // LLM 接入与 Prompt 管理 ├── rag-web // Spring MVC Web 层war 包 └── rag-eval // 离线评测与指标计算每个模块的命名尽量和 RAG 流程对应看到目录就知道这一层负责什么。rag-common 用来放公共方法避免其他模块之间互相引用造成循环依赖。rag-web 是 Web 入口负责接收 HTTP 请求调用后续模块完成整个 RAG 流程。3.3 基于 IDEA 配置 Tomcat 容器启动因为 rag-web 是 Spring MVC 项目最终需要部署到 Tomcat 容器中这里演示一下 IntelliJ IDEA 的配置方式。第一步在 rag-web 模块的 pom.xml 中设置 war 打包方式并引入 Servlet API!-- 文件路径rag-web/pom.xml -- project modelVersion4.0.0/modelVersion parent groupIdcom.example/groupId artifactIdrag-parent/artifactId version1.0.0-SNAPSHOT/version /parent artifactIdrag-web/artifactId packagingwar/packaging dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId scopeprovided/scope /dependency /dependencies /project第二步在 IDEA 中选择 Run - Edit Configurations点击加号选择 Tomcat Server - Local。如果你本机还没配置 Tomcat需要先在 Application Servers 中指定 Tomcat 安装目录。第三步在 Deployment 页签添加rag-web:war exploded也就是展开目录模式这样改代码后热部署更友好。Application context 可以设为/rag访问路径就统一以/rag开头。第四步在 Server 页签设置 HTTP port 和 JMX port。如果本地同时跑多个 Tomcat 实例JMX 端口不能冲突否则后启动的实例会报端口占用。启动前先确认向量库地址、数据库连接、Embedding 服务地址都在配置文件中配好否则 Spring 容器初始化会因为连不上依赖服务而启动失败。启动后看到类似INFO: Server startup in [xxxx] milliseconds日志就说明 Web 容器起来了。3.4 基础配置文件以 application.yml 为例把 RAG 相关配置从代码中抽离出来是模块化项目的基本要求。配置里应该能看到完整的链路参数# 文件路径rag-web/src/main/resources/application.yml server: port: 8080 servlet: context-path: /rag rag: loader: file-path: /data/knowledge_base include-types: pdf, docx, md, txt splitter: strategy: header chunk-size: 600 overlap: 100 embedding: provider: openai-compatible model: text-embedding-v3 dimension: 1024 vector-store: type: milvus host: 127.0.0.1 port: 19530 collection: knowledge_chunks retriever: top-k: 10 use-keyword: true reranker: enabled: true model: bge-reranker-v2-m3 top-k: 4 llm: provider: openai-compatible model: qwen-max temperature: 0.2配置项按模块前缀组织每一层都有独立的配置段这样替换组件时不需要改动业务代码。比如 vector-store 从 Milvus 换成 pgvector只需要修改 type 和对应连接参数再提供一个新的实现类即可。4. 核心模块设计与实现4.1 文档加载解析模块文档加载是 RAG 流程的第一步也是最容易被低估的一步。很多人以为加载就是“把文件读成文本”但真实业务里的文档格式五花八门PDF 有的带表格、有的是扫描件Word 里有文本框和批注Excel 大多是表格结构Markdown 有标题层级HTML 有导航栏和页脚噪声。如果统一按纯文本读取信息丢失非常严重。模块化设计思路是定义一个DocumentLoader接口按文件类型分发到不同的实现类// 文件路径rag-ingestion/src/main/java/com/example/rag/loader/DocumentLoader.java public interface DocumentLoader { ListRawDocument load(DocumentSource source) throws Exception; }DocumentSource包含文件路径、文件类型、来源标识等信息。RawDocument是解析后的原始文档对象包含文本内容、元数据、标题路径等字段。典型的实现类包括PdfDocumentLoader、WordDocumentLoader、MarkdownDocumentLoader。加载器内部负责把文件解析成结构化文本同时保留文档元数据。PDF 表格可以用 PDFBox 提取文本再配合坐标信息尽量还原表格结构Word 文档用 POI 读取正文段落Markdown 直接解析标题和正文。这里要特别提醒安全边界如果是在线文件上传场景必须限制文件大小、文件类型并对文件内容做校验。加载外部 URL 时还要防止 SSRF 风险不能允许用户传入任意内网地址让服务端去请求。企业知识库涉及敏感数据时加载环节就要做权限标注为后续检索层的权限过滤打基础。4.2 文档切分模块切分直接决定检索质量。如果切得太粗一个片段里包含多个主题向量化后语义被稀释如果切得太细单个片段信息量不足检索到的内容可能是一段读不懂的碎片。常见的切分策略可以做一个对比策略思路适用场景固定窗口切分按字符数或 token 数硬切快速验证不推荐生产递归字符切分按段落、句子、字符层层回退通用文本标题层级切分按标题层级划分章节再处理章节内内容有清晰结构的文档、说明书、协议语义切分用模型判断语义边界内容复杂但质量要求高的场景在生产项目中推荐先按标题层级切出逻辑区块再对超出长度的区块做窗口滑动这样既保留章节语义又控制片段长度。切分时要保留上下文重叠量比如 block_size 是 600 个字符overlap 是 100 个字符这样前后片段不会在语义衔接处断掉。这里给一个 Markdown 标题切分的最小示例// 文件路径rag-ingestion/src/main/java/com/example/rag/splitter/MarkdownHeaderSplitter.java public class MarkdownHeaderSplitter { public ListTextChunk split(String content) { ListTextChunk chunks new ArrayList(); ListString lines content.split(\\n); StringBuilder buffer new StringBuilder(); String currentHeader ; for (String line : lines) { if (line.startsWith(#)) { if (buffer.length() 0) { chunks.add(new TextChunk(currentHeader, buffer.toString())); buffer.setLength(0); } currentHeader line; } else { buffer.append(line).append(\n); } } if (buffer.length() 0) { chunks.add(new TextChunk(currentHeader, buffer.toString())); } return chunks; } }切分后的TextChunk对象需要带上标题路径、章节号、原文顺序等元数据。这样做的好处是后续检索结果能直接告诉用户“答案来自哪个文档的哪个章节”并且可以按标题层级做权限继承。4.3 Embedding 与向量库模块Embedding 模块负责把文本片段转为向量向量库负责存储和相似度检索。为了做到可替换需要定义两个核心接口EmbeddingProvider和VectorStore。// 文件路径rag-index/src/main/java/com/example/rag/embedding/EmbeddingProvider.java public interface EmbeddingProvider { Listfloat[] embed(ListString texts); }// 文件路径rag-index/src/main/java/com/example/rag/store/VectorStore.java public interface VectorStore { void insert(ListTextChunk chunks, Listfloat[] vectors); ListRetrievedChunk search(float[] queryVector, int topK, MapString, String filter); }实际项目中一个容易踩坑的地方是向量库的选择。Milvus 适合大规模向量检索Elasticsearch 可以同时支持向量和关键字检索PostgreSQL pgvector 适合中小规模且希望复用现有数据库的场景。向量库迁移并不复杂关键是接口要统一写入时保留 chunk_id、doc_id、权限字段等元数据这样检索时才能按文档、按权限做过滤。Embedding 模型的选择要结合文本语言、领域术语和向量维度考虑。中英文混合内容优先选多语言模型垂直领域如银行、通信协议、医疗需要评估专业术语的向量表达能力。这里没有绝对最优还是要通过评测数据来判断。4.4 检索模块检索模块是 RAG 的核心也是最需要精细调节的地方。它要解决的问题是给定用户问题从向量库中找到最可能包含答案的候选片段。最简单的实现是纯向量检索把 query 向量化在向量库中找相似度最高的 Top-K 片段。但纯向量检索有个明显缺点它对专有名词、精确编号、缩写不敏感。例如用户搜索“RAG 3GPP 协议规范中的安全异常处理”如果 3GPP 这样的术语在 Embedding 中没能很好地被表达纯向量检索可能召回不准确。更好的做法是混合检索同时执行向量检索和 BM25 关键字检索再把两路结果合并去重。这样既能保证语义相关性又能保证精确关键词命中。检索链路还需要支持元数据过滤。例如只搜索某个部门、某个时间段的文档根据用户角色过滤权限范围。这些过滤条件需要在向量库查询之前拼接到 filter 中否则权限隔离只做在接口层敏感内容仍然可能被召回到 Prompt 里是一个容易被忽视的安全漏洞。4.5 重排模块向量召回返回的 Top-K 只是“粗排”它主要以向量相似度为标准并不完全等价于语义相关。比如一段文本包含相似的关键词但实际并不回答问题在向量召回中可能排名靠前。重排模型的作用就是对这些候选结果做一次精细排序。重排模块可以抽象成下面的接口// 文件路径rag-retrieval/src/main/java/com/example/rag/reranker/Reranker.java public interface Reranker { ListRetrievedChunk rerank(String query, ListRetrievedChunk candidates); }常见的重排方案是使用 Cross-Encoder 结构的模型把 query 和候选片段拼成一个序列输入模型输出相关性得分。与向量召回相比它计算更精确但因为要逐条计算速度会慢很多。所以重排只对 Top-K 候选用比如召回 20 条重排后只保留最相关的 3 到 5 条再交给生成模块。重排在中文知识库场景中提升往往非常明显。向量召回 Top-10 中正确答案排在第 8 位重排后可能提升到第 1 位。没有重排模块的 RAG 系统检索准确率很容易卡在一个不上不下的位置。4.6 生成模块生成模块负责把检索到的片段和用户问题一起组装成 Prompt然后调用大模型生成答案。这里最关键的环节是 Prompt 模板和调用策略。一个合格的 RAG Prompt 至少要包含以下约束明确告诉模型只能基于给定的知识片段回答不能使用训练记忆中的知识编造。如果给定的片段中没有答案要求模型明确回答“知识库中未找到相关内容”而不是强行生成。要求模型在回答中标注引用来源便于用户核验。对长答案给出结构化输出的要求比如分点回答。大模型调用还需要处理超时、重试、限流。实际场景中一次压测就可能触发大模型供应商的限流策略所以生成模块必须有熔断和降级机制。日志里要记录 prompt 内容、模型返回、耗时和 token 消耗方便后期效果分析。5. 完整示例从数据导入到问答运行5.1 数据导入流水线示例模块化 RAG 项目的效果最终要靠真正跑通一条完整链路来验证。这里提供一个导入接口和一个问答接口的完整示例。导入接口负责接收知识库文件完成“加载 - 切分 - 向量化 - 写入向量库”的完整流程// 文件路径rag-web/src/main/java/com/example/rag/controller/RagIngestController.java RestController RequestMapping(/api/rag) public class RagIngestController { private final DocumentLoaderFactory loaderFactory; private final MarkdownHeaderSplitter splitter; private final EmbeddingProvider embeddingProvider; private final VectorStore vectorStore; public RagIngestController(DocumentLoaderFactory loaderFactory, MarkdownHeaderSplitter splitter, EmbeddingProvider embeddingProvider, VectorStore vectorStore) { this.loaderFactory loaderFactory; this.splitter splitter; this.embeddingProvider embeddingProvider; this.vectorStore vectorStore; } PostMapping(/ingest) public R agResult ingest(RequestParam(file) MultipartFile file, RequestParam(docId) String docId) throws Exception { DocumentSource source DocumentSource.builder() .fileName(file.getOriginalFilename()) .fileType(FileTypeUtil.getExtension(file.getOriginalFilename())) .bytes(file.getBytes()) .docId(docId) .build(); DocumentLoader loader loaderFactory.getLoader(source.getFileType()); ListRawDocument documents loader.load(source); ListTextChunk allChunks new ArrayList(); for (RawDocument document : documents) { ListTextChunk chunks splitter.split(document.getContent()); chunks.forEach(chunk - chunk.setDocId(docId)); allChunks.addAll(chunks); } ListString texts allChunks.stream() .map(TextChunk::getContent) .collect(Collectors.toList()); Listfloat[] vectors embeddingProvider.embed(texts); vectorStore.insert(allChunks, vectors); return R.ok(allChunks.size()); } }这段代码的关键是引入了DocumentLoaderFactory它根据文件类型返回不同的加载器实现使得加载逻辑与后续流程解耦。如果以后要支持 Excel 格式只需要新增一个ExcelDocumentLoader并注册到工厂而不需要修改 Controller 和切分逻辑。5.2 问答接口示例问答接口负责把用户问题走完“检索 - 重排 - 生成”的完整链路并返回最终答案和引用来源// 文件路径rag-web/src/main/java/com/example/rag/controller/RagQueryController.java RestController RequestMapping(/api/rag) public class RagQueryController { private final Retriever retriever; private final Reranker reranker; private final LLMGenerator llmGenerator; public RagQueryController(Retriever retriever, Reranker reranker, LLMGenerator llmGenerator) { this.retriever retriever; this.reranker reranker; this.llmGenerator llmGenerator; } PostMapping(/query) public R query(RequestBody QueryRequest request) { ListRetrievedChunk candidates retriever.retrieve(request.getQuery(), 10); ListRetrievedChunk finalChunks candidates; if (reranker.isEnabled()) { finalChunks reranker.rerank(request.getQuery(), candidates); } String answer llmGenerator.generate(request.getQuery(), finalChunks); ListString sources finalChunks.stream() .map(chunk - chunk.getDocId() chunk.getTitlePath()) .collect(Collectors.toList()); QueryResponse response new QueryResponse(answer, sources); return R.ok(response); } }这里能看到模块化带来的直接好处问答逻辑本身非常清晰检索、重排、生成三个步骤各司其职。如果关闭重排只需要把reranker.isEnabled()设为 false如果替换重排模型只需要修改配置或替换 Reranker 实现类。5.3 运行与验证启动 Tomcat 后先用导入接口上传知识库文档再用 curl 调用问答接口验证效果curl -X POST http://localhost:8080/rag/api/rag/ingest \ -F file/data/knowledge_base/员工手册.md \ -F docIddoc_001 curl -X POST http://localhost:8080/rag/api/rag/query \ -H Content-Type: application/json \ -d {query: 申请年假需要提前几天}预期返回 JSON 结构如下{ code: 200, data: { answer: 根据员工手册第四章申请年假需要提前三个工作日提交审批。, sources: [ doc_001第四章休假制度 ] } }如果返回结果中包含正确的来源文档说明最小闭环已经跑通。但要注意接口返回答案只是第一步验证真正的效果验证需要看下一章的指标评估。6. 运行结果与效果验证6.1 效果验证要从功能验证中独立出来很多 RAG 项目在这个环节会踩一个坑接口能返回答案就认为系统完成了。但接口能返回答案不代表检索效果好不代表答案准确更不代表没有幻觉。要回答“RAG 到底做得好不好”必须有一组可量化的指标。模块化 RAG 恰恰是评估的基础。因为每一层都有独立输入输出你可以把评估拆成“检索评估”和“生成评估”两步。检索评估不依赖大模型只测试召回结果是否包含标准答案文档成本很低可以频繁回归生成评估需要测试答案的忠实度和相关性通常需要人工或更强的模型打分。6.2 RAG 知识库指标有哪些实际项目中常用的 RAG 评估指标有下面几类指标含义关注点优化方向命中率Hit Rate召回 Top-K 中是否包含标准答案所在文档关键文档是否被召回切分策略、Top-K 数量、混合检索MRRMean Reciprocal Rank正确结果在召回列表中的平均倒数排名答案排得够不够靠前重排模型、切分粒度忠实度Faithfulness生成答案是否忠于检索到的上下文是否编造、张冠李戴Prompt 约束、上下文完整性答案相关性Answer Relevance答案是否回答了用户问题是否答非所问Prompt 模板、检索意图上下文相关性检索返回的片段是否真的对回答有用召回质量召回策略、重排效果6.3 如何理解各指标指标必须配合起来看单项指标高不代表系统可靠。比如命中率很高但 MRR 很低说明正确答案虽然被召回了但排在很靠后的位置用户很可能看不到这时优先优化重排模块。再比如 MRR 很高但忠实度低说明检索出来的内容是对的但大模型在生成时没有严格基于上下文增加了自己的记忆这时要检查 Prompt 约束而不是继续调检索。还有一个很容易被忽略的指标是“检索链路耗时”。RAG 应用如果每次问答要 5 到 10 秒再准也很难用。检索层加关键字召回、重排层逐条打分、大模型生成都是耗时大户需要在指标报表中同时记录每一层的耗时。6.4 一个可落地的离线评估流程评估不需要上线后才做最好在开发阶段就建立一套离线测试集。每个测试问题配一个标准答案文档 ID 或标准答案文本。跑检索后用一段简单脚本计算 Hit Rate 和 MRR# 文件路径rag-eval/src/main/python/evaluate_retrieval.py def evaluate_retrieval(results, labels): results: {query_id: [doc_id_rank1, doc_id_rank2, ...]} labels: {query_id: [correct_doc_id, ...]} hit_count 0 rr_sum 0.0 for query_id, ranked_docs in results.items(): label_set set(labels[query_id]) for rank, doc_id in enumerate(ranked_docs, start1): if doc_id in label_set: hit_count 1 rr_sum 1.0 / rank break hit_rate hit_count / len(results) mrr rr_sum / len(results) return hit_rate, mrr每次修改切分策略、替换 Embedding 模型、调整 Top-K都重新跑一遍评估脚本对比指标变化。如果 Hit Rate 下降优先回退最近改动如果 Hit Rate 稳定但 MRR 下降重点看重排模块。生成侧的忠实度和答案相关性理想情况下也建立 50 到 100 条测试问题由业务方或使用一个更强的大模型做打分。模块化 RAG 的评测日志要记录每个查询的 query、召回片段、重排结果、最终 prompt、模型回复、耗时这样即使线上出了问题也能回溯当时具体是哪一层导致的。7. 常见问题与排查方法模块化 RAG 项目在开发和生产中都会遇到一些典型问题。下面这张排查表是根据实际项目经验整理出来的比盲目改代码成功率更高。问题现象可能原因排查方式解决方案检索结果与问题明显无关切分粒度不合理或命中错误索引查看召回 Top-K 的原始文本调整切分策略增加元数据过滤答案流畅但关键数据错误生成阶段没有约束模型使用检索内容检查 Prompt 和最终上下文加入忠实度约束要求模型引用来源查询响应时间过长向量库索引参数不合理或重排链路太重查看各层耗时日志优化索引参数减少重排候选数换 Embedding 模型后效果变差新旧向量混在同一索引检查写入批次元数据使用新的 collection重建索引中文文档解析乱码或表格丢失文档解析组件配置不对对比源文档和解析后的文本换用专用解析器或做 OCR 预处理某类文档永远检索不到加载器没覆盖该文件类型检查加载器分发逻辑注册对应的 DocumentLoader 实现下面展开几个最容易反复踩的问题。第一个是“旧向量污染”。当你决定更换 Embedding 模型时如果直接在新代码里继续往同一个 collection 写入就会出现新旧向量混在一起的情况。因为不同模型输出的向量空间不一致相似度计算完全没有意义检索效果必然恶化。正确做法是切换 collection 或给向量写入增加 model 字段写入后全量重建索引。第二个是“切分导致上下文断裂”。很多中文文档中一个完整的知识点分布在标题、段落、表格里。如果切分时只看固定字符数很容易把一个知识点拦腰截断。解决方法是先按标题层级分块再对超大块做滑动切分并且把 overlap 保留在合理范围内。第三个是“Prompt 越界”。有些团队在生成阶段把检索到的片段几乎原样全塞进 Prompt导致上下文过长、大模型抓不住重点还会显著增加耗时和成本。更合理的做法是只保留重排后最相关的前 3 到 5 个片段并让 Prompt 明确告诉模型“优先使用第一条片段片段中没有的信息不要编造”。第四个是“召回命中但答案不对”。用户检索到正确答案的文档后生成结果仍然不对这时大概率是 Prompt 没有做好指令约束。模块化项目里这个问题可以直接定位到生成模块单独调 Prompt 模板不需要重跑整个链路。8. 最佳实践与工程建议8.1 模块之间只依赖接口模块化项目最容易翻车的地方是模块之间虽然分了包但仍然是隐式依赖。比如检索模块直接读取向量库中其他模块写入的内部字段生成模块又直接操作切分结果久而久之模块边界形同虚设。正确做法是每个模块只暴露接口和简单的数据对象具体实现通过 Spring 装配或工厂策略切换模块之间不能跨层访问实现细节。8.2 用元数据驱动后续所有流程切分后的每个片段都应该带上文档 ID、来源、标题路径、更新时间、权限标识等元数据。带元数据的好处非常多检索时可以按权限过滤生成后可以给用户展示来源做效果归因时可以快速定位问题文档内容下线时可以按 doc_id 删除对应向量。如果一开始不设计元数据后面再做权限和溯源会非常痛苦。8.3 配置隔离与密钥管理不要把向量库地址、API Key 写死在代码里。多环境配置要拆分比如 application-dev.yml、application-test.yml、application-prod.yml。API Key 放入环境变量或配置中心严禁提交到 Git 仓库。生产环境建议使用专门的密钥管理服务并且按最小权限原则创建账号向量库只开放业务需要访问的端口。8.4 权限过滤一定要下沉到检索层企业知识库场景中权限控制不能只做在接口层。如果检索层不按用户权限过滤那么即使用户看不到某些文档的明文这些内容仍然可能被召回到 Prompt 中并间接通过答案体现出来。这是一个容易忽略的安全问题。正确的做法是在向量查询时把用户角色和文档权限标识作为过滤条件传到向量库让没有权限的内容从源头就进不了候选集。8.5 先跑通最小闭环再谈模块化优化最后给一个很朴素但非常实用的建议不要第一次就追求六层全上。最适合的推进节奏是先用一个最简单的单模块工程跑通“加载 - 切分 - 向量化 - 检索 - 生成”确认业务效果有基本价值然后把它拆成 Maven 多模块并定义好接口边界接着加入重排、混合检索、权限过滤最后再上离线评估和监控。模块化是逐步演进出来的不是第一版就堆出来的。8.6 从模块化 RAG 走向 Agentic RAG当你已经具备模块化 RAG 之后再演进 Agentic RAG 是非常自然的过程。模块化 RAG 提供的是稳定的底座Agentic RAG 是在检索层之上增加决策模块让它判断要不要查知识库、去哪个知识库查、需不需要多轮检索、要不要调用外部工具。很多团队一上来就想做 Agentic RAG结果问题定位特别困难恰恰是因为跳过了模块化这一步。9. 总结与后续学习方向模块化 RAG 不是多写几个 package 名的问题而是把“可替换、可评估、可观测、可定位”四个原则落到工程结构上。这篇文章展示的