Java 开发者零成本构建 RAG 知识库:Spring AI Alibaba + Ollama 搭建本地 RAG 知识库
大模型再聪明也不了解你的公司内部文档。RAG 就是让通用模型学会你的知识的最佳方案。为什么需要 RAG假设你是一家公司的技术负责人团队积累了大量内部文档——产品手册、运维手册、技术规范、FAQ。现在你想做一个智能问答系统让员工可以用自然语言提问。直接把这些文档丢给大模型不现实。原因有三上下文窗口有限即使是百万 Token 的模型也无法一次性塞入整个公司的知识库成本高昂每次对话都发送大量文档Token 费用难以承受知识更新困难文档变更时需要重新训练或重新输入RAG检索增强生成Retrieval-Augmented Generation完美解决了这个问题。它的核心思路很简单用户提问 → 从知识库中检索相关片段 → 把片段 问题一起交给大模型 → 大模型基于上下文回答整个过程不需要训练模型只需要在每次对话时临时补充相关知识。一、技术架构我们要搭建的系统由四个核心组件组成┌─────────────────────────────────────────────────────────┐ │ 用户请求 │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────┐ │ │ │ Spring AI Alibaba 应用 (Java) │ │ │ │ │ │ │ │ ① 接收问题 │ │ │ │ ② 将问题转为向量 (Embedding) │ │ │ │ ③ 在向量库中检索最相关的知识片段 │ │ │ │ ④ 组装 Prompt (知识片段 用户问题) │ │ │ │ ⑤ 调用大模型生成回答 │ │ │ │ ⑥ 流式返回结果 │ │ │ └───────┬───────────────────────┬───────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌───────────────┐ ┌───────────────────┐ │ │ │ Ollama │ │ 向量数据库 │ │ │ │ (本地模型) │ │ (Chroma/Milvus) │ │ │ │ │ │ │ │ │ │ · Embedding │ │ 存储文档向量 │ │ │ │ · Chat │ │ 支持语义检索 │ │ │ └───────────────┘ └───────────────────┘ │ └─────────────────────────────────────────────────────────┘组件选型组件选择理由应用框架Spring AI AlibabaJava 生态原生API 统一企业级能力丰富模型运行时Ollama一条命令启动模型封装所有复杂性Chat 模型deepseek-r1:8b中文能力强8B 版本在消费级硬件可运行Embedding 模型nomic-embed-text轻量、效果好专门用于文本向量化向量数据库Chroma轻量级零配置适合快速原型验证二、环境搭建2.1 启动 Ollama如果你还没有安装 Ollama可以参考之前的本地部署教程。安装完成后拉取我们需要的两个模型# 对话模型负责生成最终回答ollama pull deepseek-r1:8b# 向量模型负责将文本转为向量ollama pull nomic-embed-text验证模型是否就绪ollama list你应该能看到两个模型都出现在列表中。2.2 启动向量数据库这里选择 Chroma因为它足够轻量——甚至不需要单独部署服务可以通过嵌入式模式直接在 Java 进程中运行。但为了演示更通用的场景我们用 Docker 启动一个独立服务dockerrun-d\--namechroma\-p8000:8000\-vchroma-data:/chroma/chroma\chromadb/chroma:latest启动后访问http://localhost:8000/api/v1/heartbeat如果返回心跳信息说明服务正常。向量数据库的其他选择如果你的场景需要生产级部署可以考虑 Milvus支持分布式、亿级向量、Elasticsearch向量 全文混合检索、或 PostgreSQL pgvector已有 PG 基础设施的团队。Spring AI 对这些都有官方集成。三、项目实现3.1 创建 Spring Boot 项目使用 Spring Initializr 创建一个 Spring Boot 3.x 项目添加 Web 依赖后引入以下依赖dependencies!-- Spring AI Ollama 集成 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-ollama-spring-boot-starter/artifactIdversion1.0.0/version/dependency!-- Chroma 向量存储 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-chroma-store-spring-boot-starter/artifactIdversion1.0.0/version/dependency!-- PDF 文档读取 --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-pdf-document-reader/artifactIdversion1.0.0/version/dependency/dependencies3.2 配置文件spring:ai:ollama:base-url:http://localhost:11434chat:model:deepseek-r1:8boptions:temperature:0.7num-predict:4096embedding:model:nomic-embed-textvectorstore:chroma:client:host:http://localhostport:8000collection-name:knowledge-baseinitialize-schema:true配置解读chat.model指定用于对话的模型embedding.model指定用于文本向量化的模型vectorstore.chroma向量数据库连接信息initialize-schema: true表示自动创建集合3.3 知识库初始化——将文档变为向量RAG 的第一步是把你的文档喂给系统。这个过程分为四个阶段原始文档 → 文本提取 → 文本分块 → 向量化 → 存入向量库在 Spring AI 中这一切可以用很简洁的代码完成importorg.springframework.ai.document.Document;importorg.springframework.ai.reader.pdf.PagePdfDocumentReader;importorg.springframework.ai.transformer.splitter.TokenTextSplitter;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.boot.ApplicationArguments;importorg.springframework.boot.ApplicationRunner;importorg.springframework.core.io.FileSystemResource;importorg.springframework.stereotype.Component;importjava.util.List;ComponentpublicclassKnowledgeBaseInitializerimplementsApplicationRunner{privatefinalVectorStorevectorStore;publicKnowledgeBaseInitializer(VectorStorevectorStore){this.vectorStorevectorStore;}Overridepublicvoidrun(ApplicationArgumentsargs)throwsException{// 1. 读取 PDF 文档PagePdfDocumentReaderreadernewPagePdfDocumentReader(newFileSystemResource(knowledge/product-manual.pdf));ListDocumentdocumentsreader.get();System.out.println(读取到 documents.size() 页文档);// 2. 文本分块按 Token 切分避免单个块过大TokenTextSplittersplitternewTokenTextSplitter();ListDocumentchunkssplitter.apply(documents);System.out.println(切分为 chunks.size() 个文本块);// 3. 向量化并存入向量库Spring AI 自动完成 Embedding 存储vectorStore.add(chunks);System.out.println(知识库初始化完成);}}关键点说明PagePdfDocumentReader按页读取 PDF每页作为一个 DocumentTokenTextSplitter按 Token 数量切分文本默认每块约 800 Token重叠 20%避免关键信息被截断vectorStore.add()这一步会自动调用 Embedding 模型将文本转为向量然后存入 Chroma关于分块策略分块大小直接影响检索效果。块太小→上下文不足块太大→引入噪音。一般建议 500-1000 Token重叠 10-20%。对于代码文档建议按函数/类边界切分对于问答文档可以按问答对切分。3.4 RAG 核心服务知识库就绪后接下来实现问答逻辑importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;importorg.springframework.ai.vectorstore.SearchRequest;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.core.io.Resource;importorg.springframework.stereotype.Service;importreactor.core.publisher.Flux;ServicepublicclassRagChatService{privatefinalChatClientchatClient;privatefinalVectorStorevectorStore;publicRagChatService(ChatClient.BuilderchatClientBuilder,VectorStorevectorStore){this.vectorStorevectorStore;this.chatClientchatClientBuilder.build();}/** * RAG 问答流式输出 */publicFluxStringchat(Stringquestion){// 构建检索请求取最相关的 3 个文本块SearchRequestsearchRequestSearchRequest.builder().topK(3).build();returnchatClient.prompt().advisors(newQuestionAnswerAdvisor(vectorStore,searchRequest)).user(question).stream().content();}}这里的核心是QuestionAnswerAdvisor。它的作用是在每次对话时自动执行以下操作将用户问题转为向量在向量库中检索最相关的文本块将检索到的文本块注入到 Prompt 中调用大模型生成回答你不需要手动拼接 Prompt——Spring AI 已经帮你处理好了。3.5 自定义系统提示词默认的 Prompt 模板可能不够贴合你的业务场景。可以通过自定义系统提示词来约束模型的行为在src/main/resources/prompts/system-rag.st中创建提示词模板你是一个专业的技术文档助手。请严格基于以下参考资料回答问题。 参考资料 {question_answer_context} 要求 1. 仅使用参考资料中的信息作答 2. 如果参考资料中没有相关信息请明确告知 3. 回答要准确、简洁、有条理 4. 使用中文回答然后在代码中加载它importorg.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;importorg.springframework.core.io.Resource;importorg.springframework.beans.factory.annotation.Value;ServicepublicclassRagChatService{Value(classpath:/prompts/system-rag.st)privateResourcepromptTemplate;// ...publicFluxStringchat(Stringquestion){SearchRequestsearchRequestSearchRequest.builder().topK(3).build();// 加载自定义提示词模板StringtemplatenewString(promptTemplate.getInputStream().readAllBytes());returnchatClient.prompt().advisors(newQuestionAnswerAdvisor(vectorStore,searchRequest,template)).user(question).stream().content();}}3.6 对外暴露 REST 接口最后提供一个 HTTP 接口供前端调用importorg.springframework.http.MediaType;importorg.springframework.web.bind.annotation.*;importreactor.core.publisher.Flux;RestControllerRequestMapping(/api/rag)publicclassRagChatController{privatefinalRagChatServiceragChatService;publicRagChatController(RagChatServiceragChatService){this.ragChatServiceragChatService;}GetMapping(value/chat,producesMediaType.TEXT_EVENT_STREAM_VALUE)publicFluxStringchat(RequestParamStringquestion){if(questionnull||question.isBlank()){returnFlux.just(请输入问题);}returnragChatService.chat(question);}}使用 Server-Sent EventsSSE格式TEXT_EVENT_STREAM_VALUE前端可以实时接收流式响应。四、测试验证启动应用后用 curl 测试curl-Nhttp://localhost:8080/api/rag?question产品A的保修期是多久你会看到回答像打字机一样逐字输出而不是等待全部内容生成后才返回。对比效果场景直接问模型RAG 增强后问通用知识✅ 回答准确✅ 回答准确问公司内部政策❌ 不知道/编造✅ 基于文档回答问产品技术细节❌ 泛泛而谈✅ 引用具体参数五、生产级优化上面的实现已经能跑通完整的 RAG 流程。但如果要用于生产环境还需要考虑以下优化方向5.1 混合检索向量 全文纯向量检索擅长语义匹配但对精确关键词如产品型号、错误码可能不够准确。可以结合全文检索SearchRequestsearchRequestSearchRequest.builder().topK(5).similarityThreshold(0.6)// 设置相似度阈值过滤低质量匹配.build();如果使用的向量数据库支持如 Elasticsearch、Milvus可以配置混合检索策略。5.2 重排序Rerank检索到的文本块按相关性排序后可以再用一个 Rerank 模型进行精排确保最相关的内容被优先使用ChatClientruntimeChatClientChatClient.builder(chatModel).defaultAdvisors(newRetrievalRerankAdvisor(vectorStore,rerankModel,// 重排序模型searchRequest,promptTemplate,0.2// 最低分数阈值)).build();5.3 云端模型混合使用本地模型推理速度受硬件限制。可以采取本地 Embedding 云端 Chat的混合策略spring:ai:# 本地 Embedding免费、快速ollama:embedding:model:nomic-embed-textenabled:truechat:enabled:false# 关闭本地 Chat# 云端 Chat 模型高质量回答dashscope:api-key:${DASHSCOPE_API_KEY}chat:model:qwen-plus向量检索是计算密集型操作本地跑 Embedding 模型完全够用而最终的回答生成交给云端更强的模型兼顾成本和效果。5.4 元数据过滤为文档块添加元数据可以实现更精细的检索控制DocumentdocnewDocument(content,Map.of(source,product-manual,version,2.0,department,engineering,language,zh));检索时按元数据过滤SearchRequest.builder().topK(3).filterExpression(source product-manual AND department engineering).build();这在多部门、多产品的场景中非常实用——可以确保检索结果只来自相关的文档范围。5.5 增量更新生产环境中知识库会持续更新。不需要每次都全量重建可以按需增删// 添加新文档vectorStore.add(newDocuments);// 按 ID 删除旧文档vectorStore.delete(List.of(doc-id-1,doc-id-2));六、常见问题排查Q检索结果不相关怎么办A排查顺序确认 Embedding 模型已正确加载ollama list检查检查分块大小——过大的块会引入噪音过小的块会丢失上下文尝试降低similarityThreshold阈值考虑换用更强的 Embedding 模型如bge-m3Q回答速度慢A8B 模型在 CPU 上推理确实较慢。优化方向换用更小的模型1.5B/7B确保 Ollama 使用了 GPU 加速减少num-predict上限或切换到云端模型Q模型回答中带有think思考过程ADeepSeek-R1 是推理模型会输出思考过程。处理方式代码层面用正则过滤think.*?/think之间的内容或换用非推理版本的模型如qwen2.5Q向量库连接失败A确认 Chroma 服务已启动且端口正确。如果使用了initialize-schema: trueSpring AI 会自动创建 Collection。首次启动时查看日志确认连接状态。七、总结搭建一个 RAG 应用本质上就是三件事把文档变成向量——读取、分块、Embedding、存储把问题变成答案——检索相关片段组装 Prompt调用模型把答案交给用户——流式输出实时响应Spring AI Alibaba 的价值在于它把这套流程封装成了声明式的 API。你不需要手动处理向量计算、Prompt 拼接、HTTP 调用这些底层细节只需要关注业务逻辑本身。对于 Java 团队来说这意味着学习成本低Spring Boot 开发者可以零门槛上手技术栈统一不需要引入 Python 生态数据不出本机Ollama 本地向量库完全离线运行可扩展性强同一套代码可以无缝切换到云端模型或其他向量数据库 福利时间如果你正在备战面试或者想要学习其他知识给大家推荐一个宝藏知识库作者整理了一些列 Java 程序员需要掌握的核心知识有需要的自取不谢。知识库地址https://farerboy.com/