Spring AI 实战系列(九):RAG检索实战 —— 私有知识库
系列栏目:Spring AI
Spring AI 实战教程(一)入门示例
Spring AI 实战系列(二):ChatClient封装,告别大模型开发样板代码
Spring AI 实战系列(三):多模型共存+双版本流式输出
Spring AI 实战系列(四):Prompt工程深度实战
Spring AI 实战系列(五):结构化输出,让大模型严格适配你的业务数据模型
Spring AI 实战系列(六):Tool Calling深度实战,让大模型自动调用你的业务接口
Spring AI实战系列(七):Chat Memory实战,基于Redis实现持久化多轮对话
Spring AI 实战系列(八):多模态能力--文生图、语音合成与向量嵌入实战
Spring AI 实战系列(九):RAG检索实战 —— 私有知识库
Spring AI 实战系列(十):MCP深度集成 —— 工具暴露与跨服务调用
一、系列回顾与本篇定位
1.1 系列回顾
- 第一篇:完成 Spring AI 与阿里云百炼的基础集成,基于
ChatModel实现同步对话。 - 第二篇:解锁
ChatClient高层级 API,告别样板代码。 - 第三篇:实现 DeepSeek/Qwen 双模型共存与流式输出。
- 第四篇:深度拆解 Prompt 工程,掌握与大模型高效沟通的方法论。
- 第五篇:掌握结构化输出,实现大模型与业务系统的无缝对接。
- 第六篇:掌握 Tool Calling,让大模型自动调用业务接口。
- 第七篇:掌握 Chat Memory,基于 Redis 实现持久化多轮对话。
- 第八篇:解锁多模态能力 —— 文生图、语音合成与向量嵌入。
1.2 本篇定位
大模型虽然具备强大的通用知识,但它也有明确的能力边界:
- 它不知道你的私有业务数据(如内部运维手册、产品文档、客户资料)。
- 它的知识有截止日期,不知道最新的实时信息。
- 它可能会 **“幻觉”**(Hallucination),编造不存在的事实。
而RAG(Retrieval Augmented Generation,检索增强生成),正是解决这些问题的核心技术:它通过 “检索私有知识库 + 大模型基于检索结果生成回答” 的方式,让大模型能基于你的私有数据回答问题,同时大幅减少幻觉。
本篇是系列企业级核心收官篇,我们将完整实现一套基于 Spring AI 的 RAG 智能问答系统:
- 从核心原理出发,深度拆解 RAG 的全流程与 Spring AI 的模块化 RAG 架构。
- 基于 RedisStack 实现向量存储,完成文档加载、分块、向量化、存储的全流程。
- 实现带去重逻辑的知识库初始化,避免重复加载文档。
- 基于
RetrievalAugmentationAdvisor一行配置 RAG,实现私有知识库问答。 - 覆盖动态过滤、自定义 Prompt 模板、查询改写等进阶场景。
- 补充生产环境最佳实践与高频踩坑避坑指南。
二、核心概念拆解:Spring AI RAG 全原理
2.1 什么是 RAG
RAG 的核心流程非常简单,分为三步:
- 检索(Retrieval):当用户提问时,先从向量数据库中检索出与问题最相关的文档片段。
- 增强(Augmentation):将检索到的文档片段作为上下文,拼接到用户的问题中。
- 生成(Generation):将包含上下文的 Prompt 发送给大模型,让大模型基于上下文回答问题。
简单来说:RAG = 向量检索 + 大模型生成,它让大模型能 “查资料” 后再回答,既保留了大模型的语言能力,又注入了私有知识,同时减少了幻觉。
2.2 Spring AI 模块化 RAG 架构
Spring AI 1.0+ 版本推出了模块化 RAG 架构,参考了论文《Modular RAG: Transforming RAG Systems into LEGO-like Reconfigurable Frameworks》,将 RAG 拆分为多个可插拔的模块,你可以像搭积木一样组合出适合自己业务的 RAG 流程。
| 模块类型 | 作用 | 典型实现 |
|---|---|---|
| Pre-Retrieval(检索前) | 处理用户查询,提升检索质量 | RewriteQueryTransformer(查询改写)、MultiQueryExpander(查询扩展)、TranslationQueryTransformer(查询翻译) |
| Retrieval(检索) | 从数据源检索相关文档 | VectorStoreDocumentRetriever(向量库检索) |
| Post-Retrieval(检索后) | 处理检索到的文档,提升生成质量 | 文档重排序、去重、压缩 |
| Generation(生成) | 基于上下文生成最终回答 | ContextualQueryAugmenter(上下文增强) |
而对于大多数常见场景,Spring AI提供了两个开箱即用的Advisor,一行代码即可完成 RAG 配置:
QuestionAnswerAdvisor:简单场景的RAG Advisor,适合快速上手。RetrievalAugmentationAdvisor:高级场景的RAG Advisor,支持模块化配置,适合生产环境。
三、实战落地:从零构建企业级 RAG 系统
3.1 环境前提
- 已完成 JDK 17+、Spring Boot 3.2.x 环境搭建
- 已配置阿里云百炼 API Key 环境变量
DASHSCOPE_API_KEY - 已安装并启动RedisStack(包含向量搜索能力的 Redis 版本)
- 已在
pom.xml中引入以下依赖:
<!-- Spring AI Alibaba Starter --> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>1.0.0.2</version> </dependency> <!-- Spring AI RAG Advisors --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-advisors-vector-store</artifactId> </dependency> <!-- Spring AI Redis Vector Store --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-redis-store</artifactId> </dependency> <!-- Spring Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Hutool工具类(用于MD5去重) --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.26</version> </dependency>3.2 第一步:配置 Redis 与向量存储
首先配置 Redis 连接,注册RedisTemplate用于去重逻辑,同时 Spring AI 会自动配置VectorStore(基于 RedisStack)。
import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * Redis配置类:用于向量存储与去重逻辑 */ @Configuration @Slf4j public class RedisConfig { /** * 配置RedisTemplate,用于去重逻辑 */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置Key序列化方式:String redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); // 设置Value序列化方式:JSON redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }application.yml 配置:
spring: data: redis: host: localhost port: 6379 password: 123456 database: 0 ai: vectorstore: redis: initialize-schema: true # 自动初始化向量库Schema3.3 第二步:知识库初始化(带去重逻辑)
这是生产环境非常重要的一步:避免重复加载文档。我们通过 Redis 的setIfAbsent(SETNX)命令实现去重,确保同一文档只加载一次。
首先在src/main/resources目录下准备你的私有知识库文档(例如code.txt,运维故障手册):
故障编码:C00001 故障描述:服务器CPU使用率超过90% 解决方案: 1. 登录服务器,使用top命令查看CPU占用最高的进程 2. 如果是Java进程,使用jstack查看线程栈 3. 优化代码或扩容服务器 故障编码:C00002 故障描述:数据库连接池耗尽 解决方案: 1. 检查数据库连接池配置,适当增加最大连接数 2. 检查是否有连接泄漏,使用druid的监控功能 3. 优化慢SQL,减少连接持有时间然后编写知识库初始化配置类:
import cn.hutool.crypto.SecureUtil; import jakarta.annotation.PostConstruct; import org.springframework.ai.document.Document; import org.springframework.ai.reader.TextReader; import org.springframework.ai.transformer.splitter.TokenTextSplitter; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.data.redis.core.RedisTemplate; import java.nio.charset.Charset; import java.util.List; /** * 知识库初始化配置类:带去重逻辑 */ @Configuration public class InitVectorDatabaseConfig { @Autowired private VectorStore vectorStore; @Autowired private RedisTemplate<String, Object> redisTemplate; // 注入私有知识库文档 @Value("classpath:code.txt") private Resource opsFile; @PostConstruct public void init() { // 1. 读取文档:使用TextReader读取文本文件 TextReader textReader = new TextReader(opsFile); textReader.setCharset(Charset.defaultCharset()); // 2. 文档分块:使用TokenTextSplitter将长文档切分为适合向量化的片段 // 默认分块策略:每块800 Token,重叠200 Token List<Document> documents = new TokenTextSplitter().transform(textReader.read()); // 3. 去重逻辑:基于Redis SETNX实现,避免重复加载同一文档 // 计算文档源文件的MD5作为唯一标识 String sourceMetadata = (String) textReader.getCustomMetadata().get("source"); String textHash = SecureUtil.md5(sourceMetadata); String redisKey = "vector-initialized:" + textHash; // SETNX:如果Key不存在则设置值并返回true,否则返回false Boolean isFirstLoad = redisTemplate.opsForValue().setIfAbsent(redisKey, "1"); if (Boolean.TRUE.equals(isFirstLoad)) { // 4. 首次加载:将文档向量化并存入向量数据库 vectorStore.add(documents); System.out.println("✅ 知识库初始化成功,文档已向量化并存入RedisStack"); } else { System.out.println("ℹ️ 知识库已初始化过,跳过重复加载"); } } }关键说明:
- 文档分块:
TokenTextSplitter是 Spring AI 提供的默认分块器,它会基于 Token 数量切分文档,默认每块 800 Token,重叠 200 Token(重叠可以避免上下文丢失)。 - 去重逻辑:通过计算文档源文件的 MD5,结合 Redis 的
setIfAbsent命令,确保同一文档只加载一次,避免服务重启后重复向量化。 - 元数据:
TextReader会自动添加source元数据(文档路径),我们可以基于此做更多扩展。
3.4 第三步:配置 RAG Advisor
Spring AI 提供了RetrievalAugmentationAdvisor,这是一个模块化的 RAG Advisor,适合生产环境使用。我们将其集成到ChatClient中。
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ChatModel+ChatClient+RAG配置类 */ @Configuration public class LLMConfig { // 模型名称常量定义 private final String DEEPSEEK_MODEL = "deepseek-v3"; private final String QWEN_MODEL = "qwen-plus"; // ==================== ChatModel 实例注册 ==================== @Bean(name = "deepseek") public ChatModel deepSeekChatModel() { return DashScopeChatModel.builder() .dashScopeApi(DashScopeApi.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .build()) .defaultOptions(DashScopeChatOptions.builder() .withModel(DEEPSEEK_MODEL) .withTemperature(0.1) // RAG场景建议调低温度,提升回答稳定性 .build()) .build(); } @Bean(name = "qwen") public ChatModel qwenChatModel() { return DashScopeChatModel.builder() .dashScopeApi(DashScopeApi.builder() .apiKey(System.getenv("DASHSCOPE_API_KEY")) .build()) .defaultOptions(DashScopeChatOptions.builder() .withModel(QWEN_MODEL) .withTemperature(0.1) // RAG场景建议调低温度 .build()) .build(); } // ==================== 带RAG的ChatClient 实例注册 ==================== @Bean(name = "qwenChatClient") public ChatClient qwenChatClient( @Qualifier("qwen") ChatModel qwenChatModel, VectorStore vectorStore) { // 配置RAG Advisor RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(5) // 返回最相关的5条文档 .similarityThreshold(0.7) // 相似度阈值,只返回相似度大于0.7的文档 .build()) .build(); return ChatClient.builder(qwenChatModel) .defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build()) .defaultAdvisors(ragAdvisor) // 全局默认启用RAG .build(); } @Bean(name = "deepseekChatClient") public ChatClient deepseekChatClient( @Qualifier("deepseek") ChatModel deepSeekChatModel, VectorStore vectorStore) { RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(5) .similarityThreshold(0.7) .build()) .build(); return ChatClient.builder(deepSeekChatModel) .defaultOptions(ChatOptions.builder().model(DEEPSEEK_MODEL).build()) .defaultAdvisors(ragAdvisor) .build(); } }关键说明:
- 温度设置:RAG 场景建议将
temperature调低至 0.1-0.3,降低大模型的随机性,让它更严格地基于上下文回答。 - TopK 与相似度阈值:
topK:设置返回最相关的 N 条文档,建议 5-10 条。similarityThreshold:设置相似度阈值,只返回相似度大于该值的文档,避免检索到不相关的内容。
3.5 第四步:实现 RAG 问答接口
最后编写 RAG 问答接口,实现基于私有知识库的智能问答。
import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.rag.advisor.RetrievalAugmentationAdvisor; import org.springframework.ai.rag.retrieval.search.VectorStoreDocumentRetriever; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; /** * RAG智能问答接口 */ @RestController public class RagController { @Resource(name = "qwenChatClient") private ChatClient chatClient; /** * RAG智能问答接口 * 访问示例:http://localhost:8012/rag4aiops?msg=C00001 * 访问示例:http://localhost:8012/rag4aiops?msg=数据库连接池耗尽怎么办 */ @GetMapping("/ragaiops") public Flux<String> rag(@RequestParam(name = "msg") String msg) { // 系统提示词:设定AI角色,要求它基于上下文回答 String" 你是一个专业的运维工程师,你的职责是根据提供的运维故障手册,回答用户的故障问题。 请严格遵循以下规则: 1. 如果故障信息在上下文中,请基于上下文给出清晰的解决方案。 2. 如果故障信息不在上下文中,请直接回复“抱歉,未找到该故障的相关信息”,不要编造内容。 3. 回答要简洁、专业、有可操作性。 """; return chatClient .prompt() .system(systemPrompt) .user(msg) .stream() .content(); } }接口测试说明:
- 启动服务后,知识库会自动初始化(如果是首次加载)。
- 访问
http://localhost:8080/ragaiops?msg=C00001,大模型会基于检索到的故障手册,给出 CPU 使用率过高的解决方案。 - 访问
http://localhost:8080/ragaiops?msg=数据库连接池耗尽怎么办,大模型会基于语义检索,找到对应的故障手册并回答。
四、进阶场景
4.1 动态过滤:基于元数据过滤检索结果
如果你的文档有元数据(如type、department、version),可以通过动态过滤表达式,只检索特定类型的文档。
@GetMapping("/rag4aiops/filtered") public Flux<String> ragFiltered(@RequestParam(name = "msg") String msg) { // 动态过滤:只检索type为"database"的文档 return chatClient .prompt() .user(msg) .advisors(advisorSpec -> advisorSpec .param(VectorStoreDocumentRetriever.FILTER_EXPRESSION, "type == 'database'")) .stream() .content(); }4.2 自定义 Prompt 模板:定制上下文拼接方式
默认情况下,RetrievalAugmentationAdvisor会使用默认的模板拼接上下文和用户问题。你可以通过自定义PromptTemplate来定制拼接方式。
// 在SaaLLMConfig中配置自定义PromptTemplate import org.springframework.ai.chat.prompt.PromptTemplate; import org.springframework.ai.rag.generation.augmentation.ContextualQueryAugmenter; @Bean(name = "qwenChatClient") public ChatClient qwenChatClient( @Qualifier("qwen") ChatModel qwenChatModel, VectorStore vectorStore) { // 自定义Prompt模板:必须包含{query}和{question_answer_context}两个占位符 PromptTemplate customPromptTemplate = PromptTemplate.builder() .template(""" 你是一个专业的运维工程师。 以下是运维故障手册的相关内容: --------------------- {question_answer_context} --------------------- 用户的问题是:{query} 请基于上述故障手册回答用户的问题。如果没有相关信息,请直接说“未找到”。 """) .build(); // 配置ContextualQueryAugmenter使用自定义模板 ContextualQueryAugmenter queryAugmenter = ContextualQueryAugmenter.builder() .promptTemplate(customPromptTemplate) .build(); RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(5) .similarityThreshold(0.7) .build()) .queryAugmenter(queryAugmenter) // 使用自定义的QueryAugmenter .build(); return ChatClient.builder(qwenChatModel) .defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build()) .defaultAdvisors(ragAdvisor) .build(); }4.3 查询改写:提升检索质量
如果用户的问题比较模糊或冗长,可以使用RewriteQueryTransformer先改写用户问题,再进行检索,提升检索质量。
import org.springframework.ai.rag.preretrieval.query.RewriteQueryTransformer; @Bean(name = "qwenChatClient") public ChatClient qwenChatClient( @Qualifier("qwen") ChatModel qwenChatModel, VectorStore vectorStore) { // 配置查询改写:使用大模型改写用户问题 RewriteQueryTransformer queryTransformer = RewriteQueryTransformer.builder() .chatClientBuilder(ChatClient.builder(qwenChatModel)) .build(); RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder() .queryTransformers(queryTransformer) // 添加查询改写模块 .documentRetriever(VectorStoreDocumentRetriever.builder() .vectorStore(vectorStore) .topK(5) .similarityThreshold(0.7) .build()) .build(); return ChatClient.builder(qwenChatModel) .defaultOptions(ChatOptions.builder().model(QWEN_MODEL).build()) .defaultAdvisors(ragAdvisor) .build(); }五、实践建议
5.1 文档处理最佳实践
- 文档格式:优先使用纯文本(.txt)、Markdown(.md)格式,避免复杂的 Word、PDF 格式(如果必须使用,需要引入对应的文档加载器)。
- 分块策略:
- 通用文档:每块512-1024Token,重叠 100-200 Token。
- 代码文档:每块256-512Token,重叠 50-100 Token(代码上下文敏感)。
- 长文档:每块1024-2048Token,重叠 200-400 Token。
- 元数据管理:给文档添加丰富的元数据(如
type、department、version、author),便于后续动态过滤。
5.2 检索优化最佳实践
- 相似度阈值:根据业务场景调整相似度阈值,一般建议 0.6-0.8。阈值太高会漏检,阈值太低会引入不相关内容。
- TopK 设置:根据文档长度和模型上下文窗口调整 TopK,一般建议 5-10 条。确保检索到的文档总 Token 数不超过模型上下文窗口的 1/3。
- 向量模型选择:优先使用
text-embedding-v3,语义理解能力强,检索准确率高。 - 查询改写:对于用户问题模糊的场景,添加
RewriteQueryTransformer,提升检索质量。
5.3 运维监控最佳实践
- 知识库版本管理:给知识库添加版本号,更新知识库时先清空旧版本,再加载新版本,避免新旧数据混杂。
- 检索效果监控:定期抽查检索结果,记录准确率和召回率,及时调整分块策略和相似度阈值。
- 向量库监控:监控 RedisStack 的内存使用情况、向量数量、查询耗时,及时扩容或优化。
- 成本监控:监控向量模型的 Token 消耗和大模型的 Token 消耗,控制成本。
六、避坑指南
6.1 文档重复加载
- 现象:服务重启后,向量库中的文档数量翻倍。
- 原因:没有去重逻辑,每次启动都重新加载文档。
- 解决方案:参考我们的
InitVectorDatabaseConfig,使用Redis的setIfAbsent实现去重。
6.2 检索结果不准确
- 现象:检索到的文档与用户问题不相关。
- 原因:
- 分块策略不合理,文档切得太碎或太大。
- 相似度阈值设置得太低。
- 向量模型选择不当。
- 解决方案:
- 优化分块策略,调整块大小和重叠大小。
- 调高相似度阈值(如从 0.6 调到 0.75)。
- 更换语义理解能力更强的向量模型(如
text-embedding-v3)。
6.3 大模型仍然幻觉
- 现象:即使有 RAG,大模型仍然编造内容。
- 原因:
- 系统提示词不够严格。
- 温度设置得太高。
- 检索到的文档不相关。
- 解决方案:
- 在系统提示词中明确要求 “如果没有相关信息,请直接说未找到,不要编造”。
- 调低温度至 0.1-0.3。
- 优化检索策略,确保检索到的文档相关。
6.4 向量库连接失败
- 现象:启动服务时报错,提示无法连接到向量库。
- 原因:
- RedisStack 未启动。
- 配置文件中的连接信息错误。
- 使用的是普通 Redis 而非 RedisStack。
- 解决方案:
- 检查 RedisStack 是否正常启动。
- 核对配置文件中的连接信息。
- 确保使用的是 RedisStack(包含向量搜索模块),而非普通 Redis。
七、本篇总结
本篇我们实现了一套基于Spring AI的RAG智能问答:
- 从核心原理出发,深度拆解了RAG的全流程与Spring AI的模块化 RAG 架构。
- 基于 RedisStack 实现了向量存储,完成了文档加载、分块、向量化、存储的全流程。
- 实现了带去重逻辑的知识库初始化,避免重复加载文档。
- 基于
RetrievalAugmentationAdvisor一行配置RAG,实现了私有知识库问答。 - 覆盖了动态过滤、自定义 Prompt 模板、查询改写等进阶场景。
- 补充了生产环境最佳实践与高频踩坑避坑指南。
RAG是企业级AI应用的核心能力,它让大模型能基于你的私有数据回答问题,同时大幅减少幻觉,是将大模型落地到业务系统的最常用方式。
八、下篇预告
在Tool Calling实战中,我们的工具是与Spring Boot应用强绑定的,无法跨语言、跨服务复用,也无法接入社区丰富的第三方工具生态。下一篇让大模型能以统一的方式连接外部工具、数据源和服务--MCP (Model Context Protocol)
传送门:Spring AI 实战系列(十):MCP深度集成 —— 工具暴露与跨服务调用
如果本系列教程对你有帮助,欢迎点赞、收藏、评论。