前言
随着大语言模型(LLM)的广泛应用,如何让模型"知道"企业内部知识成为一个关键问题。RAG(Retrieval-Augmented Generation)技术通过将检索与生成相结合,让模型能够基于外部知识库回答问题,有效解决了大模型知识滞后和幻觉的问题。
本文将手把手教你使用 Spring AI Alibaba 与 Redis Stack 向量数据库 构建一个智能运维故障解答系统。系统能够根据故障编码,自动检索运维文档并给出对应的故障解释。
技术栈:Spring Boot 3.x + Spring AI Alibaba + DashScope 灵积模型服务 + Redis Stack 向量存储
一、项目背景与需求
假设我们有一份运维故障编码说明文档 ops.txt,内容大致如下:
00000 系统正常
C2222 CPU温度过高
A1001 磁盘空间不足
...
我们要实现一个接口,用户输入故障编码(如 C2222),系统能够从文档中检索到对应说明,并通过大模型整理成自然语言回复。
二、环境准备与版本适配
1. Redis Stack 安装
Redis Stack 提供了向量检索能力,可以直接使用 Docker 快速启动:
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
2. 开通阿里云灵积模型服务
在阿里云百炼平台 开通 DashScope 服务,获取 API Key,并确保账户有 deepseek-r1、deepseek-v3、qwen-plus、text-embedding-v3 等模型的调用权限。
3. 重要版本适配说明
- 必须使用 Redis Stack :普通 Redis 不支持向量检索,请确认运行的是
redis-stack或redis-stack-server镜像。 - 嵌入模型固定使用
text-embedding-v3:保证向量维度与后续检索匹配,不可随意切换,否则会导致维度不一致。 - 多模型共存时必须指定 Bean 名称 :项目中同时配置了 DeepSeek 和 Qwen 模型,通过
@Bean(name = "xxx")配合@Qualifier明确注入,避免启动时因类型冲突报错。
三、项目依赖与配置
1. Maven 依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Alibaba DashScope 支持 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- Redis 向量数据库支持 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency>
</dependencies>
2. application.properties 配置
server.port=8012
# 全局编码
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true
server.servlet.encoding.charset=UTF-8
spring.application.name=SAA-12RAG4AiDatabase
# Spring AI Alibaba 配置
spring.ai.dashscope.api-key=${aliQwen-api}
spring.ai.dashscope.chat.options.model=deepseek-r1
spring.ai.dashscope.embedding.options.model=text-embedding-v3
# Redis Stack 配置
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.username=default
spring.data.redis.password=
spring.ai.vectorstore.redis.initialize-schema=true
spring.ai.vectorstore.redis.index-name=atguigu-index
spring.ai.vectorstore.redis.prefix=atguigu-prefix
注意:
${aliQwen-api}需要在环境变量中设置,避免密钥泄漏。
四、核心配置类详解
1. 多模型共存配置 -- SaaLLMConfig
在实际项目中,我们可能需要同时接入多个模型(如 DeepSeek 和 Qwen),Spring AI 支持通过 @Qualifier 区分不同的 ChatModel 和 ChatClient。
@Configuration
public class SaaLLMConfig {
private final String DEEPSEEK_MODEL = "deepseek-v3";
private final String QWEN_MODEL = "qwen-plus";
@Bean(name = "deepseek")
public ChatModel deepSeek() {
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder()
.apiKey(System.getenv("aliQwen-api"))
.build())
.defaultOptions(DashScopeChatOptions.builder()
.withModel(DEEPSEEK_MODEL)
.build())
.build();
}
@Bean(name = "qwen")
public ChatModel qwen() {
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder()
.apiKey(System.getenv("aliQwen-api"))
.build())
.defaultOptions(DashScopeChatOptions.builder()
.withModel(QWEN_MODEL)
.build())
.build();
}
@Bean(name = "deepseekChatClient")
public ChatClient deepseekChatClient(@Qualifier("deepseek") ChatModel deepSeek) {
return ChatClient.builder(deepSeek).build();
}
@Bean(name = "qwenChatClient")
public ChatClient qwenChatClient(@Qualifier("qwen") ChatModel qwen) {
return ChatClient.builder(qwen).build();
}
}
2. Redis 序列化配置 -- RedisConfig
为避免 Redis 中出现乱码,我们需要自定义 RedisTemplate 的序列化方式。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key 使用 String 序列化
template.setKeySerializer(new StringRedisSerializer());
// value 使用 JSON 序列化
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
五、文档向量化与初始化存储
为什么会出现向量重复入库?
Spring AI 默认的 VectorStore 并没有幂等机制。如果你直接将文档切片后的 List<Document> 调用 vectorStore.add(),那么每次项目重启时都会重新读取文件、重新生成向量、再次插入数据库,导致:
- 向量库数据冗余,存储膨胀;
- 检索时召回多条完全相同或高度重复的片段,干扰大模型判断;
- 结果重复错乱,回答质量下降。
本文的解决方案
- 基于文件源 MD5 校验:精准定位同一知识库文件,无论重启多少次,只要源文件未变,就不会重复写入。
- 利用 Redis 原子
SETNX命令:在分布式、单机环境下均能保证初始化操作的幂等性。 - 无需手动清空向量库:支持项目反复重启,自动跳过已导入的数据。
- 轻量无侵入:不改变原有 RAG 检索逻辑,只在初始化环节增加防重判断。
具体实现如 InitVectorDatabaseConfig 所示:
@Configuration
public class InitVectorDatabaseConfig {
@Autowired
private VectorStore vectorStore;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Value("classpath:ops.txt")
private Resource opsFile;
@PostConstruct
public void init() {
// 1. 读取文件
TextReader textReader = new TextReader(opsFile);
textReader.setCharset(Charset.defaultCharset());
// 2. 分割文档为段落并转为 Document 列表
List<Document> documents = new TokenTextSplitter().transform(textReader.read());
// 3. 生成唯一标识(基于文件路径的 MD5)
String source = (String) textReader.getCustomMetadata().get("source");
String textHash = SecureUtil.md5(source);
String redisKey = "vector-xxx:" + textHash;
// 4. 使用 Redis setIfAbsent 实现初始化防重
Boolean firstTime = redisTemplate.opsForValue().setIfAbsent(redisKey, "1");
if (Boolean.TRUE.equals(firstTime)) {
vectorStore.add(documents);
System.out.println("向量数据初始化完成");
} else {
System.out.println("向量数据已存在,跳过初始化");
}
}
}
核心防重流程:
- 使用
TextReader获取源文件元数据中的路径(source)。 - 对 source 进行 MD5 哈希,生成固定长度的唯一标识。
- 拼接 Redis Key
vector-xxx:md5值。 - 调用
setIfAbsent尝试写入,若返回true表示键不存在,执行向量写入;返回false表示已写入过,直接跳过。
六、RAG 检索增强接口实现与原理解析
6.1 接口实现代码
Controller 层使用 ChatClient 与 RetrievalAugmentationAdvisor 实现 RAG 问答。
@RestController
public class RagController {
@Resource(name = "qwenChatClient")
private ChatClient chatClient;
@Resource
private VectorStore vectorStore;
@GetMapping("/rag4aiops")
public Flux<String> rag(String msg) {
String systemInfo = """
你是一个运维工程师,按照给出的编码给出对应故障解释,否则回复找不到信息。
""";
// 构建检索增强顾问:指定向量数据库检索器
RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(
VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.build()
)
.build();
return chatClient.prompt()
.system(systemInfo)
.user(msg)
.advisors(advisor) // 注入 RAG 顾问
.stream()
.content();
}
}
流程说明:
- 用户传入故障编码
msg。 RetrievalAugmentationAdvisor调用VectorStoreDocumentRetriever从 Redis 向量库中检索与msg语义最相似的文档片段。- 将检索到的文档片段作为上下文,与 system prompt 和 user message 一起发送给 Qwen 模型。
- 模型根据上下文生成自然语言回答,并以流式方式返回。
6.2 为什么检索不到时会输出"找不到信息"?
在测试接口时,如果输入的知识库中不存在的编码(例如 XYZ),你会发现模型会直接返回"找不到对应信息",而不是胡编乱造。这背后是一整套 严格 RAG 机制在起作用。
完整 RAG 流程回顾
整个 RAG(检索增强生成)链路如下:
- 用户提问:例如问"XYZ 是什么故障"。
- 向量检索:把问题文本向量化,去 Redis 向量库做相似度搜索。
- 上下文组装:将检索回来的文档片段与原始问题一起拼入 Prompt。
- 大模型回答:模型只被允许基于 Prompt 中的文档片段作答。
产生"找不到信息"的三大原因
① Prompt 强约束(最核心)
RAG 系统会在 System Prompt 中明确限制模型的行为,典型示例:
你只能使用【参考文档】里面提供的内容回答用户问题。
如果参考文档为空,或者没有和问题相关的信息,请直接回复:找不到对应信息,不要编造、不要使用自己的固有知识。
【参考文档】
{retrieve_context}
用户问题:{user_query}
当向量库检索不到任何内容时,{retrieve_context} 就会被替换为空字符串。大模型读到"参考文档为空",就会严格遵守指令,输出"找不到对应信息",禁止调用模型自身的训练知识。
很多新手会困惑:大模型明明知道 "XYZ" 可能代表什么,为什么不回答?------就是因为 Prompt 锁死了,不允许使用内部知识,只能基于检索回来的上下文。
② 向量库确实没有匹配片段
如果知识库文件中根本没有与用户提问相关的主题(比如根本没有 "XYZ" 这个编码),那么:
- 文档切片、向量化入库后,Redis 里就不存在与 "XYZ" 语义相近的向量;
- 用户提问向量化后去 Redis 做近似搜索,返回的文档列表为空;
- 组装 Prompt 时,参考文档部分就是空的,最终触发"找不到信息"。
③ 相似度阈值过滤
向量检索通常会设置一个相似度阈值(例如 0.7)。即使向量库里有其他文档,但如果它们与用户问题的向量相似度全部低于阈值,这些文档也会被直接过滤掉,最终交给模型的上下文仍然是空。
Spring AI Alibaba 的
VectorStoreDocumentRetriever支持配置similarityThreshold,如果设置了该值,那么只有相似度高于阈值的文档才会被采用。
两种模式对比
| 模式 | 行为 |
|---|---|
| 严格 RAG(生产常用) | 检索不到内容 → 返回 "找不到信息",禁止幻觉,不瞎编 |
| 宽松 RAG | 检索不到内容,允许大模型使用自身知识回答问题 |
本文的示例就是典型的 严格 RAG,通过 System Prompt 做了强限制。这样做的好处是:
- 杜绝大模型幻觉,回答可控、可追溯;
- 所有答案都基于企业真实文档,适合生产环境。
七、测试与验证
1. 启动服务
确保 Redis Stack 已启动,环境变量 aliQwen-api 已设置,然后运行 Spring Boot 应用。启动日志中应看到"向量数据初始化完成"。
2. 接口调用
-
查询正常编码:
GET http://localhost:8012/rag4aiops?msg=00000
响应:系统运行正常,无故障。 -
查询已知故障:
GET http://localhost:8012/rag4aiops?msg=C2222
响应:编码C2222表示CPU温度过高,请检查散热系统。 -
查询未知编码:
GET http://localhost:8012/rag4aiops?msg=XYZ
响应:找不到对应的故障信息。
重启应用后再次调用接口,观察日志应输出"向量数据已存在,跳过初始化",且检索结果依旧准确,不会出现冗余重复。
八、总结
本文通过一个简单的运维问答场景,展示了如何利用 Spring AI Alibaba + Redis Stack 实现本地知识库的 RAG 应用。核心步骤包括:
- 文档预处理:读取、分割、向量化。
- 向量存储:利用 Redis Stack 存储向量索引。
- 检索增强生成 :通过
RetrievalAugmentationAdvisor自动完成检索与上下文增强。 - 防重初始化:使用 Redis 分布式锁思想,保证数据只加载一次。
- 严格 RAG 策略:通过 Prompt 约束和阈值控制,确保模型只基于知识库回答,杜绝幻觉。