Agent实战 #03:RAG模块从理论到工程落地 📅 2026/7/18 13:42:35 Agent实战系列|RAG模块从理论到工程落地本文RAG 不是新东西但把它做到生产可用难度远超大多数人的预期。本文基于 Dream-SaaS 项目的 RAG 模块实战经验从架构设计、混合检索、分块策略到 Prompt Engineering拆解一个 Java Agent 系统中 RAG 模块的真实落地过程——包括踩过的坑和做过的取舍。为什么要自己做 RAG 模块大模型有三个绕不开的问题知识过时、缺乏私有知识、幻觉。RAGRetrieval-Augmented Generation的核心思想很简单——先检索再回答相当于给 LLM 开卷考试。道理简单做起来不简单。在 Dream-SaaS 这个 Java Agent 系统中RAG 面临一组现实约束Java 生态缺乏成熟的 RAG 框架Python 有 LangChain、LlamaIndexJava 没有对标物企业内部知识库格式混杂PDF、Word、Markdown解析质量参差不齐检索质量直接决定回答质量纯向量召回在专有名词场景下表现糟糕需要与 Agent 编排层解耦同时又要提供足够的扩展点这些问题不是选一个框架就能解决的。最终的决定是基于 Spring AI 自建 RAG 模块分两层架构实现。两层架构为什么这么拆Dream-SaaS 的 RAG 模块被拆成了两个独立的微服务模块职责关键能力dream-ai-rag独立 RAG 微服务向量存储、文档解析、关键词检索ILIKE、基础索引dream-ai-agent-rag-serviceAgent 集成层混合检索BM25RRF、Rerank、Prompt 组装、评测对比另有一个dream-ai-agent-ragdoc模块承载前沿探索GraphRAG、AgenticRAG。这个拆分的核心逻辑是职责分离dream-ai-rag做的是脏活累活文档解析、切片、向量化、存储。它的接口通过RagLearningPort暴露是一个简单的门面dream-ai-agent-rag-service做的是精细活混合检索、多路打分融合、Rerank 精排、检索质量评测。它通过RagFragmentRetriever接口承载完整管线为什么不合一因为检索策略的迭代速度远快于存储层。你可能每周都在调 BM25 权重、Rerank 模型、RRF 参数但不可能每周重构向量存储。拆开之后改检索策略不影响底层换向量数据库也不影响上层。核心技术点一混合检索与 RRF 融合纯向量检索有个致命弱点专有名词召回率低。用户问BM25 算法的原理向量模型可能返回文本搜索方法——语义相近但没有提到 BM25。关键词检索恰好相反精确匹配强但无法理解同义词。混合检索的思路是两路都要然后融合。Dream-SaaS 实现了四种检索模式模式召回策略适用场景VECTOR_ONLY仅向量相似度通用语义问答BM25_ONLY仅 BM25 关键词专有名词/精确匹配HYBRID向量 BM25 RRF 融合需要兼顾语义和关键词HYBRID_RERANK混合 DashScope Rerank 精排问答默认模式召回宽、排序准这些模式定义在RagRetrievalMode枚举中是整个检索管线的核心开关。RRF 融合让两路分数说上话向量检索输出余弦相似度0~1BM25 输出的是无界分数。两者量纲完全不同怎么融合**RRFReciprocal Rank Fusion**的巧妙之处在于不看分数只看排名。RRF_score(d) Σ weight_i × 1 / (k rank_i(d))在LegacyRagRetrievalAdapter中核心流程是向量召回多取一些fetchK ≥ finalK×2给融合留余地BM25 打分在向量候选集上做进程内 BM25 打分不是全库关键词检索RRF 融合两路排名按权重累加得到融合排序Rerank 精排调用 DashScope Rerank API 做交叉编码重排组装输出每个 RagFragment 携带四路分数vectorScore / bm25Score / fusedScore / rerankScore注意一个设计取舍这里的 BM25 是在向量候选集上打分的不是像 Elasticsearch 那样全库倒排索引。好处是实现简单、速度快代价是向量完全没召回的专有名词补不回来。这是一个典型的工程权衡。核心技术点二BM25 与 Rerank 的实现细节进程内 BM25轻量但有边界Bm25Ranker是一个纯 Java 实现的轻量 BM25 打分器没有依赖外部索引。核心参数参数值作用k11.2词频饱和系数控制 TF 增长的天花板b0.75长度归一化强度0不考虑长度1充分惩罚长文档分词采用简单的非字母数字切分小写策略。对英文效果不错但中文是短板——连续中文整句可能被当成一个 token。生产环境需要替换为 jieba 或 ICU 分词器。// Bm25Ranker 的打分核心 for (String term : qTerms) { int f tf.getOrDefault(term, 0); if (f 0) continue; // IDF稀有词权重大 double idf Math.log(1 (n - dfi 0.5) / (dfi 0.5)); // TF 饱和 长度归一 double denom f K1 * (1 - B B * len / avgLen); sum idf * (f * (K1 1)) / denom; }DashScope Rerank交叉编码精排DashScopeTextRerankService负责调用阿里云 DashScope 的 Rerank API。它的设计有两个值得注意的点优雅降级API Key 未配置或调用失败时返回RerankOutcome.skipped()保持原始排序不阻断流程结构化输出返回orderedIds排序后的 ID 列表scoresById每个 ID 的 relevance_score方便上层按需使用核心技术点三分块策略对比分块策略直接影响检索质量。DocumentChunker支持三种可切换的策略策略原理优势劣势TOKEN按 Token 数切分 智能标点截断精确对齐模型上下文窗口无 overlap可能切断语义PARAGRAPH按空行分段保留文档自然结构段落长度不可控SEMANTIC按句子边界聚合到目标长度语义完整性最好句子边界检测可能不准TOKEN 策略底层用的是 Spring AI 的TokenTextSplitter它的智能标点截断机制是一个亮点// 取前 chunkSize 个 token 后找最后一个标点截断 if (tokens.size() chunkSize) { int lastPunct getLastPunctuationIndex(chunkText); if (lastPunct ! -1 lastPunct minChunkSizeChars) { chunkText chunkText.substring(0, lastPunct 1); } }这个设计的意义在于尽量在句号、问号、换行处截断避免从句子中间切断。实验表明保持语义边界的分块能让检索召回的片段更完整最终生成的答案也更连贯。一个值得注意的问题TokenTextSplitter默认使用 CL100K_BASE 编码对齐 OpenAI 模型并且没有实现 overlap。相邻块之间没有共享内容在对话上下文场景可能丢失连贯性。如果你的场景需要 overlap需要自行扩展。源码亮点RagFragment 与四路分数设计整个检索管线的输出统一封装在RagFragment中它是一个 record携带了完整的溯源信息和四路分数public record RagFragment( String text, // 片段正文 String documentId, // 来源文档 ID String filename, // 来源文件名 Integer chunkIndex, // 块序号 String knowledgeBaseId,// 知识库 ID String chunkStrategy, // 分块策略 Double vectorScore, // 向量相似度分 Double bm25Score, // BM25 关键词分 Double fusedScore, // RRF 融合分对外主分 Double rerankScore, // Rerank 精排分 Map metadata // 扩展元数据 ) {}为什么要把四路分数都暴露出来因为评测需要。RagCompareService可以用四种模式各跑一遍对比同一 query 下不同策略的命中情况和耗时。没有这些分数的透传检索质量调优就是盲人摸象。工程踩坑实录坑一文档解析质量是隐性瓶颈项目使用 Apache Tika 做文档解析TikaDocumentReader支持近百种格式。但实际体验中PDF 的表格、多列布局经常导致文本提取顺序混乱BodyContentHandler(-1)不限制字符数超大文档有 OOM 风险元数据作者、页码没有被暴露给下游Document只注入了 source 字段应对方案对 PDF 场景考虑使用 Spring AI 的PagePdfDocumentReader或ParagraphPdfDocumentReader替代通用 Tika 解析。坑二BM25 的中文分词问题前文提到Bm25Ranker的 tokenize 方法对中文不友好。一个真实的案例查询向量数据库选型可能被当成一个 token 处理导致 BM25 完全失效。当前缓解方式dream-ai-rag 模块里另有一条 ILIKE 扫描通路KeywordSearchService用 N-gram 凑召回。但这是两套独立的稀疏通路没有融合。坑三检索质量调优缺乏度量早期没有评测体系时检索质量好不好全靠肉眼看。后来引入了RagEvaluationService和RagCompareService配合预置的rag-queries.json测试集实现了四种检索模式的 A/B 对比每次检索的耗时上报RagRetrievalMonitorReporter参考 RAGAS 框架的四维评估Context Precision / Recall / Faithfulness / Answer Relevance前沿探索AgenticRAG 的状态图编排在dream-ai-agent-ragdoc模块中已经实现了两个前沿方向的探索代码AgenticRAG自适应检索策略AgenticRagOrchestrator基于阿里通义 Graph 框架构建了一个状态图StateGraph实现了查询分解 → 多策略检索 → 评估充分性 → 自适应切换的闭环START → [Plan] → [Retrieve] → [Evaluate] ↓ ↓ 不充分→切策略 充分→[Generate] → END关键设计检索策略会在 vector → graph → keyword 之间循环切换最多迭代 3 轮。每一轮都会用RetrievalEvaluator评估检索结果是否充分。GraphRAG知识图谱增强KnowledgeGraphService提供了图结构的检索通路通过KnowledgeGraphPromptBuilder和GraphSubgraphAnswerPromptBuilder将图谱查询结果融入 Prompt。这为多跳推理场景如A 公司的 CEO 和 B 公司有什么合作提供了可能。架构全景图最后用一张图总结整个 RAG 模块的数据流几点思考1. RAG 的 80% 工作在数据侧。文档解析质量、分块策略选择、元数据管理——这些不性感的工作决定了系统 80% 的上限。模型和算法只是在这个上限内做优化。2. 混合检索是必选项不是可选项。纯向量检索在专有名词、编号、代码片段等场景下的召回率不够用。BM25 RRF 融合的实现成本不高但收益显著。3. 评测体系是检索质量的护栏。没有评测的 RAG 系统就是在裸奔。哪怕只是一个简单的测试集 四模式对比也比肉眼看效果强十倍。4. Java 生态的 RAG 基建在补课。Spring AI 提供了基础能力向量存储、文档解析、分块但混合检索、Rerank、评测这些生产级能力需要自建。好消息是 Spring AI 的扩展性足够好自建的成本可控。RAG 不是银弹但在当前大模型的能力边界下它仍然是让 AI 系统懂业务最务实的方案。Dream-SaaS 的 RAG 模块还在持续迭代中——AgenticRAG 的自适应检索、GraphRAG 的多跳推理、以及中文分词的优化都是接下来要重点攻克的方向。有问题评论区见欢迎交流~Agent实战系列导航编号标题核心内容状态#00从0到上线14个Agent4C8G部署全记录14个AI工具上线、极简部署、避坑清单✅已发#01Dream-SaaS 整体架构拆解28 个 Java 模块 5 个独立部署 App 的宏观架构✅已发#02一个 Agent 的 5 层工程化能力Code Review Agent 从 Demo 到生产的工程细节✅已发#03RAG 模块从理论到工程落地混合检索、分块策略、四路分数设计本文本文#04多Agent协作Supervisor DAG编排任务分解→并行调度→checkpoint→HITL→Reflexion自反思即将更新#05多模态Agent实战Vision LLM 截图理解→Bug工单/设计Token/结构化JSON⏳规划中#06生产级Agent可观测评估GuardrailsOpenTelemetry链路追踪、评估体系、输出守卫链⏳规划中