☰
Spring AI RAG实战:Java开发者构建知识库问答系统的完整指南
2026/10/22 18:11:19 网站建设 项目流程

我去年在一个内部知识库项目里尝试接入RAG,当时最头疼的就是怎么选框架——LangChain生态丰富但学习曲线陡,直接调OpenAI接口又得自己写一堆检索逻辑。后来换了Spring AI,整个开发节奏明显顺了很多,尤其是它把向量检索、模型调用、提示词管理这些脏活封装得相当干净。这篇就围绕Spring AI RAG实战,把我踩过的坑、调试过程和最终落地方案完整记录下来,希望能帮你少走弯路。

1. 项目背景与方案选型:为什么最终锁定Spring AI

先说结论:如果你所在的团队以Java为主,且现有系统已经重度依赖Spring生态,Spring AI几乎是目前融入成本最低的RAG落地方式。这个判断是我对比了多种实现路径之后得出的,下面说说具体考量。

1.1 RAG到底解决了什么问题

RAG(Retrieval-Augmented Generation,检索增强生成)本质上解决的是大模型“不知道”的问题。纯靠模型参数里的固有知识,回答私有知识库、实时数据、企业内部文档这类问题时,要么答非所问,要么一本正经地编造——也就是业内常说的幻觉问题。RAG的思路很直接:在模型回答问题之前,先从外部知识库中检索出相关内容片段,然后把这些片段和用户问题一起拼进提示词,让模型基于给定的上下文作答。

这样做有两个明显好处:一是回答可以追根溯源,因为答案是基于检索到的实际文档生成的,不是凭空捏造;二是知识更新成本低,需要更新知识时只需刷新向量库,不用重新训练模型。

1.2 对比LangChain、LlamaIndex和Spring AI的选型过程

我最初调研时重点看了三条技术路线,各有特点,最终结合团队情况做了取舍:

方案核心优势主要问题适用场景
LangChain组件全、社区活跃、生态丰富Python栈,与Java团队技术栈割裂;版本迭代快,API变动频繁纯Python团队快速验证原型
LlamaIndex数据索引和检索能力强,组件化程度高同样受限于Python栈,文档较复杂偏重数据管道、文档分析场景
Spring AI原生融入Spring生态,抽象统一,Java类型安全,生产级配置完善起步相对晚,部分进阶能力还在演进Java/Spring技术栈的团队落地RAG

我们团队的技术栈是Java + Spring Boot,如果强行引入Python的RAG框架,等于要在架构里额外维护一套Python服务,还要处理跨语言调用的序列化协议、部署链路和监控成本。Spring AI则可以让RAG能力直接嵌入现有Spring服务,复用原有的配置中心、日志链路和部署体系,运维心智负担小很多。

1.3 Spring AI的核心抽象

Spring AI借鉴了Spring生态一贯的“约定优于配置”理念,提供了一套统一的接口抽象,核心就三个词:Model、VectorStore、Advisor。

  • Model是对模型提供方的抽象,不管是OpenAI、通义千问,还是本地部署的Ollama,对外都以统一的ChatClient接口暴露,切换模型只需改配置。
  • VectorStore是对向量数据库的抽象,Pincone、Redis、PGVector、Milvus都实现了统一的存储和相似度检索接口。
  • Advisor是拦截器机制,类似Spring MVC里的Interceptor,可以在调用模型前后插入处理逻辑,比如自动检索、记录日志、追加安全审查等。

这三层抽象让RAG流水线的搭建变成了一种“组合式”开发:定义好多维向量字段、指定Embedding模型、注册向量存储、配置Advisor链,一个具备RAG能力的最小服务就搭起来了。

2. 环境准备与基础依赖配置

开始动手之前,先把开发环境和依赖配好。这块看似简单,但版本兼容问题能让人卡一整天,我把验证过的组合直接列出来。

2.1 技术栈版本组合

我实测下来比较稳的一套组合是:

  • JDK 17(Spring Boot 3.x要求JDK 17起步)
  • Spring Boot 3.3.x / 3.2.x
  • Spring AI 0.8.1(版本变化快,建议锁定一个已验证的稳定版本)
  • PostgreSQL 14+(我用的PGVector插件来存向量)
  • 向量维度建议与Embedding模型输出对齐,我使用的是常见BGE系列或OpenAI embedding,维度一般是1024或1536

依赖配置方面,如果使用Maven,在pom.xml中加入:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-pgvector-store</artifactId> <version>0.8.1</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai</artifactId> <version>0.8.1</version> </dependency>

因为Spring AI目前有些模块还没有完全发布到Maven中央仓库,如果你拉不到依赖,需要在pom中额外引入Spring仓库:

<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>

2.2 向量数据库选型:为什么优先PGVector

我知道市面上的向量数据库不少——专门的Milvus、轻量的Chroma、云原生的Pinecone。但我们最终选了PGVector,理由很朴素:PostgreSQL本来就是业务数据的主库,把向量直接存在同一套数据库里,不需要额外部署一个基础组件,也省掉了两套数据之间的同步痛点。

PGVector本质上是PostgreSQL的一个扩展插件,提供了vector数据类型和HNSW/IVFFlat两种索引。对于百万级以下的向量规模,PGVector性能完全够用,更重要的是对DBA友好,备份、权限、监控都能沿用现有体系。

如果你的向量量级更大(千万级以上)、查询并发很高,那需要认真考虑Milvus这类专用引擎;但如果只是企业内部知识库、个人项目验证,PGVector性价比是最高的。

安装PGVector扩展只需一条SQL:

CREATE EXTENSION IF NOT EXISTS vector;

Spring AI在启动时会自动建表并初始化向量字段,默认的表结构我后面会展示,你基本不需要手写建表语句。

3. 构建RAG流水线的完整实操过程

理论和配置都备好了,现在进入核心环节——把一条可用的RAG链路搭起来。我按实际开发的顺序来写,这样更容易理解每一步在整条链路里的位置和作用。

3.1 定义向量存储结构

Spring AI使用VectorStore接口屏蔽底层差异,对接PGVector时,它会自动管理一张名为vector_store的表。如果你想调整表名或基础结构,可以通过PgVectorStore的构造参数定制。

在代码中,我们通过配置类把它注册成Spring Bean:

@Configuration public class VectorStoreConfig { @Bean public VectorStore vectorStore(JdbcTemplate jdbcTemplate, EmbeddingModel embeddingModel) { return new PgVectorStore(jdbcTemplate, embeddingModel); } }

这里EmbeddingModel是Spring AI对Embedding模型的抽象,底层可以是OpenAI接口、本地Ollama,也可以是HuggingFace上的模型。定义完成后,调用vectorStore.add(List<Document>)即可将文档写入向量库,Spring AI内部会把文档内容切块并逐批调用Embedding模型生成向量,整个过程对业务方透明。

3.2 文档加载与切块策略

RAG的检索质量很大程度取决于文档怎么切。切块(splitting)直接把控着“检索到的片段是否够用”。

我记得第一次测试时,用的是几百字的默认切块大小,结果检索效果很一般。后来研究了一下Spring AI的文档切分器,发现它支持按固定长度、按段落、按递归字符切分。核心参数有三个:

  • maxChunkSize:单个切块的最大字符数
  • overlap:相邻切块之间的重叠字符数
  • separators:分隔符优先级

我的经验是,默认值适合英文文本,中文文档需要调整切分参数。因为中文没有天然的空格分词,按字符切容易切断语义。实测中,对于中文技术文档,每块300到500字、重叠80到100字的效果相对理想。

TokenTextSplitter splitter = new TokenTextSplitter(500, 100, true); List<Document> documents = splitter.split(document);

这里TokenTextSplitter按Token数切分,比按字符切更符合LLM的上下文窗口逻辑。参数里第一个是目标Token数(我通常按500配),第二个是重叠Token数,第三个表示是否启用边界感知。

切割完成后,一定要给文档设置自定义元数据(metadata),比如来源文件名、章节标题、所属部门等,这样后续在prompt中才能展示引用来源,也方便按条件过滤检索范围。

Document doc = new Document(content); doc.getMetadata().put("source", "产品手册-2024v3.pdf"); doc.getMetadata().put("category", "产品文档");

3.3 Embedding模型选择与向量化

Embedding是RAG的质量底座,选什么样Embedding模型直接影响检索准确率。Spring AI这边对Embedding模型的接入方式比较灵活,配置在application.yml里即可。

如果你使用OpenAI的text-embedding-3-small,配置如下:

spring: ai: openai: api-key: ${OPENAI_API_KEY} embedding: model: text-embedding-3-small

如果你想用完全本地化的方案(数据不出内网),可以通过Ollama跑bge-m3或nomic-embed-text,配置方式类似。我个人在当前项目里,因为对数据安全要求比较高,最终用了本地的Ollama +bge-m3:

spring: ai: ollama: base-url: http://localhost:11434 embedding: model: bge-m3

需要强调的是,切换Embedding模型时,最容易翻车的一个坑是:向量维度不一致导致已入库数据无法被检索。一旦换了模型,历史向量的维度大概率会变,老数据就全废了。所以项目早期就应该锁定Embedding模型,后续尽量不切换。

3.4 检索与问答链路的组装

Spring AI把“从向量库检索TopK片段 → 拼接到系统提示词 → 调用LLM生成答案”这整套流程,封装成了Advisor链。我们只需要构建一个带检索能力的ChatClient即可:

@Bean ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) { return builder .defaultAdvisors(QuestionAnswerAdvisor.builder(vectorStore).order(1).build()) .build(); }

这个QuestionAnswerAdvisor就是RAG的核心——它会把每次用户提问转换成向量,去向量库检索最相似的K个文档片段,然后把片段作为上下文注入系统提示词,再交给LLM回答。

整个调用形式非常简洁:

String answer = ragChatClient.prompt() .user(question) .call() .content();

调用入口只有一行,但内部完成了检索、拼接、生成三步。这就是Spring AI这种框架最“爽”的地方——框架帮你处理了80%的样板代码,你只需要关注业务和调优。

3.5 系统提示词设计:让模型“会说话”

如果不设计提示词,模型直接把检索到的片段摆出来回答,效果会非常生硬。我建议自定义系统提示词,明确几个规则:

  • 优先使用给定的上下文回答,不要臆造
  • 如果上下文中没有相关内容,直接说明“知识库中未找到答案”
  • 回答时标注引用来源(从元数据中取)

在Spring AI里,可以通过Advisor自定义提示词模板,也可以直接覆盖默认的QuestionAnswerAdvisor系统提示词。我的方案是基于QuestionAnswerAdvisor.builder()传入自定义文本:

QuestionAnswerAdvisor advisor = QuestionAnswerAdvisor.builder(vectorStore) .systemText(""" 你是一名严谨的知识库助手。请基于以下【参考资料】内容回答问题。 规则: 1. 只使用参考资料中的信息作答。 2. 如果参考资料中没有答案,请直接回复“知识库中暂未找到相关信息”。 3. 回答原文使用【参考资料】中对应的语言和格式。 4. 在回答末尾,列出信息来源文件名。 """) .build();

这段提示词是我在不断试错中调整出来的最终版本,基本可以满足大多数内部知识库问答场景。实际效果中,“知识库中暂未找到相关信息”这句兜底话术非常重要,它大幅降低了模型瞎编的概率。

4. 常见问题与排查技巧实录

这部分我认为是最有价值的实战沉淀。我把自己在开发Spring AI RAG过程中真实踩过的坑,按排查过程完整整理出来。

4.1 检索结果不准,答非所问

症状:检索出来的片段和用户问题明明语义无关,模型回答也就胡说八道。

排查步骤:

先看Embedding模型是否是同一个。我踩过的一个坑是,入库时用的BGE模型,检索时配置被切成了OpenAI模型,导致两个向量的语义空间完全不一致,相似度计算结果全部失真。

再看TopK设置。Spring AI里QuestionAnswerAdvisor默认检索的片段数量是4,如果知识库文档比较碎,4个片段可能不够,我可以调整K值:

QuestionAnswerAdvisor.builder(vectorStore) .topK(8) ... .build()

最后检查元数据过滤。如果你的知识库有多个类别、多种文档,不加过滤条件的话,检索范围是全库。如果文档差异很大,建议按category字段过滤。Spring AI里可以通过动态表达式过滤:

.prompt() .advisor(检索过滤表达式, "category == '产品文档'")

4.2 OpenAPI Key或连接异常导致的500错误

症状:首次请求RAG接口报错,日志里能看到连接超时或API Key未认证。

排查方向我会优先确认配置项有没有被Spring正确加载——Spring AI对OpenAI的API Key有默认的占位符配置spring.ai.openai.api-key,如果你只配置了裸token没配完整前缀,或者用了错误的环境变量名,启动是不会报错的,但调用模型时会401。建议在启动日志里看Spring配置加载情况,确认实际生效的配置文件。

另一个常见问题是网络策略,本地开发环境如果用了代理,调用OpenAI接口时有可能出现连接超时或证书错误,需要在JVM参数中配置代理信息或临时关闭代理调试。

4.3 中文文档检索效果差,切块导致语义断裂

我反复测试过,切块策略对中文文档的结果影响非常明显。踩了两次坑之后,我总结出的经验是:

  • 不要盲信默认参数,针对中文内容手动指定分隔符优先级,优先按章、节、段落边界切分
  • 对表格类内容,尽量整表保留在一个切块里,避免表格被拦腰截断
  • 对代码示例,尽量按代码块切分,避免代码中间被切断

调整切分器时,我固定了一个思路:先用段落标题/换行符分割出大块,再对大块内部按长度二次切分。实现上可以用TokenTextSplitter+ 自定义分隔符:

TokenTextSplitter.builder() .maxTokens(500) .overlapTokens(100) .separators(Arrays.asList("\n\n", "\n", "。", ";", " ")) .build();

4.4 PGVector检索性能慢或索引失效

问题场景:向量数据量到了几十万条之后,检索开始明显变慢。

排查发现,PGVector插件虽然默认帮你建索引,但如果数据量小的时候可能不会自动构建HNSW索引,或者建索引时没有指定合适的distance函数。需要在建索引时注意向量距离类型与检索时的一致性:

CREATE INDEX ON vector_store USING hnsw (embedding vector_cosine_ops);

同时,表数据量过大时建议定期清理重复或已失效的向量数据,避免检索范围无意义膨胀。

5. 生产化改造与性能调优

能跑通Demo和能上线是两回事。RAG项目到了生产环节,还需要关注几个容易被忽略的点。

5.1 流式输出改造

原型阶段的call()是阻塞式的,用户要在后台等几百毫秒甚至几秒才能看到结果。这在真实使用场景里是不可接受的。Spring AI对ChatClient提供了流式调用的方式,改为stream()即可:

Flux<String> answerStream = ragChatClient.prompt() .user(question) .stream() .content();

前端通过SSE或WebSocket承接这个流,就能实现打字机式输出,体验感完全不一样。

5.2 检索策略继续升级:从单路检索到融合检索

用过一段时间后你会发现,纯靠向量检索在某些场景下还是不够准——比如姓名、编号这类精确匹配词,语义检索匹配不到。为此我引入了混合检索:向量检索 + 关键词(BM25)检索,然后通过结果融合算法(如RRF)排序合并。

Spring AI原生目前不直接提供混合检索,但可以自己实现一个Advisor,在内部并行调用向量检索和全文检索后做合并。这块改造虽然增加了一点开发量,但检索召回率提升非常明显。

5.3 缓存与可观测性

生产环境里,同义或重复的问题会频繁出现,每次触发向量检索和LLM调用既费钱又费时。加一层Redis缓存(以用户问题的Hash为Key)能显著降低成本,我遇到的热点知识问答,缓存命中率大约有35%到45%。

运维侧,建议把每次RAG问答链路里涉及的检索片段、Token数、耗时、引用来源都记录下来。Spring AI虽然自带简单的观测日志,但生产级还需要接上链路追踪工具,把advisorName和检索到的文档ID作为标签上报,这对于追查回答质量问题和成本分析太重要了。

6. 实践经验总结与下一步想法

这个项目从搭骨架到跑通,我用了一周多一点的时间——前三天配置环境踩坑,后面主要花在切块和提示词调优上。回头看,Spring AI确实是Java技术栈做RAG应用当前最顺手的一条路,但它的“顺手”不只体现在封装度上,更体现在它保留了足够的灵活性:我想换Embedding模型、想覆盖系统提示词、想自定义检索逻辑,都有对应的扩展点,不会被框架卡住。

另一个体会是RAG项目的性能瓶颈往往不是框架本身,而是你对文档切片和检索策略的调优深度。做得好的RAG,检索结果精准到用户提的问题恰好能在Top1、Top2的片段里找到答案;做得不好的,再强的LLM也救不回来。

目前我还在尝试的方向是:把多轮对话历史也纳入RAG的检索上下文,让用户追问上一轮问题时,检索能结合历史提问意图去召回相关内容,这个点Spring AI也有对应的Advisor可以扩展,后续有成果了我再写一篇续文分享。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询