2026/10/9 14:18:10

Spring AI + Java + PostgreSQL实现企业知识库RAG检索增强生成

Spring AI + Java + PostgreSQL实现企业知识库RAG检索增强生成 最近好几个朋友都在问我公司内部攒了一大堆运维文档、规章制度、产品手册大模型再聪明也不知道这些非公开的内容直接问它等于白问。怎么让大模型学会“查自己家的资料”答案就是 RAGRetrieval-Augmented Generation检索增强生成。这篇文章我打算用Spring AI Java PostgreSQL配合 pgvector 向量插件把整套 RAG 链路完整做一遍从环境准备、文档入库、相似度检索到最终的对话生成全部代码可跑。适合零基础接触 Java AI 的开发者也适合想快速验证向量数据库落地效果的工程团队。我会尽量把每一步背后的原因讲清楚而不是只丢一份能跑的代码。因为如果你不理解“为什么叫向量检索”“为什么要切分文档”“为什么选 PostgreSQL 当向量库”换个业务场景照样不会调。1. 先搞懂 RAG 到底在解决什么问题1.1 大模型的知识边界在哪里大模型的知识来源于训练语料存在两个死穴一是训练数据有截止时间二是只能学到公开语料。企业内部文档、私有知识库、实时业务数据大模型一概不知。有很多人一开始想用微调解决这个问题把文档喂给模型继续训练。但微调有几个现实问题成本高、周期长、知识更新就要重新训练而且微调本质上是在“改模型的习惯”不是在“往模型里塞知识”。你用一两百篇文档去微调效果往往很差。RAG 的思路完全不同它不让模型记住新知识而是让模型在回答前先去查资料。这就好比把“闭卷考试”改成“开卷考试”模型随身带一本参考书遇到不会的问题就翻书翻到相关内容再作答。这样一来知识更新只需要改参考书模型本身不用动。1.2 RAG 的执行链路拆解一个标准的 RAG 流程分五步准备知识库把原始文档PDF、Word、Markdown、TXT读出来。文档切分把长文档切成固定大小的 chunk比如每 5001000 个字符一段。向量化用嵌入模型Embedding Model把每段文本转换成一组浮点数也就是向量。存入向量数据库正版 PostgreSQL 加上 pgvector 插件就能干这事。检索增强生成用户提问时先把问题也向量化去向量库中找出最相似的 top K 段内容拼进 Prompt再交给大模型生成答案。核心要点在第 5 步问题向量化之后向量数据库会在里面做相似度计算把语义最接近的文本段捞出来。这种相似度是“语义相似”不是关键词匹配。比如用户问“服务器宕机怎么办”知识库里可能没有“宕机”这个词但有“服务不可用时的处理流程”这也能被检索到因为它们在向量空间里的距离很近。1.3 RAG、微调、长上下文怎么选很多人会把 RAG 和“把整本文档塞进 Prompt 的长上下文方案”放在一起比。长上下文方案确实随着模型能力提升越来越常用但有两个硬伤一是 token 成本高几十万字每次都要全部交给模型处理二是无关信息太多时模型照样抓不住重点。我习惯用这张选择表来判断该用哪种方案场景推荐方案原因私有知识问答、企业文档、实时数据检索RAG知识好更新、成本低、可跟踪来源让模型改变回答风格、擅长特定专业术语微调是在改变模型行为模式而不是塞知识少量文档、一次性问题、预算充足长上下文简单直接不用建设知识库知识频繁更新每周甚至每天变化RAG更新文档即可不用重新训练下面整篇文章围绕 RAG 展开这正是标题里“检索增强生成”的落脚点。2. 技术选型与方案设计思路2.1 为什么选 Spring AI而不是 LangChain4j 或自己写在 Java 生态里做 AI 应用主流选择就是 Spring AI 和 LangChain4j。我选 Spring AI 的理由很朴素如果你们团队原本就是 Spring Boot 技术栈Spring AI 的学习成本最低。它由 Spring 官方推动抽象风格跟 Spring 高度一致——配置类、Bean 注入、自动配置、Starter 依赖Java 工程师几乎没有额外认知负担。LangChain4j 也很优秀在 Java 社区起步早很多设计参考了 Python 版 LangChain。但对零基础的人来说有个问题它的生态相对独立跟 Spring Boot 的结合不如 Spring AI 顺手部分功能需要自己手动桥接。Spring AI 目前已经孵化了 ChatClient、EmbeddingModel、VectorStore、Advisor 等一套完整抽象聊到后面你会发现改模型供应商、换向量存储都是改配置的事写业务代码时完全不用关心底层实现。2.2 PostgreSQL pgvector 凭什么能当向量数据库你可能会问向量数据库不是有 Milvus、Chroma、FAISS、Weaviate 吗为什么推荐 PostgreSQL最大的理由是PostgreSQL 本身已经在企业里大规模部署了再加一个 pgvector 插件不需要额外引入一套新的数据库体系。很多团队为了跑一个 AI Demo 去搭 Milvus结果发现还得请人运维。而 PostgreSQL 装扩展就和CREATE EXTENSION vector;一样简单普通 DBA 都能接管。数据管理能力更是 PostgreSQL 的强项。向量数据往往和业务数据强关联比如电商场景里商品的向量要关联商品 ID、类目 ID、库存状态。在专用向量库里做这种关联查询很别扭但在 PostgreSQL 里就是普通的 SQL可以 JOIN、可以加业务过滤条件、可以用事务保证一致性。这是“文档型向量库”比不了的。当然它不是万能的。数据量到千万级、并发要求极高时专用向量数据库的性能和水平扩展会更占优。但对于零基础入门、中小规模业务、企业内部知识库PostgreSQL pgvector 已经是性价比极高的选择。我在做知识库类项目时十有八九都是这套组合。2.3 嵌入模型与距离策略怎么选RAG 链路里有一个容易被忽视的问题向量化的效果直接决定检索质量。在示例里我使用 OpenAI 的text-embedding-3-small它是 1536 维。为什么强调维度因为存入向量库的每一行数据都要带上这些维度算相似度就是拿这 1536 个数做计算。pgvector 支持三种距离策略我现在实际项目中也是按场景选的距离类型计算公式特征适用场景余弦距离COSINE只看方向、不看长度文本语义检索默认推荐欧氏距离L2看向量空间的实际距离图像特征、数值嵌入内积IP擅长处理归一化向量推荐场景中有时效果更好文中的 Spring AI 配置默认走余弦距离对中文文本的语义相似度计算表现稳定新手可以放心用。3. 环境准备与最小可运行工程搭建3.1 推荐版本与组合这个示例的版本组合我实测过建议直接照抄组件版本JDK17 或 21Spring Boot3.4.xSpring AI1.0.0PostgreSQL15 或 16pgvector0.7.0 及以上Maven3.9.x注意 Spring AI 1.0.0 要求 Spring Boot 3.3 以上JDK 17 以上。如果你用的是 JDK 8 或 Spring Boot 2.x硬凑版本会非常痛苦建议直接升级环境。3.2 安装 PostgreSQL 并启用 pgvector 扩展macOS 上如果用了 Homebrew安装很简单brew install postgresql16 brew install pgvector创建并启动服务后连接到目标数据库执行CREATE EXTENSION IF NOT EXISTS vector;检查是否生效SELECT * FROM pg_extension WHERE extname vector;Linux 或 Windows 上pgvector 可以通过包管理器安装也可以源码编译。Windows 下最省力的方式是用 Dockerdocker run --name pgvector-demo -e POSTGRES_PASSWORDpostgres -p 5432:5432 -d pgvector/pgvector:pg16这个镜像已经预装了 pgvector进容器执行CREATE EXTENSION vector;就能用。我自己第一次踩的坑就是连接库里没有执行扩展创建导致后面的建表语句一直报type vector does not exist。3.3 建 Spring Boot 工程与核心依赖创建一个普通的 Maven 项目pom.xml里引入这些依赖。Spring AI 的 Starter 和 Spring Boot 的依赖版本建议通过 BOM 统一管理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.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency 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 dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId scoperuntime/scope /dependency注意不要漏掉 JDBC 和 PostgreSQL 驱动否则 PgVectorStore 拿不到数据源启动会直接报错。我也见过有人只引了 Spring AI忘了加spring-boot-starter-jdbc结果运行时抛DataSource找不到的异常。application.yml 配置如下spring: datasource: url: jdbc:postgresql://localhost:5432/postgres username: postgres password: postgres ai: openai: api-key: ${OPENAI_API_KEY} chat: options: model: gpt-4o-mini embedding: options: model: text-embedding-3-small vectorstore: pgvector: initialize-schema: true index-type: HNSW其中initialize-schema: true表示启动时自动创建向量表我建议新手先用它跑通流程再考虑手工管理表结构。index-type: HNSW是向量索引类型检索量大时作用明显后面会展开讲。4. 完整代码实现一条 RAG 链路4.1 定义向量存储 BeanSpring Boot 的自动配置会根据依赖自动注册EmbeddingModel和数据源我们只需要把两者组合成VectorStore。注意要走注入而不是自己在代码里 new 一个因为自动配置还承担了读取 API Key、初始化模型客户端的工作import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.pgvector.PgVectorStore; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; Configuration public class VectorStoreConfig { Bean public VectorStore vectorStore(EmbeddingModel embeddingModel, DataSource dataSource) { return new PgVectorStore(dataSource, embeddingModel); } }为什么这里不需要传向量维度因为 Spring AI 会自动向EmbeddingModel查询它实际输出的维度比如text-embedding-3-small是 1536 维。这样你后面换模型时不用改这段代码只需要换配置。正确的做法是尽量依赖注入而不是到处硬编码维度。4.2 准备知识文档并完成切分入库知识文档是 RAG 的原料。我习惯把测试文档放在src/main/resources/docs下你先随便准备一个 Markdown 或 TXT 文件里面写几段关于服务器运维、数据库备份的内容。接着用文件读取的方式把文档内容加载成 Spring AI 的Documentimport org.springframework.ai.document.Document; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.io.InputStream; import java.util.List; Component public class KnowledgeIngestService { private final VectorStore vectorStore; public KnowledgeIngestService(VectorStore vectorStore) { this.vectorStore vectorStore; } public void ingestDocs() { try { InputStream is new ClassPathResource(docs/help.md).getInputStream(); String content new String(is.readAllBytes(), StandardCharsets.UTF_8); Document doc new Document(docs/help.md, content); TokenTextSplitter splitter new TokenTextSplitter(); ListDocument chunks splitter.split(doc); vectorStore.add(chunks); } catch (Exception e) { throw new RuntimeException(加载文档失败, e); } } }这里有个必修课文档必须切分不能整篇塞进向量库。原因有两方面。第一嵌入模型的输入长度有限而且长度越长向量化效果越差第二检索时我们只关心相关片段整篇文档作为一个向量检索结果会非常粗糙。Spring AI 的TokenTextSplitter默认按 token 数量切分每段约 1000 token相邻段之间保留 200 token 的冗余保证上下文不割裂。如果有条件也可以根据你的实际文档结构调整 chunk 大小比如偏 FAQ 风格用 300500 token 更精准偏长文叙述用 8001200 token 更容易保持结构。在 Spring Boot 启动时触发入库可以用ApplicationRunner或监听ApplicationReadyEvent。测试阶段我建议写一个启动后自动执行的方法方便观察Component public class KnowledgeInitRunner implements ApplicationRunner { private final KnowledgeIngestService ingestService; public KnowledgeInitRunner(KnowledgeIngestService ingestService) { this.ingestService ingestService; } Override public void run(ApplicationArguments args) { ingestService.ingestDocs(); } }4.3 实现相似度检索逻辑向量库写入后核心功能就是把用户问题和库里的文本做相似度匹配。Spring AI 的VectorStore.similaritySearch支持返回 top K 条结果K 值决定了我们拼给大模型的参考资料条数。K 太小可能漏掉关键信息K 太大又会把不相关的内容也塞进上下文增加 token 成本还容易把模型带偏我实际项目中取 35 比较稳妥。import org.springframework.ai.vectorstore.SearchRequest; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.stereotype.Service; import java.util.List; Service public class RagRetrievalService { private final VectorStore vectorStore; public RagRetrievalService(VectorStore vectorStore) { this.vectorStore vectorStore; } public ListString retrieve(String question) { SearchRequest request SearchRequest.builder() .query(question) .topK(4) .similarityThreshold(0.5) .build(); return vectorStore.similaritySearch(request) .stream() .map(doc - doc.getContent()) .toList(); } }similarityThreshold是相似度阈值用于过滤掉那些“不太相关”的片段。这里有个经验阈值不是越大越好。余弦相似度在 0.5 左右时已经能过滤掉大量无关内容但中文高质量语义对还是能稳定召回。如果业务要求宁可漏掉也不给错误答案可以调高到 0.7如果知识库内容偏少可以降到 0.3 提高召回率。4.4 用 ChatClient 把检索结果拼进 Prompt 并生成回答Spring AI 1.0 里最常用的对话客户端是ChatClient。它用起来跟 REST 客户端有点像链式调用很直观。先定义一个 Beanimport org.springframework.ai.chat.client.ChatClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class ChatConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是一名企业内部知识库助手。回答问题时优先参考给定资料不编造事实。) .build(); } }然后写生成服务。核心手法是把检索到的文本拼成一个临时提示词再交给大模型import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; Service public class RagChatService { private final ChatClient chatClient; private final RagRetrievalService retrievalService; public RagChatService(ChatClient chatClient, RagRetrievalService retrievalService) { this.chatClient chatClient; this.retrievalService retrievalService; } public String answer(String question) { ListString chunks retrievalService.retrieve(question); if (chunks.isEmpty()) { return 知识库中没有找到相关内容请补充资料后重试。; } String context chunks.stream() .map(c - - c) .collect(Collectors.joining(\n)); String userPrompt 请根据以下参考资料回答问题。如果资料中没有答案请直接说“资料中未找到相关内容”。 参考资料 %s 问题 %s .formatted(context, question); return chatClient.prompt() .user(userPrompt) .call() .content(); } }这里为什么要手动拼上下文而不是直接把检索结果原样返回因为大模型擅长从上下文中抽取信息但如果没有明确的指令约束它会把参考内容当成自己的知识。所以我在 Prompt 里显式写了“资料中没有答案就直接说没有”这类边界约束。很多人做 RAG 效果差一部分原因就是 Prompt 指令写得含糊。4.5 暴露 HTTP 接口并验证完整效果最后加一个 Controller方便用浏览器或 curl 直接测试import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; RestController public class ChatController { private final RagChatService ragChatService; public ChatController(RagChatService ragChatService) { this.ragChatService ragChatService; } GetMapping(/chat) public String chat(RequestParam(q) String question) { return ragChatService.answer(question); } }启动应用然后用 curl 验证curl --location http://localhost:8080/chat?q服务器磁盘满了怎么办如果知识库里有相关运维文档大模型会引用其中内容并组织答案如果文档里没有相关内容它会按 Prompt 约束回复“资料中未找到相关内容”。我第一次把它跑通时这种“模型胡编”到“模型知道该说没有”的转变对理解 RAG 的价值非常直观。4.6 进阶用 Advisor 快速实现 RAGSpring AI 还提供了一种更简洁的写法利用 Advisor 把检索步骤自动嵌入对话流程。你不需要手动拼 Prompt只需要传一个向量库作为参数chatClient.prompt() .user(服务器如何做定时备份) .advisors(spec - spec.param(vector_store, vectorStore)) .call() .content();底层原理是为用户消息自动做检索并拼上下文。对于几百行的小项目手动拼上下文更透明方便调试对于复杂的生产项目Advisor 可以由点及面地接入多个文档源。我建议先把手动方案吃透再切换成 Advisor否则出了问题你根本不知道是哪一步丢的。5. 常见问题与避坑实录5.1 环境与扩展类问题最常见的报错就是type vector does not exist。原因基本都是在当前数据库中没执行CREATE EXTENSION vector;。注意是哪个库连的就在哪个库里建扩展别在默认库建完然后在另一个库里用。命令行里可以这样检查psql -U postgres -c CREATE EXTENSION IF NOT EXISTS vector;另一个高频问题是我前面提过的DataSource注入失败。Spring AI 的 PgVectorStore 依赖spring-boot-starter-jdbc只引spring-boot-starter-data-jpa或没引入任何 JDBC starter 都会报缺数据源。解决方案就是补上依赖。5.2 向量维度不一致运行时机有可能报类似different vector dimensions的错误。为什么因为你之前用 768 维模型写入了数据之后又换成 1536 维模型新问题向量跟库里旧向量维度不同pgvector 没法做相似度计算。这个坑我已经踩过不止一次了。解决办法很简单换模型后把向量表清空重建。因为你是在做套索索引旧数据也不匹配新模型留着反而影响检索。另外请检查表里的 embedding 列定义确保它不是人为写死的固定维度SELECT column_name, data_type FROM information_schema.columns WHERE table_name vector_store;5.3 检索质量差命中不了相关内容如果你的 RAG 回答经常说“资料中未找到相关内容”但知识库里明明有通常不是代码问题而是三个细节没做好。第一文档没切好。过度切分会让一个段落的信息碎成多个片段每个片段都缺少关键上下文切分太粗则会让向量平均化失去语义区分度。建议围绕主题句切分让每个 chunk 尽量有完整逻辑。第二topK 太小。对较短的 chunk 建议 K 值取 5 甚至 8我见过不少项目 K2 导致召回率极低。第三阈值卡得太死。调试阶段可以先不设similarityThreshold看原始相似度分布再确定阈值。5.4 向量检索慢如何启用索引数据量小时不需要索引顺序扫描也无所谓。但数据量超过几十万行每次全表扫描做向量计算就受不了。pgvector 提供两个索引类型HNSW 和 IVFFlat索引类型特点推荐使用场景HNSW构建慢、内存占用大但查询快、召回率高绝大多数中小项目推荐IVFFlat构建快但需要先聚类且查询精度受列表数影响超大规模数据离线构建启动时配置index-type: HNSW后Spring AI 会在建表的同时创建索引。如果不想用自动建表也可以手工执行CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);注意 HNSW 索引构建要比 IVFFlat 慢但它查询速度更快。一般企业内部知识库不到百万级HNSW 稳得很。5.5 敏感信息泄露与回答不可信RAG 做企业内部知识库时最容易忽视的是权限问题。PostgreSQL 本身有行级权限机制如果你直接对整个vector_store表做全局检索任何能问的人都能把知识库内容套出来。我的做法是给向量文件附加元数据比如owner、department、permission_level检索时在搜索请求里加上过滤条件SearchRequest.builder() .query(question) .topK(4) .filterExpression(permission_level userLevel) .build();这个问题在真实项目中非常重要很多 RAG 项目上线后才发现用户能问到别人部门的数据到时候改起来费时费力。强烈建议你第一天设计表结构时就把权限元数据带上。我一个下午把整套链路跑通最耗时间的不是写代码而是环境版本和 pgvector 扩展这两个地方。但反过来看恰恰是在这些小坑里才真正理解了向量数据库和传统 SQL 的关系它不是一个神秘的新技术而是 PostgreSQL 能力的一次自然延伸。后续你可以继续实验不同的 chunk 大小、换不同的嵌入模型甚至把知识库从内部文档扩展到数据库里的业务数据。只要把这条链路想明白Java 后端接入 AI 能力就不再是一件需要“另起炉灶”的事了。