
公司里早就建了知识库语雀、Confluence、内部 Wiki 加起来几千篇文档MySQL 里还躺着几万条 FAQ。可业务人员遇到问题第一反应仍然是在群里问同事而不是去查知识库。我见过一个客户很无奈地说明明答案都在里面大家就是找不到。 这句话背后是个很现实的问题知识库是给人看的不是给模型用的。人需要在几十篇文档里翻来翻去才能拼出一个答案而模型根本没有打开知识库的能力。Java 程序员现在要做的事就是在这条文档 - 向量 - 检索 - 大模型的流水线上把知识库变成业务系统可以随时调用的知识服务。这篇文章是一份基于 Spring AI 的 RAG 实战记录从选型、文档切分、向量化、检索问答到多轮对话、权限过滤和 Agentic RAG 的边界目标读者是已经熟悉 Java 和 Spring Boot、但对 RAG 还停留在概念层面的后端工程师。你不需要懂机器学习不需要会 Python只要会写 Java 和一点 SQL就够了。1. 先别急着写代码已有知识库离可问答还差一整条流水线1.1 先盘点你手里的知识库到底长什么样知识库不是一个东西而是多种数据形态的集合。动手之前先把现有资产按这三类盘一遍非结构化文档PDF、Word、PPT、Markdown、txt散落在文件服务器、NAS 或 Wiki 里。这是 RAG 最典型、也最头疼的输入因为格式千奇百怪PDF 可能是扫描件、可能是排版错乱的导出文件还有可能包含大量页眉页脚和导航噪声。半结构化数据Excel、CSV、JSON经常是产品手册转出来的表格、运营导出的问答对。这类数据的特点是有结构但不干净表头不统一同一类信息在不同表格里含义不一样接进来之前必须做清洗。结构化数据MySQL、PostgreSQL 里的业务表比如用户反馈、工单记录、FAQ 表。质量往往最高但要接入 RAG通常得先落成文本记录或者走 NL2SQL 通道让模型直接查询数据库。我为什么强调先盘点因为很多团队把知识库已经有了当成可以跳过前期工作的理由结果一上来就陷入局部优化纠结 Embedding 选型、纠结向量库品牌却连最基础的文档清洗都没做。知识库的存在解决了存储和浏览问题没有解决检索和生成问题。大模型的知识停留在训练截止日期它根本不认识你公司内部的项目立项审批要过三关这种说法。要让模型基于你的知识库回答问题核心就是把它改造成模型可以检索的形式——文本块加向量索引这一步就是 Java 程序员要亲手搭的管道。1.2 Java 程序员在 RAG 链路中的真实分工把 RAG 完整展开典型链路是加载 - 清洗 - 切分 - 向量化 - 存储 - 检索 - 增强 - 生成很多人以为 Java 程序员只负责写个 Controller把用户问题转发给大模型大模型自己会懂。真这么做返回的答案大概率是从训练数据里瞎编的和你的知识库一点关系都没有。实际上需要 Java 程序员亲自动手的是这些环节写数据接入管道把文件服务器、Wiki 导出包、数据库记录统一读进来变成统一的 Document 对象。这活儿看着简单做起来全是脏活PDF 里的页眉页脚、Excel 里的合并单元格、Wiki 导出的 HTML 里全是样式标签。设计切分策略一段话被切成两个块检索时只能召回一半答案自然残缺。切多大切多少、要不要重叠、中文怎么别切碎都要反复试。配置 Embedding 模型把文本变成向量这是检索效果的决定因素之一。选错模型、维度不匹配索引都建不起来。搭检索服务并调参topK 取几、相似度阈值用 0.7 还是 0.8、要不要过滤部门、要不要混合检索这些参数直接决定用户体验。设计 Prompt 模板检索出来的内容怎么喂给模型、怎么让模型只说上下文里有的内容、怎么引用来源这是 RAG 应用体验的分水岭。做权限、审计和命中分析企业内部知识一定有敏感级别谁的账号能检索到什么必须有审计日志。这个听起来不性感但上线时全是硬需求。一句话知识库只是原材料仓库Java 程序员要做的是搭建一套把原材料加工成可交付知识服务的流水线。2. Spring AI 在 RAG 链路里到底负责什么凭什么选它2.1 Spring AI 的核心组件正好覆盖 RAG 每一环Spring AI 是 Spring 官方出品的 AI 应用框架1.0 版本已经稳定。它没有发明新概念而是把 RAG 链路里每个环节都抽象成了可替换的接口DocumentReader负责加载不同来源的文档。TikaDocumentReader用 Apache Tika 解析 PDF、Word、PPT、HTML 等几十种格式PagePdfDocumentReader、ParagraphPdfDocumentReader针对 PDF 做了细化你也可以自己实现一个 DocumentReader 去读数据库或调用内部接口。TextSplitter负责切块。TokenTextSplitter按 token 切SentenceTextSplitter按句子切还支持自定义切分器。EmbeddingModel统一封装各家向量化模型。OpenAI、智谱、阿里、Ollama 都能通过 starter 引入业务代码不用关心底层 HTTP 调用和重试逻辑。VectorStore存储向量并提供相似度检索。SimpleVectorStore适合开发调试PGVectorStore、RedisVectorStore、MilvusVectorStore、ElasticsearchVectorStore都是产线常见选项。ChatClient Advisor组装最终问答。QuestionAnswerAdvisor会自动完成向量检索 - 把检索片段拼进 Prompt - 调大模型生成把 RAG 管线收口成一行代码。我用一个比喻Spring AI 对 Java 程序员的意义就像 Spring JDBC 对数据库操作的意义。以前你要自己管理连接、处理异常、拼接 SQL现在框架帮你把样板代码吃掉了你只需要关注业务逻辑。2.2 和 LangChain4j、自研方案比选型的取舍Java 生态里做 RAG 不止 Spring AI 一个选择简单对比一下我用过的几个方案对比项Spring AILangChain4j自研与 Spring Boot 集成原生一致配置即用需要额外桥接完全自己写组件丰富度覆盖加载、切分、存储、问答组件多风格贴近 Python取决于投入维护方Spring 官方社区驱动自己的团队上手学习成本低熟悉 Spring 就会用中等要学一套新 API高长期演进随 Boot 版本同步社区节奏全自己维护LangChain4j 更早出现在 Java 圈集成组件也不少。如果你的团队已经写了大量 LangChain4j 代码没必要强行迁移如果是从零开始我更推荐 Spring AI。原因有三第一Spring AI 是官方项目长期维护有保障第二配置体系和 Spring Boot 完全一致一个application.yml就能管理模型、向量库、重试第三Advisor 机制和 ChatClient 设计非常贴近 Spring 风格写起来顺手。自研我也见过不是不行只是要做太多脏活Embedding 调用要自己写重试和降级、向量库连接要自己管理、替换模型厂商要改一大片代码、版本升级没人帮你兼容。有团队自己封装了一套 RAG三个月后换向量库改了整整一周。用框架的意义就是把易变的部分隔离在配置层。2.3 一套最小可用技术栈清单这是我目前在标准 RAG 项目里的推荐组合环节推荐选型说明基础框架Spring Boot 3.3 / JDK 17Spring AI 1.x 要求 JDK 17 起步AI 框架Spring AI 1.0.x按模型厂商引入 starter对话模型智谱 GLM / 通义千问 / OpenAI 兼容接口内部工具选性价比高的即可不一定要最强模型Embedding与模型服务商配套的 embedding 模型注意维度换模型必须重建索引向量库开发用 SimpleVectorStore生产用 PGVector / MilvusPGVector 最省事复用现有 PostgreSQL文档源文件系统 / OSS / FTP按实际情况接入用 DocumentReader 抽象这组选型的原则是先跑通、再上规模。内部知识助手、客服问答这类标准 RAG这套已经够用等知识量到百万级文档再考虑专业向量库和重排服务。3. 文档加载、清洗与切分检索质量的第一道分水岭3.1 文档加载不是读文件这么简单直接看代码。将 PDF 导入向量库的最小实现TikaDocumentReader reader new TikaDocumentReader( new FileSystemResource(/data/knowledge/manual.pdf)); ListDocument documents reader.get(); for (Document doc : documents) { doc.getMetadata().put(source, manual.pdf); doc.getMetadata().put(department, finance); doc.getMetadata().put(docId, UUID.randomUUID().toString()); }Tika 的威力在于格式兼容同一个 reader 能处理 PDF、Word、HTML甚至可以从 URL 直接加载网页。但有两个问题必须提前处理第一扫描版 PDF 没有文本层Tika 读出来是空内容或乱码必须接入 OCR 服务比如 PaddleOCR 或云厂商的文档解析 API。这个判断要在项目初期做别等上线了才发现核心文档全是扫描件。第二从 Wiki 或 HTML 导入时Tika 会把导航栏、页脚、脚本都读进来。清洗时建议用正则或 HTML cleaner 去掉噪声否则这些垃圾内容会被向量化检索时频繁被命中严重污染结果。如果知识库在数据库里就自己实现一个 DocumentReader把查询结果转成 Document 列表。Document 的 content 字段就是最终要切分和向量化的正文。3.2 切块策略怎么定才不会把一句话切碎切块是 RAG 里影响最大、最容易被忽视的环节。块太大检索命中一个大块会浪费大量 token而且大块里只有一句话相关块太小一句话被切成两半语义就碎了。Spring AI 中我用得最多的是 TokenTextSplitterTokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(800) .withChunkOverlap(150) .build(); ListDocument chunks splitter.split(documents);chunkSize 和 chunkOverlap 怎么定我的经验是参考对话模型的上下文窗口。假设模型上下文是 4K token检索要拼 5 个块。如果每个块 800 token拼接后就是 4000 token留给生成的空间很少。所以一般 chunkSize 取上下文窗口的 1/6 到 1/4比如 4K 窗口配 600 到 800overlap 取 chunkSize 的 10% 到 20%让前后两个块之间保留一部分重叠文本避免语义在边界断裂。中文场景还有一点要注意TokenTextSplitter 按 token 切中文的 token 边界和自然语言分句并不完全一致有时会把完整句子从中间截断。如果文档有清晰的段落结构比如 Markdown 标题、有序列表优先按段落切分段落太长的再按 token 二次切。这个思路在实战中比无脑按 token 切效果稳定得多。3.3 元数据设计权限过滤和来源追溯的伏笔Document 除了 content还带一个 Map 类型的 metadata很多人把它当摆设其实它是后面所有精细操作的基石。我建议入库时至少放这几个字段source来源文件或 URL用于引用溯源docId文档唯一 ID用于增量更新时定位旧数据department / team所属部门用于权限过滤securityLevel数据敏感级别用于权限过滤lastModified文档更新时间用于增量索引没有元数据后面做权限过滤时只能重建整个索引代价极大。所以文档入库时宁可多放几个字段也别省这一步。好的元数据设计等于提前给检索装上了开关。4. 向量化、检索到生成让查得到升级为答得准4.1 Embedding 模型与向量库怎么搭配Embedding 就是把文本映射成一个向量。相似语义的文本向量距离近RAG 就靠这个在向量库里捞相关内容。选 Embedding 模型时看三件事中文效果通用英文模型对中文效果不一定好。中文场景优先考虑智谱 embedding-3、阿里 text-embedding-v3、BGE-M3 这类在中文语料上打磨过的模型。向量维度维度越高通常效果越好但存储和计算成本也越高。PGVector 建表时要指定维度换模型必须重建索引所以一开始就要定下来。计费与稳定性Embedding 接口按 token 收费批量建索引时开销不小。国内业务尽量选国内能稳定访问的模型服务避免批量任务因为网络问题反复失败。4.2 一段可运行的 Spring AI 检索问答链路先配置向量库以 PGVector 为例Configuration public class RagVectorStoreConfig { Bean public VectorStore vectorStore(EmbeddingModel embeddingModel, DataSource dataSource) { JdbcTemplate jdbcTemplate new JdbcTemplate(dataSource); return new PgVectorStore(jdbcTemplate, embeddingModel, PgVectorStore.PgDistanceType.COSINE_DISTANCE, true, PgVectorStore.PgIndexType.HNSW); } }注意建表前需要先执行CREATE EXTENSION IF NOT EXISTS vector;否则 PGVector 会报错。这是新手最容易踩的坑。再写导入任务Component public class KnowledgeBaseImporter implements ApplicationRunner { private final VectorStore vectorStore; public KnowledgeBaseImporter(VectorStore vectorStore) { this.vectorStore vectorStore; } Override public void run(ApplicationArguments args) { TikaDocumentReader reader new TikaDocumentReader( new FileSystemResource(/data/knowledge/faq.pdf)); ListDocument documents reader.get(); TokenTextSplitter splitter TokenTextSplitter.builder() .withChunkSize(600) .withChunkOverlap(100) .build(); vectorStore.add(splitter.split(documents)); } }问答接口RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder, VectorStore vectorStore) { QuestionAnswerAdvisor advisor new QuestionAnswerAdvisor(vectorStore, 你只可以根据知识库内容回答如果知识库没有相关信息请直接回答知识库中没有找到相关内容。回答时不要编造事实。); this.chatClient builder.defaultAdvisors(advisor).build(); } PostMapping(/chat) public String chat(RequestBody String question) { return chatClient.prompt().user(question).call().content(); } }这套代码的核心是 QuestionAnswerAdvisor你不需要手动查向量库再拼 Prompt它把检索 - 组装上下文 - 调用大模型全部封装好了。对 Java 程序员来说RAG 落地可能真的就是几十行业务代码。4.3 Prompt 增强与引用溯源答案必须有出处QuestionAnswerAdvisor 只是兜底。生产级 RAG 的 System Prompt 还要加这些约束只能基于上下文回答不能使用自身知识补充上下文无法回答时明确说未知按顺序用 [1][2] 标注来源复杂问题先拆解再回答引用溯源不能依赖大模型自己记得要在后端做。需要自定义检索时用 SearchRequestSearchRequest request SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.7) .build(); ListDocument documents vectorStore.similaritySearch(request);然后把 documents 的 metadata 里的 source 字段返回给前端展示。检索结果在哪个文件是你后端在回答时拼进 Prompt 的你应该把它原样交给前端。这既是对用户负责也是给后续答非所问审计留证据。5. 多轮对话、权限过滤与 Agentic RAG上线避不开的进阶问题5.1 多轮对话中的查询重写解决它指的是谁多轮 RAG 有个经典坑用户先问报销单怎么填第二句问要审批几天如果不带历史对话单独拿要审批几天去向量检索搜出来的内容必然跑偏。解决方案是查询重写在大模型回答前先让模型把当前问题 历史对话改写成一个独立的完整问题。String rewritePrompt 请把用户的追问改写为一个独立、清晰的问题。 要求保留关键主体不丢失上下文只输出改写后的内容。 历史对话 {history} 当前问题 {question} ;改写后的完整问题再走向量检索召回效果会好很多。这个方案实现成本很低但多轮体验的提升非常大。如果不想每次都调 LLM也可以用简单规则检测到它、这个、审批这类指代词时才触发改写能省不少 token。5.2 权限过滤必须在检索阶段做别指望大模型企业内部知识一定有边界。财务部的文档不能让人力部搜出来但如果你不主动过滤它们被检索到以后就会拼进 Prompt。你指望大模型遵守权限是不现实的因为模型根本不知道文档的权限属性。正确做法是在检索阶段就做元数据过滤。Spring AI 的 SearchRequest 原生支持 filterExpressionSearchRequest request SearchRequest.builder() .query(query) .topK(5) .filterExpression(department finance securityLevel 3) .build();filterExpression 的语法类似 Spring 的 SpEL不同向量库支持的表达式会有差异核心思路一致把权限条件翻译成检索过滤条件先过滤、再检索、再生成。这一步做扎实RAG 才敢在内部开放。5.3 RAG、Agentic RAG 和 MCP 到底有什么区别标准 RAG 是一次检索一次生成用户提问查向量库拼 Prompt输出。适合事实型、有明确答案的问题。Agentic RAG 多了一个决策循环大模型先判断需要哪些信息决定检索几次、查哪些数据源第一次检索不够再检一次最后合成答案。适合开放式、跨知识点的问题比如对比上季度和本季度报销流程的差异。MCP 则是另一个维度它解决的是模型能调用哪些工具完成任务比如查数据库、创建工单、发送邮件。RAG 解决知识从哪来MCP 解决工作怎么做。两者经常一起出现RAG 提供领域知识MCP 执行业务操作。这是当前 Agent 类应用的通用架构。很多初学者把这三者混在一起面试时能说清边界比背一个概念更有价值。6. 跑通 Spring AI RAG 时踩过的坑与调优经验6.1 版本不一致引发的一连串诡异报错Spring AI 从 0.8 到 1.0包名、类名、自动配置变化很大。网上大量教程是 0.8.x 时代的写法直接抄过来大概率编译不过或运行时报 Bean 找不到。我建议新建项目时直接用 Spring Initializr 生成最新稳定版本再去读对应版本的官方文档。遇到类找不到先怀疑版本问题不要硬改代码。我见过有人在 1.0 项目里硬塞 0.8 的依赖最后整个上下文起不来排查了一整天。6.2 中文场景切块与检索效果怎么优化中文检索效果差先按这条排查顺序走先看召回用同一问题直接调向量检索看 top 5 里有没有正确文档。如果没有问题在切块、Embedding 或过滤条件如果有但答案不对那是大模型生成环节的问题。再调参数topK 从 3 到 5 试相似度阈值从 0.6 到 0.8 试。阈值太高召回不全太低噪声太多。检查切块是否切碎语义看命中的 chunk 是不是一个完整句子如果不是换切分器或调 overlap。考虑混合检索向量检索对同义改写效果好但对精确 ID、专有名词不敏感。可以叠加 BM25 关键词检索再用 RRFReciprocal Rank Fusion合并结果。Spring AI 支持向量库检索多路合并的逻辑需要自己写但代码量不大提升却很明显。6.3 性能、成本与增量更新批量建索引时Embedding 接口调用是最大瓶颈。文档量大时一定要异步批量处理加本地缓存避免重复调用同一个文本块。索引导入跑批最好做成可重入任务失败了能断点续跑。检索性能方面PGVector 数据量超过几十万条要建 HNSW 索引否则相似度计算会明显变慢数据量再大再考虑 Milvus 这类专业向量库。增量更新是另一个容易忽略的坑。知识库文档频繁变动时不能每次全量重建。正确做法是按 docId 删除旧向量再重新加载新文本文档删除时也要有机制同步清掉对应向量。元数据里的 lastModified 就是为增量更新准备的一开始就要存。最后再分享一个个人经验第一次上线内部知识助手时我最大的教训是高估了大模型的理解能力低估了文档清洗和切分的重要性。当时觉得反正模型能读懂结果切分不合理召回全乱再厉害的 Prompt 也救不回来。后来我总结出一套很土的验证方法上线前拿二十条典型问题先不走大模型直接看向量检索的召回结果。召回准了再打开生成环节召回不准就先优化文档管道。每一步单独验证问题定位会清晰很多。另外一个小技巧给知识库做定期体检。每隔一段时间随机抽一批历史问题跑一遍检索统计命中率。命中率掉了多半是新增文档没清洗干净或者切分策略变了趁早发现比被用户投诉再改要舒服得多。RAG 项目的长期维护拼的不是模型多强而是文档管道稳不稳。