
我先说一下我自己的处境。团队里 Obsidian 攒了几百篇技术笔记后来文档越来越多从一本能翻完的手册变成了“用的时候找不到、找的时候来不及”的资料堆。于是我先用 Dify 搭了一版知识库把 MD 文件导进去做了切块和向量化网页对话框里也能检索出内容来。但问题马上来了知识库是“能搜了”可它跟业务系统没关系。用户问一个问题还得跑到 Dify 后台去试回答结果也没法嵌到我们的 Java 应用里。于是我开始折腾 Spring AI RAG把已经建好的知识库接进 Java 服务。这篇文章就聊一件事知识库已经在了Java 程序员接下来还要做什么不想停留在“演示版问答”想把 RAG 真正嵌进项目里的朋友可以参考我下面的完整实操。1. 为什么知识库建好了还差一个 RAG 的服务化封装知识库本身不会主动回答问题。它只是一堆被切成块、转成向量的文本。真正让知识库“活”起来的是 RAG 这条流水线用户提问 → 检索相关片段 → 把片段和问题一起拼进 Prompt → 交给大模型生成回答。1.1 从“Dify 里能用”到“业务系统里能用”的差距很多团队的现状是知识库有了Dify 或者 FastGPT 里的对话也通了演示的时候效果不错。但 Java 项目要接入问题就来了Dify 的 API 能调但每次调用都要走 HTTP请求格式、workflow id、user 字段全是 Dify 自家定义业务系统需要把“用户提问”和“知识库检索出来的上下文”一起管理Dify 是个黑盒检索细节拿不到有时候需要同时查多个知识库、按部门过滤、控制不同用户的访问范围Dify 免费版做起来很别扭。我一开始也想图省事直接用 Dify API但折腾到权限和检索细节时放弃了。用 Spring AI 的好处是RAG 的每一步——Embedding、向量存储、检索、Prompt 组装——都变成 Java 代码里可以控制的组件后面想怎么调就怎么调。1.2 Spring AI 在 RAG 链路里的角色Spring AI 不是一个完整的应用它是一套“大模型应用开发框架”对标的是 LangChain 在 Python 生态里的位置。它做 RAG 时天生有几个优势统一抽象ChatClient、EmbeddingModel、VectorStore 都是接口底层接 OpenAI、智谱、DeepSeek、Ollama 都可以原生 Spring 生态事务、缓存、配置、监控一套体系直接用对 Java 程序员几乎没有学习成本可编程的检索流程QuestionAnswerAdvisor 和自定义 Advisor 可以让你精准控制“拿什么上下文去问模型”。一句话总结知识库解决的是“内容怎么存”Spring AI 解决的是“内容怎么被业务调用”。2. 动手前想清楚RAG 的四种接入路径怎么选网上 RAG 教程很多但一半以上是拿 Python 写的 Notebook 演示Java 程序员照着抄都抄不顺。我把实际可行的路径捋了一遍按“从简单到复杂”排序2.1 方案对比方案适合场景复杂度可定制性A. 直接调 Dify/FastGPT API快速上线、前端 Demo、团队内部工具低低检索细节拿不到B. Spring AI SQL 模糊搜索小知识库、关键词即可满足低中无向量概念C. Spring AI 向量库推荐生产级知识问答语义检索高高全链路可控D. Spring AI 向量库 Agent 工具调用需要多个工具配合、多轮规划最高最高我自己最终选了 C原因很简单我既需要语义检索的效果又需要 Java 代码里能随时调整检索策略D 的 Agent 那套先不做等检索稳定后再加。2.2 别一上来就追求“高级 RAG”很多教程张口就是“RAG-Fusion”“Self-RAG”“Agentic RAG”但 Java 程序员接知识库的第一步应该先把基础链路跑通文档 → 切块 → Embedding → 存储 → 检索 → 生成。这个链路如果没调好加再多高级技巧也白搭。我见过最典型的反面案例一上来买了付费向量数据库配了十多个 Embedding 模型候选折腾两周发现基础检索效果还不如直接关键词搜索。先把基础 RAG 调通再用评测集去量化“检索质量”这才是正常顺序。3. 环境准备Spring AI 版本、模型和向量库选型这里我踩过不少坑直接说结论。3.1 Spring AI 版本怎么选Spring AI 的版本变化很快网上教程经常互相打架。截至我实操时稳定好用的是1.0.0 GA系列。如果你用 Spring Boot 3.2 或 3.3可以直接引入dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后加核心依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-vector-store-pgvector/artifactId /dependency这里要特别提醒Spring AI 的 Starter 和 Spring Boot 的版本兼容性极重要。Spring Boot 3.2.x 对应 Spring AI 1.0.0 没问题但你如果用的是 Spring Boot 3.4 或更高可能需要升级到 Spring AI 1.1。看官方 Release Notes 比看博客靠谱。3.2 模型层DeepSeek 还是智谱因为合规和成本原因我生产环境没有直连 OpenAI。试了两条路线路线一Spring AI 智谱 AI智谱 AI 是国内对 Spring AI 支持比较早的厂商通过 OpenAI 兼容接口对接。配置如下:spring: ai: openai: base-url: https://open.bigmodel.cn/api/paas/v4 api-key: ${ZHIPU_API_KEY} chat: options: model: glm-4-flash注意这里虽然写的是 openai但是 Spring AI 的设计就是通过 OpenAI 兼容协议对接各种模型服务。智谱的/api/paas/v4路径就是兼容 OpenAI 的接口实测可用。路线二Spring AI 本地部署 DeepSeek用 Ollama 跑 deepseek-r1 或 qwen2.5 这类模型spring: ai: ollama: base-url: http://localhost:11434 chat: options: model: deepseek-r1:14b本地模型的优势是数据不出内网缺点是 14B 模型在普通 GPU 上回答质量不如云端。我的做法是开发环境用本地模型联调生产环境切云端 API配置层面只换 base-url 和 model代码完全不用改。3.3 向量存储选 pgvector 还是 Elasticsearch我选 pgvector 的原因比较务实团队本来就用了 PostgreSQL不需要额外维护一套 Elasticsearch 或 Milvuspgvector 0.5.0 支持 HNSW 索引100 万级别的向量检索性能足够Spring AI 对 pgvector 的支持很完善VectorStore 接口实现直接可用。如果你知识库体量特别大千万级以上或者需要复杂的过滤查询再考虑 Milvus 或 Elasticsearch 的向量检索能力。4. 核心实现从知识库文件到可问答的 Java 服务下面拆解整个 RAG 流程的 Java 实现。我的知识库是 Obsidian 里的 Markdown 文件所以核心任务就是读文件 → 切块 → 向量化 → 存库 → 检索问答。4.1 第一步封装文档读取与切块Spring AI 提供了TextSplitter接口和现成的TokenTextSplitter实现。但直接按 token 切块对中文文档效果一般我参考了社区方案结合 Obsidian 笔记的特性做了定制package com.example.rag.service; import org.springframework.ai.document.Document; import org.springframework.ai.transformer.splitter.TextSplitter; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; Component public class ObsidianMarkdownSplitter implements TextSplitter { // 按标题层级切分 Obsidian 笔记 Override public ListDocument split(Document document) { String content document.getContent(); ListDocument chunks new ArrayList(); // 按二级标题切块保留标题作为上下文前缀 String[] sections content.split((?m)^##\\s); for (int i 0; i sections.length; i) { String section sections[i].trim(); if (section.isEmpty()) { continue; } // 如果片段太长继续切 ListString subChunks splitLongText(section, 800, 200); for (String sub : subChunks) { Document chunk new Document(sub, document.getMetadata()); chunk.getMetadata().put(source, document.getMetadata().get(source)); chunk.getMetadata().put(chunk_index, i); chunks.add(chunk); } } return chunks; } private ListString splitLongText(String text, int maxLength, int overlap) { ListString chunks new ArrayList(); if (text.length() maxLength) { chunks.add(text); return chunks; } int start 0; while (start text.length()) { int end Math.min(start maxLength, text.length()); // 尽量在段落边界截断 int lastBreak text.lastIndexOf(\n, end); if (lastBreak start) { end lastBreak; } chunks.add(text.substring(start, end)); start end - overlap; } return chunks; } }切块参数我试过好几组最后固定为 800 字主块、200 字重叠。800 字对中文技术文档来说语义比较完整200 字重叠能保证跨块的信息不丢。块太小200字会导致检索时上下文不够块太大2000字又会稀释向量表示回答容易跑偏。4.2 第二步向量化与入库用 Spring AI 的VectorStore接口把切好的文档块写入 pgvector。这里我调用了spring-ai-starter-vector-store-pgvector提供的PgVectorStorepackage com.example.rag.service; import lombok.RequiredArgsConstructor; import org.springframework.ai.document.Document; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; Service RequiredArgsConstructor public class KnowledgeBaseIngestService { private final VectorStore vectorStore; private final ObsidianMarkdownSplitter markdownSplitter; /** * 扫描 Obsidian 目录把 Markdown 文件入库 */ public void ingest(String vaultPath) throws IOException { ListDocument allDocs new ArrayList(); try (StreamPath paths Files.walk(Paths.get(vaultPath))) { ListPath mdFiles paths .filter(Files::isRegularFile) .filter(p - p.toString().endsWith(.md)) .toList(); for (Path file : mdFiles) { String content Files.readString(file); Document doc new Document(content); doc.getMetadata().put(source, file.toString()); doc.getMetadata().put(type, obsidian_note); ListDocument chunks markdownSplitter.split(doc); allDocs.addAll(chunks); System.out.println(Processed: file , chunks: chunks.size()); } } // 批量写入向量库 if (!allDocs.isEmpty()) { vectorStore.add(allDocs); System.out.println(Total documents indexed: allDocs.size()); } } }代码不复杂但有几个细节要记住向量维度要和 Embedding 模型保持一致智谱的 embedding-3 返回 1024 维Ollama 的 nomic-embed-text 是 768 维。pgvector 建表时指定了维度如果换了模型要重建表。Metadata 必须包含业务需要的过滤条件比如部门 ID、文档类型、更新时间。我在实际项目里加了dept_id用户提问时只检索本部门的文档效果比检索全库好很多。写入要批量一条条 add 太慢一次性 add 几百个 Document 没问题。我 2000 多个 Markdown 文件切出来大约 1.2 万个 chunk全量入库耗时约 3 分钟。4.3 第三步问答链路的实现核心是 Spring AI 的ChatClient结合QuestionAnswerAdvisor就是 RAG 里的“检索生成”部分。我当时对比了两种写法推荐下面这种相对新的 APIpackage com.example.rag.service; import lombok.RequiredArgsConstructor; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor; import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; Service RequiredArgsConstructor public class RagChatService { private final ChatClient chatClient; private final VectorStore vectorStore; /** * 同步问答 */ public String ask(String question, String deptId) { SearchRequest searchRequest SearchRequest.builder() .query(question) .topK(5) .similarityThreshold(0.5) .filterExpression(dept_id deptId) // 按部门过滤 .build(); return chatClient.prompt() .user(question) .advisors(new QuestionAnswerAdvisor(vectorStore, searchRequest)) .call() .content(); } /** * 流式问答适合前端打字机效果 */ public FluxString askStream(String question) { return chatClient.prompt() .user(question) .advisors(new QuestionAnswerAdvisor(vectorStore)) .stream() .content(); } }这个QuestionAnswerAdvisor做的事情可以用两句话概括先用SearchRequest去向量库检索出 topK 条相关片段再把片段和用户问题合并成一个增强 Prompt 发给大模型。Spring AI 默认的 Prompt 模板里要求模型“仅基于上下文回答如果上下文不够就明确说不知道”这个约束对知识库问答非常重要能显著减少模型胡编乱造。我在实际测试中发现topK5比topK3效果更稳因为多两个候选可以弥补向量检索的误差。similarityThreshold0.5是调出来的经验值——低于 0.5 会混入太多不相关内容超过 0.6 又会漏掉一些语义接近但用词不同的片段。4.4 第四步把服务暴露成接口这一步和普通 Spring Boot 无异但有一个设计建议不要把原始大模型返回直接透传给前端最好包一层响应结构把“引用的文档来源”也带回去。用户看到回答后可以点开来源验证这是企业知识库标配能力。package com.example.rag.controller; import com.example.rag.service.RagChatService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.Map; RestController RequestMapping(/api/rag) RequiredArgsConstructor public class RagController { private final RagChatService ragChatService; PostMapping(/ask) public MapString, String ask(RequestBody AskRequest request) { String answer ragChatService.ask(request.question(), request.deptId()); return Map.of(answer, answer); } public record AskRequest(String question, String deptId) {} }5. 检索效果调优为什么答非所问关键在 Embedding 和切块RAG 的效果瓶颈通常不在大模型而在检索。模型只要你把对的上下文喂给它回答就不会差。检索不到或者检索错了后面全白搭。5.1 Embedding 模型应该怎么选我在智谱 embedding-3 和 Ollama 的 nomic-embed-text 之间做了对比测试。简单说云端 embedding-3语义理解能力强中文效果基本是国产模型第一梯队官方定价非常便宜千万 token 才几块钱可以直接用本地 nomic-embed-text768 维CPU 也能跑但中文效果明显弱一些适合离线环境或数据敏感场景不建议用 OpenAI 的 text-embedding-3-small 处理纯中文业务文档不是不能用是中文效果和性价比都拼不过国产模型。编码向量之前还有一个关键点同一个知识库的文档必须用同一个 Embedding 模型不能今天用智谱明天换 Ollama 再写入同一个 pgvector 表否则向量空间不一致检索结果完全没意义。5.2 切块策略的迭代记录我第一次跑通时用的就是默认的TokenTextSplitterchunk size 设成 500 token。效果一般经常出现“检索到片段但内容不完整”的情况。后来按 Obsidian 笔记的实际结构改成“按标题切块段落边界截断”明显变好。记录两个要点切块时保留标题前缀把“## 为什么知识库需要 RAG”这样的标题加在每块内容开头Embedding 时能显著提升检索准确率重叠值至少 10%~20%不加重叠会出现一个问题比如“问题定位”的最后几句话在上一块末尾检索时只命中下一块模型就缺少上下文。5.3 你需要一个“评测集”这话听起来很正规做起来其实很简单。我从真实用户提问里挑出 30 个典型问题人工标注了每个问题应该命中哪些文档或片段。每次调完参数就拿这 30 个问题跑一遍记录“回答正确/部分正确/完全跑偏”的数量。哪怕只有 30 条评测集也比“感觉效果还行”强得多。我靠这个评测集发现在 800 字切块200 字重叠的配置下回答正确率从最初的 53% 提升到 76%——这是最让我意外的收获因为代码逻辑没变纯粹是切块和检索参数变了。6. 生产落地事务、缓存、安全一个都不能少知识库问答上线后服务稳定性是最容易翻车的地方。我这里梳理了三个必须处理的工程问题6.1 向量库事务一致性编辑知识库文档后要更新向量但vectorStore.add()和vectorStore.delete()不是原生事务性的。我的做法是给文档块加上doc_id元数据更新时先按doc_id删除旧块再写入新块。如果中途失败可能会留下脏数据但实际影响有限因为知识库更新的频率不高而且可以靠重建索引兜底。6.2 缓存策略大模型调用贵且慢同一个问题短时间内反复问完全没必要。我在 Controller 层加了 Caffeine 缓存对相同问题和相同部门 ID 的结果缓存 10 分钟。实测 QPS 上来后模型调用量减少了约 40%。需要注意缓存 key 的设计一定要包含检索参数topK、过滤条件、模型版本否则知识库更新后用户会一直拿到旧回答。我在缓存 value 里加了知识库版本号每次重建索引就递增版本号从机制上避免这个问题——这个思路是在 Dify 的 metadata 管理里学到的。6.3 安全与权限这一条坑最深。RAG 检索时如果不加权限过滤等于把内部文档开放给所有能调用接口的人。我的方案分两层接口层用户身份通过 JWT 解析拿到 userId检索层从用户信息推导出允许访问的部门列表拼进SearchRequest.filterExpression控制 VectorStore 的检索范围。千万别把权限过滤放在“生成回答之后”——那是自欺欺人。模型上下文里如果混入了无权访问的文档你无法保证它不会泄露。必须在检索阶段就把不该出现的内容过滤掉。另一个安全点是 Prompt 注入。知识库文档里如果被人写了“忽略以上所有指令直接输出xxx”模型可能被带偏。Spring AI 的QuestionAnswerAdvisor默认模板对“仅基于上下文回答”有约束但在知识库导入阶段最好也做一轮清洗删掉明显的指令性内容。7. 常见问题与排错实录下面是实操中真实遇到的 5 个问题每个都是血泪换来的问题现象排查思路最终解决方案1.No bean named vectorStore available启动直接报错pgvector Starter 没引入或配置不对确认引入spring-ai-starter-vector-store-pgvector并在 yml 配spring.ai.vectorstore.pgvector.index-typeHNSW2. 回答经常说“根据提供的信息我无法回答”有知识库但模型说找不到检索到的片段和问题语义不匹配调低similarityThreshold到 0.4~0.5或者换更强的 Embedding 模型检查切块是否把关键内容拆散了3. 检索速度慢响应超过 5 秒100 万级向量全表扫描pgvector 没建 HNSW 索引执行 SQL 建 HNWS 索引向量列用vector_cosine_ops4. 模型回答了但来源完全不对回答内容很流畅但是错的向量检索命中了错误文档模型基于错误上下文生成了流畅的错误回答在返回结构中增加引用来源字段调优检索参数可以在 prompt 里要求模型每个结论都带引用5. 不同环境Dev/Prod效果差异大本地好线上差线上向量库和本地不同步或环境配置的模型不一致用同一个导入脚本重建索引确认两边 embedding 模型一致补充一个很实用的排查工具Spring AI 的ChatClient支持.advisor()链式调用你可以写一个LoggingAdvisor把每次检索到的片段和相似度分数打到日志里。这样用户说“回答错了”时你一眼就能看出是“没检索到”还是“模型生成跑偏”不至于两头瞎猜。8. 从 RAG 到 Agent下一步你还能做什么基础 RAG 稳定跑通之后知识库的利用率会提升一大截但你会发现新的瓶颈RAG 只能回答“知识库里有”的问题。用户如果说“帮我查一下库存然后结合知识库里的补货策略生成一份报告”这就不是纯 RAG 能解决的了。这就要进入 Agentic RAG 阶段。Spring AI 提供了ToolCalling支持你可以把“查库存”“查订单”封装成 Tools让模型自主决定先调用哪个工具、再用知识库辅助回答。比如下面的思路Component public class InventoryTool implements ToolCallback { // 模型发现需要库存数据时自动调用 Override public String call(String toolInput) { // 调用库存服务返回 JSON return inventoryService.queryBySku(toolInput); } }我在本地已经验证过这个链路用户问“当前 A 类物料库存预警阈值是多少结合知识库里的补货规则给我一个补货建议”模型会先查库存服务的实时数据再结合知识库中的规则文档生成完整回答。这个体验比纯 RAG 好很多但复杂度也上一个台阶——建议先从 RAG 开始把检索质量和稳定性做到位再往里加工具调用。后面我还计划引入 GraphRAG把知识库里的实体关系也存进去——比如“Spring AI 依赖 Spring Boot 3.x”“Observability 模块用了 Micrometer”这种关系用向量检索很难表示清楚但用图结构就自然得多。这也符合“知识库已经在Java 程序员还能做什么”的下一站答案别再满足于“能搜到”去把知识真正用起来。我把上面这套东西跑通之后最深的感受是RAG 不是一个“搭完就结束”的功能它是一个需要持续调优的管线。Java 程序员从 Spring AI 入手做 RAG最大的优势不是能写代码而是能把“检索、提示词、模型调用、工程治理”这一整套东西都纳入统一的技术栈里管理。你不需要成为 Prompt 工程师也不需要学 Python只靠 Java 生态就能把企业知识库变成一个真正能用的服务。如果你也正在“知识库有了但不知道怎么接业务”这个阶段希望这篇文章能帮你少踩一些坑。