Spring AI 接入 RAG:DeepSeek + Ollama + SimpleVectorStore 文档问答 Demo

📅 2026/6/19 3:21:04
Spring AI 接入 RAG:DeepSeek + Ollama + SimpleVectorStore 文档问答 Demo
Spring AI 接入 RAGDeepSeek Ollama SimpleVectorStore 文档问答 Demo做公司知识库助手时经常会遇到一个尴尬场景。用户问试用期可以请几天年假模型回答一般公司试用期员工也可以享受年假具体天数根据入职时间计算。听起来像那么回事但你们公司制度明明写着试用期员工不享受年假只能申请事假或病假。模型为什么答错不是它不会推理而是它根本没看到你的内部文档。这时很多人第一反应是要不要微调微调当然是一条路但如果只是让模型回答公司制度、接口文档、操作手册这类内容更现实的做法通常是先上 RAG。RAG 的思路很朴素调模型之前先把相关资料查出来再让模型基于资料回答。这篇文章不讲大而全的 RAG 架构我们只做一件事用 Spring AI 跑通一个最小可用的文档问答 Demo。一、RAG 到底解决什么问题RAG 全称是 Retrieval-Augmented Generation也就是“检索增强生成”。它不是训练也不是让模型拥有长期记忆。它做的是两步准备阶段 文档 - 切分 - 向量化 - 存入向量库 查询阶段 用户问题 - 检索相似文档 - 补进 prompt - 模型回答所以 RAG 不是让模型“永久记住”你的文档。模型参数没变能力也没变。变的是这次请求里多了一批可参考的资料。这也是它和微调、Memory 的区别微调改模型参数让模型更适应某类任务或表达方式 Memory带上历史对话让模型接上前文 RAG检索外部文档让模型基于资料回答RAG 最适合解决这类问题模型不知道你的私有知识文档经常变不适合频繁训练回答需要有依据不能只靠模型猜。二、Spring AI 里要认识的几个组件接 RAG 之前先把几个核心组件捋清楚。不用背概念知道它们各自干什么就够了。1. DocumentDocument是 Spring AI 里的文档对象。它不是本地的.txt或.pdf文件而是承载文本和元数据的 Java 对象DocumentdocnewDocument(这是文档内容,Map.of(source,leave-policy.txt));后面的读取、切分、向量化、检索基本都围绕Document展开。2. DocumentReaderDocumentReader负责把外部文件读成Document。常见 Reader 有TextReader读纯文本JsonReader读 JSONPagePdfDocumentReader读 PDFTikaDocumentReader借助 Apache Tika 读取多种格式。读一个文本文件大概是这样ResourceresourcenewClassPathResource(docs/leave-policy.txt);TextReaderreadernewTextReader(resource);ListDocumentdocumentsreader.get();TextReader实现的是get()DocumentReader也提供了read()默认方法。这里用get()。3. TokenTextSplitterTokenTextSplitter用来把长文档切成小片段。为什么要切因为检索时通常不是拿整份文件去匹配而是拿一个个 chunk 去匹配。如果一整份制度文档都塞进去用户问“试用期年假”检索很难精确命中那一小段。TokenTextSplittersplitternewTokenTextSplitter();ListDocumentchunkssplitter.split(documents);Spring AI 1.1.7 里TokenTextSplitter默认目标 chunk size 是 800 token。先用默认值跑通后面再根据文档类型调整。4. VectorStoreVectorStore是向量库抽象。它负责两件事把文档向量化后存起来根据问题检索相似文档。Demo 阶段可以用SimpleVectorStore它是内存实现适合本地验证。生产环境再换成 PgVector、Milvus、Redis、Elasticsearch、Chroma 等持久化向量库。vectorStore.add(chunks);ListDocumentresultsvectorStore.similaritySearch(SearchRequest.builder().query(试用期年假).topK(5).build());5. QuestionAnswerAdvisorQuestionAnswerAdvisor是 Spring AI 提供的基础 RAG Advisor。它会在调用模型前根据用户问题去VectorStore检索相关文档再把这些文档补进 prompt。QuestionAnswerAdvisoradvisorQuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(3).build()).build();ChatClientchatClientChatClient.builder(chatModel).defaultAdvisors(advisor).build();配置好以后业务代码还是正常问StringanswerchatClient.prompt().user(试用期可以请几天年假).call().content();表面看只是问了一个问题背后 Advisor 已经帮你完成了检索和上下文注入。三、最小 Demo接入一份请假制度目标很简单把一份公司请假制度文档接进来让模型能基于这份文档回答问题。假设你已经有一个能正常调用ChatClient的 Spring Boot 项目。1. 准备依赖和配置先说结论。这个 Demo 需要三个东西ChatModel - 负责最终回答 EmbeddingModel - 负责把问题和文档转成向量 VectorStore - 负责存储和检索向量所以只有 DeepSeek 聊天模型还不够还要有一个 embedding 模型。这篇用下面这个组合DeepSeek - ChatModel Ollama - EmbeddingModel SimpleVectorStore - VectorStore先加依赖。这里默认项目已经引入 Spring AI BOM所以不写版本号。dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-deepseek/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-ollama/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-vector-store/artifactId/dependencydependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-advisors-vector-store/artifactId/dependency再配application.ymlspring:ai:model:chat:deepseekembedding:ollamadeepseek:api-key:${DEEPSEEK_API_KEY}chat:options:model:deepseek-v4-flashollama:base-url:http://localhost:11434embedding:options:model:bge-m3本地还要准备 Ollama。如果你已经装过 Ollama可以跳过安装步骤。如果没装先去 Ollama 官网下载桌面版macOS 或 Linux 也可以用官方脚本curl-fsSLhttps://ollama.com/install.sh|sh启动 Ollama 后默认服务地址是http://localhost:11434然后拉取 embedding 模型ollama pull bge-m3最后准备 DeepSeek 的 API KeyexportDEEPSEEK_API_KEY你的DeepSeek API Key这几个配置要对应上spring.ai.model.chatdeepseek - 用 DeepSeek 提供 ChatModel spring.ai.model.embeddingollama - 用 Ollama 提供 EmbeddingModel spring.ai.deepseek.chat.options.modeldeepseek-v4-flash - 用 DeepSeek 的 v4 flash 模型回答 spring.ai.ollama.embedding.options.modelbge-m3 - 用 bge-m3 做向量化这里用bge-m3主要是因为它对中文和多语言文档更友好拿来做公司知识库 Demo 更贴近真实场景。spring.ai.ollama.base-url默认就是http://localhost:11434这里显式写出来是为了让读者知道 Spring AI 连接的是本机 Ollama 服务。DeepSeek 官方当前推荐使用deepseek-v4-flash或deepseek-v4-pro。旧的deepseek-chat、deepseek-reasoner仍然兼容但官方已经标注后续会废弃。另外V4 模型默认是 thinking mode。官方文档说明thinking mode 下temperature、top_p这类采样参数不会生效。所以这个最小 Demo 里先不配这些参数避免看起来配了、实际没起作用。如果你不想用 Ollama也可以换成 OpenAI、Azure OpenAI、智谱等支持 embedding 的模型。原则只有一个RAG 必须同时有 ChatModel 和 EmbeddingModel。如果生产环境用 PgVector再把内存版SimpleVectorStore换成 PgVector starterdependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-vector-store-pgvector/artifactId/dependency这篇先用SimpleVectorStore不用额外启动数据库重点是把 RAG 链路跑通。2. 准备测试文档在src/main/resources/docs/下创建leave-policy.txt公司请假制度 1. 试用期员工不享受年假只能申请事假或病假。 2. 正式员工入职满一年后每年享有 5 天年假。 3. 病假需要提交医院开具的病假证明。 4. 请假超过 3 天需要直属主管和部门负责人双审批。 5. 年假需要提前 5 个工作日提交申请。这就是我们的私有知识。但准确说不是“教给模型”而是后面提问时让模型能检索到它。3. 读取、切分、写入向量库创建一个配置类ConfigurationpublicclassRagConfig{BeanpublicVectorStorevectorStore(EmbeddingModelembeddingModel){returnSimpleVectorStore.builder(embeddingModel).build();}BeanpublicApplicationRunnerloadDocumentsToVectorStore(VectorStorevectorStore){returnargs-{ListDocumentchunksloadAndSplitDocuments();vectorStore.add(chunks);System.out.println(已加载 chunks.size() 个文档片段到向量库);};}privateListDocumentloadAndSplitDocuments(){ResourceresourcenewClassPathResource(docs/leave-policy.txt);TextReaderreadernewTextReader(resource);ListDocumentdocumentsreader.get();TokenTextSplittersplitternewTokenTextSplitter();returnsplitter.split(documents);}}这段代码做了三件事创建SimpleVectorStore应用启动时读取并切分文档把切分后的 chunk 写入向量库。写入时VectorStore会通过EmbeddingModel把文本转成向量。SimpleVectorStore主要用于测试和 Demo。它有保存/加载能力但默认就是内存向量库生产环境不要把它当成正式方案。4. 配置 QuestionAnswerAdvisor再把 RAG 能力接到ChatClientConfigurationpublicclassChatClientConfig{BeanpublicChatClientchatClient(ChatModelchatModel,VectorStorevectorStore){QuestionAnswerAdvisoradvisorQuestionAnswerAdvisor.builder(vectorStore).searchRequest(SearchRequest.builder().topK(3).build()).build();returnChatClient.builder(chatModel).defaultAdvisors(advisor).build();}}topK(3)表示每次最多取 3 个相似文档。真实项目里不要一开始就调很大先从 3 或 5 开始观察效果。5. 写个接口测试RestControllerpublicclassKnowledgeController{privatefinalChatClientchatClient;publicKnowledgeController(ChatClientchatClient){this.chatClientchatClient;}GetMapping(/ask)publicStringask(RequestParamStringquestion){returnchatClient.prompt().user(question).call().content();}}启动应用后访问http://localhost:8080/ask?question试用期可以请几天年假理想情况下模型会回答试用期员工不享受年假只能申请事假或病假。再问http://localhost:8080/ask?question请假超过3天需要谁审批它应该能答出请假超过 3 天需要直属主管和部门负责人双审批。这两个答案都来自leave-policy.txt。如果去掉QuestionAnswerAdvisor模型大概率会回到通用回答而不是你公司的具体规定。四、一次 RAG 调用背后发生了什么业务代码看起来只有一行chatClient.prompt().user(试用期可以请几天年假).call().content();但真正执行时多了几步。1. Advisor 拿到用户问题QuestionAnswerAdvisor会先拿到当前问题试用期可以请几天年假2. 去 VectorStore 检索它会用这个问题构造SearchRequest然后去向量库找相似文档。vectorStore.similaritySearch(SearchRequest.builder().query(试用期可以请几天年假).topK(3).build());对SimpleVectorStore来说它会调用EmbeddingModel把问题转成向量再和库里的文档向量做相似度计算。可能返回chunk 1试用期员工不享受年假规则 chunk 2正式员工年假规则 chunk 3年假申请流程3. 把文档补进 promptAdvisor 会把检索到的文档内容拼成上下文再增强这次用户问题。大概意思是用户问题 试用期可以请几天年假 参考资料 试用期员工不享受年假只能申请事假或病假。 正式员工入职满一年后每年享有 5 天年假。 年假需要提前 5 个工作日提交申请。 请基于参考资料回答。真实默认模板可以自定义。Spring AI 1.1.7 里如果你要自定义QuestionAnswerAdvisor的 prompt template需要保留query和question_answer_context两个占位符。4. 模型基于资料回答模型看到资料后就能回答试用期员工不享受年假只能申请事假或病假。它不是“学会”了制度。只是这次请求里它看到了制度。五、生产环境先别急着堆功能Demo 跑通后很容易马上想加 rerank、query rewrite、hybrid search。可以但别一上来就堆。RAG 的效果先看基础链路稳不稳。1. topK 不要开太大topK不是越大越好。检索太多上下文会变长噪音也会变多。模型看到一堆弱相关 chunk反而容易答偏。建议先从topK3或topK5开始。2. chunk size 要跟文档类型走TokenTextSplitter默认目标 chunk size 是 800 token但这不是最佳答案。FAQ、短问答可以更小。流程说明、接口文档可以稍大一点避免把完整语义切断。核心原则是一个 chunk 最好表达一个相对完整的意思。3. SimpleVectorStore 不适合生产SimpleVectorStore适合 Demo、测试、样例。生产环境建议换成持久化向量库比如 PgVector、Milvus、Redis、Elasticsearch 或 Chroma。怎么选先选团队最熟、最容易运维的。不要为了“看起来高级”引入一套没人会维护的基础设施。4. 一定要看检索日志RAG 优化不能只靠感觉。至少要记录用户问了什么检索到了哪些 chunk相似度大概是多少chunk 是按什么顺序塞进 prompt 的最终回答有没有用上关键资料。如果回答错了你要先判断是没检索到还是检索到了但排序靠后还是文档本身写得不清楚没有日志这些都只能猜。5. 文档质量比算法更重要不是所有公司文档都适合直接丢进 RAG。优先接这些制度文档FAQ接口文档操作手册故障处理手册。这些内容结构清楚、信息密度高检索效果通常更好。会议纪要、聊天记录、口语化总结这类内容先别急着放进去。RAG 不能把烂文档变成好答案。写在最后Spring AI 接 RAG基础链路可以记成这条线DocumentReader - TokenTextSplitter - VectorStore - QuestionAnswerAdvisor - ChatClient跑通 Demo 不难。真正难的是后面的优化文档要写清楚chunk 要切得合理topK 和相似度阈值要调检索日志要能看生产环境要换持久化向量库。我的建议是先跑通最小 Demo - 接入一批真实文档 - 看检索日志 - 调 chunk 和 topK - 再考虑 rerank、query rewrite、hybrid searchRAG 不是堆功能。它的核心就一句话找到正确资料并让模型基于资料回答。先把这条链路打稳再谈复杂优化。后续会继续更新 Spring AI、RAG、Memory、Tool Calling、MCP 等实战内容。