翻了几千页内部文档,就为了找一个故障排查步骤?新员工入职还在靠老人口口相传?总不能把公司机密数据直接丢给公开模型吧——数据安全这一关就过不去……
这其实就是 RAG 要解决的事:让 AI 先“翻书”再回答,答案基于你的私有知识,而不是瞎编。(圈内黑话叫“给AI装个外挂脑子”,话糙理不糙)
网上聊 RAG 的基本都是 Python,Java 开发者看在眼里急在心里
。今天咱们就用 Spring AI + DeepSeek + PGVector,用极简的步骤搭一个能跑的企业级知识库问答系统。这套组合的好处是:数据不出域、代码你熟、不用额外装复杂中间件。
RAG(检索增强生成)的核心思想很简单:让AI考试的时候可以翻书,答案有出处。
它的价值有多大?一个真实的案例是:企业内部5000+份文档,新员工入职查个报销规范平均要翻40分钟(40分钟都够摸好几次鱼了)。用RAG改造之后,3秒出结果,问答精度直接从40%提到92%。
白话总结只需三步:
RAG 需要一个能按语义检索向量的数据库。PostgreSQL + PGVector 是最省心的选择。用 Docker 一条命令搞定:
docker run -it --rm --name postgres-pgvector \ -p 5432:5432 \ -e POSTGRES_USER=postgres \ -e POSTGRES_PASSWORD=postgres \ pgvector/pgvector:pg16建表并开启扩展:
CREATE EXTENSION IF NOT EXISTS vector;CREATE EXTENSION IF NOT EXISTS "uuid-ossp";CREATE TABLE IF NOT EXISTS vector_store ( id uuid DEFAULT uuid_generate_v4() PRIMARY KEY, content text, metadata json, embedding vector(1536));CREATE INDEX ON vector_store USING HNSW (embedding vector_cosine_ops);(第一次搞这个的时候,我漏掉了
uuid-ossp扩展,表结构索引没整对,项目启动时直接给我刷了一屏幕红色的报错——当时我就盯着电脑懵了,还以为是自己JDK版本有问题。大家先在DB里顺手执行完,确认没报错再往后走。)
在 pom.xml 中引入向量库支持:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-vector-store-pgvector</artifactId></dependency><dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope></dependency>在 application.yml 中配置数据库与 DeepSeek 模型:
spring: datasource: url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres ai: dashscope: api-key: ${DASHSCOPE_API_KEY} # API Key embedding: options: model: text-embedding-ada-002 vectorstore: pgvector: index-type: HNSW distance-type: COSINE_DISTANCE dimensions: 1536 initialize-schema: false # 已建表,不重复建准备导入知识库文档(以考试大纲 exam.pdf 为例):

使用 ParagraphPdfDocumentReader 将文档切分并存入向量库:
@RestController@RequestMapping("/document/readers")public class DocumentReadersController { private final VectorStore vectorStore; public DocumentReadersController(VectorStore vectorStore) { this.vectorStore = vectorStore; } @RequestMapping("/pdf") public String readPdf() { ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader("classpath:exam.pdf", PdfDocumentReaderConfig.builder() .withPageTopMargin(0) .withPagesPerDocument(1) .build()); List<Document> documentList = pdfReader.read(); vectorStore.add(documentList); return "success"; }}(第一次跑测试的时候,我把上百页的运维手册直接怼进去,PGVector瞬间吐出了1000多条向量记录。当时我们几个后端围着屏幕看——说实话那速度确实比预想的快。写到这儿我又想起以前刚入职那会儿,为了找个部署配置文件问了三轮老员工,硬是没人说得清在哪个Wiki目录下……要早有这玩意儿我也不至于那么狼狈。)
与其硬编码使用 RAG,不如增加一层保命防翻车指南:先轻量探测知识库,有资料就用 RAG 回答,没资料就赶紧退回到普通对话模式。(这招属于典型的事后诸葛亮——我也是被测试坑了几次才想到的
)
后端控制层代码实现:
@RequestMapping(value = "/stream", produces = "text/html;charset=utf-8")public Flux<String> stream(String prompt, String chatId) { // 1. 保存聊天历史 chatHistoryTitleRepository.save(chatId, prompt); // 2. 【轻量向量探测】判断向量库里是否有匹配知识 SearchRequest probeRequest = SearchRequest.builder() .query(prompt) .similarityThreshold(0.8) // 探测门槛 .topK(1) // 只查1条,省资源 .build(); List<Document> probeResults = vectorStore.similaritySearch(probeRequest); boolean hasKnowledge = !probeResults.isEmpty(); // 3. 构建 ChatClient 请求 ChatClient.ChatClientRequestSpec requestSpec = chatClient.prompt() .user(prompt) .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId)); // 4. 智能路由:有知识挂载 RAG Advisor,无知识切回通用对话 if (hasKnowledge) { SearchRequest ragRequest = SearchRequest.builder() .query(prompt) .similarityThreshold(0.7) .topK(3) .build(); QuestionAnswerAdvisor ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore) .searchRequest(ragRequest) .build(); requestSpec.advisors(ragAdvisor); logger.info("📚 知识库命中,启用 RAG 增强。"); } else { logger.info("💬 知识库未命中,使用普通对话模式。"); } // 5. 执行流式响应 return requestSpec.stream().content();}当前端发起流式问答请求 GET/POST /stream?prompt=公共科目名称有哪些?&chatId=xxx 时,后端的完整执行流程如下:
prompt 与 chatId 保存至历史会话记录中。probeRequest(如 similarityThreshold = 0.8, topK = 1),快速去 vector_store 向量库搜索是否有匹配的私有知识,生成 hasKnowledge 标记。hasKnowledge == true):构建正式的 RAG 顾问 QuestionAnswerAdvisor(设置更宽泛的 topK = 3),将其添加到 ChatClient 请求拦截器列表中,开启 RAG 检索增强。hasKnowledge == false):不添加 RAG 顾问,请求直接透传,智能切回 普通对话模式。QuestionAnswerAdvisor 会在大模型调用前拦截请求,把从向量库检索到的最相关文档片段拼接到 Prompt 的 System 上下文中作为证据。Flux<String> 响应流的形式将结果实时吐回前端。搞个流程图说一下:
在后端接口配置完成后,针对三种场景进行了对比验证:
当用户提问exam.pdf中包含的私有知识(如“公共科目名称有哪些?”)时,系统进行探测性检索,发现向量库包含匹配文档(hasKnowledge = true),成功触发 RAG 检索增强:

实测结果:探测命中知识库,RAG Advisor 成功抓取上下文并精确回答《职业能力倾向测验》与《综合应用能力》,出处明确。
当用户提问通用技术问题(如“Java 多线程示例”)时,向量探测未命中匹配记录(hasKnowledge = false),系统自动选择不挂载 RAG 顾问,降级至通用大模型对话模式:

实测结果:探测未命中向量库,系统自动切回普通对话模式,由 DeepSeek 模型的通用能力顺畅输出 Java 多线程代码。
如果对所有问题都强制死板地应用 RAG 检索:

痛点分析:当用户询问通用问题“Java是什么”时,因向量库中仅有
exam.pdf,硬套 RAG 会向模型塞入空上下文,逼得 DeepSeek 只能委屈巴巴回复“抱歉,关于Java的信息我无法提供”——当时我看到这句直接笑喷了,这回答也太“呵呵”了吧。这就印证了“先探测再路由”的必要性。
TokenTextSplitter 按标题层级切分才算稳定。topK=3 在面对复杂问题时上下文不足,后来调成 topK=5 并加了 similarityThreshold 过滤,召回质量显著提升。Spring AI 这套东西的逻辑其实一点都不复杂:VectorStore管存和查,QuestionAnswerAdvisor管拼上下文,ChatClient管跟模型说话。搞懂这三件套,RAG 就算彻底搞定了——至于后面搞什么多路召回、重排序(Rerank),那是项目跑顺之后老板加预算的事了。反正先把这三件套整明白,至少今晚能准时下班。
源码已更新,公众号回复「SpringAI」获取完整项目。
我是麻雀,6年央国企实战派,专注 Java后端工程踩坑 + AI原生开发实战!
🎁 粉丝专属开源代码与福利:
👉 如果觉得本文对你有帮助,欢迎【点赞 + 收藏 + 转发】,有问题评论区随时交流!