目录
[一、先聊背景:为什么需要 RAG?](#一、先聊背景:为什么需要 RAG?)
[3.1 技术栈](#3.1 技术栈)
[3.2 安装与启动 Redis Stack](#3.2 安装与启动 Redis Stack)
[3.2.1 前置条件:启用 WSL 2](#3.2.1 前置条件:启用 WSL 2)
[3.2.2 安装 Docker Desktop](#3.2.2 安装 Docker Desktop)
[3.2.3 启动 Redis Stack 容器](#3.2.3 启动 Redis Stack 容器)
[3.2.4 验证 Redis Stack 是否正常运行](#3.2.4 验证 Redis Stack 是否正常运行)
[3.2.5 常用管理命令](#3.2.5 常用管理命令)
[四、Maven 依赖](#四、Maven 依赖)
[六、RedisVectorStore 配置类](#六、RedisVectorStore 配置类)
[七、文件上传 + 文档入库](#七、文件上传 + 文档入库)
[7.1 核心 Controller](#7.1 核心 Controller)
[7.2 这段代码做了什么](#7.2 这段代码做了什么)
[八、RAG 问答接口](#八、RAG 问答接口)
前言
这篇是 Spring AI 进阶系列的第三篇。在前两篇中,我们已经完成了 Spring Boot 3.3.4 集成 Spring AI Alibaba、使用 DashScope 大模型实现流式对话。但通用大模型有一个致命短板:它对我们的私有知识一无所知。本文将通过 RAG(检索增强生成)技术,配合 Redis Stack 向量数据库,手把手实现一个可上传文档、切割向量化、基于私有知识库回答的完整 RAG 系统。
一、先聊背景:为什么需要 RAG?
前面两篇我们成功把大模型接入了 Spring Boot,对话也跑通了。但很快就发现一个问题,你问它"我们公司最新的产品手册里有哪些功能",它完全答不上来。这不是模型不行,而是它的训练数据里根本没有你的私有文档。大模型就像一位知识渊博但从未读过你公司资料的专家,知识广度够但深度不够。
解决这个问题有两条路:
-
微调(Fine-tuning) :拿私有数据重新训练模型。成本高、周期长、每次数据更新都要重新训练。
-
RAG(Retrieval-Augmented Generation) :不改模型,而是在问答时先从私有知识库检索相关内容,拼接进 prompt 一起发给模型。
RAG 就像给大模型外挂了一个"知识U盘"。用户提问时,系统先去向量库里找最相关的文档片段,然后把这些片段和问题一起"喂"给大模型,让它基于参考资料回答。这回要做的就是把这个流程完整落地。
二、最终目标
完成后你将拥有:
-
一个文件上传接口,支持 PDF、DOCX、TXT 等格式
-
文档自动经过 Tika 解析 → Token 切割 → 向量化 → 存入 Redis Stack
-
一个问答接口,提问时自动检索私有知识库,由大模型基于文档内容回答
-
一条完整的 ETL 数据流:上传 → 解析 → 切割 → 向量化 → 存储 → 检索 → 增强回答
三、环境准备
3.1 技术栈
| 组件 | 版本 |
|---|---|
| JDK | 17 |
| Spring Boot | 3.3.4 |
| Spring AI | 1.0.3 |
| Spring AI Alibaba | 1.0.0.4 |
| Redis | Redis Stack(Docker 运行) |
| 对话模型 | deepseek-v4-flash(阿里云 MaaS) |
| 嵌入模型 | text-embedding-v3 |
3.2 安装与启动 Redis Stack
RAG 需要向量检索能力,普通 Redis 不行,必须用 Redis Stack,它内置了 RediSearch 模块,支持向量索引和相似度搜索。在 Windows 上运行 Redis Stack,官方推荐的方式是通过 Docker 容器-。下面从零开始,分步完成环境搭建。
3.2.1 前置条件:启用 WSL 2
Docker Desktop 在 Windows 11 上需要 WSL 2 作为后端-。如果你的系统尚未安装 WSL 2,以管理员身份打开 PowerShell,执行:
bash
wsl --install
安装完成后重启电脑。再次打开 PowerShell,验证 WSL 版本:
bash
wsl --version
确保 WSL 版本不低于 2.1.5-。如果版本过低,执行 wsl --update 升级。
3.2.2 安装 Docker Desktop
-
访问 Docker 官网,下载 Docker Desktop for Windows (AMD64) 安装包-。
-
双击
Docker Desktop Installer.exe运行安装程序。在安装向导中,务必勾选"Use WSL 2 instead of Hyper-V (recommended)" 选项-。 -
按照提示完成安装,安装完成后重启电脑。
-
从开始菜单启动 Docker Desktop。首次启动会提示接受服务条款,点击 Accept。等待任务栏托盘中的 Docker 图标变为稳定的绿色,表示 Docker 引擎已就绪。
-
打开 PowerShell,验证 Docker 是否安装成功:
bash
docker --version
若正常输出版本号(如 Docker version 27.x.x),说明安装成功。
3.2.3 启动 Redis Stack 容器
Docker Desktop 就绪后,在 PowerShell 中执行以下命令,一键拉取并启动 Redis Stack:
bash
docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 -e REDIS_ARGS="--requirepass 123456" redis/redis-stack:latest
参数含义:
| 参数 | 说明 |
|---|---|
-d |
后台运行容器 |
--name redis-stack |
容器命名为 redis-stack,便于后续管理 |
-p 6379:6379 |
将宿主机 6379 端口映射到容器,Spring Boot 通过此端口连接 Redis- |
-p 8001:8001 |
映射 RedisInsight 可视化界面端口,浏览器访问 http://localhost:8001 即可使用- |
-e REDIS_ARGS="--requirepass 123456" |
设置 Redis 密码为 123456 |
redis/redis-stack:latest |
使用官方 Redis Stack 镜像,包含 RediSearch 等模块- |
首次执行时 Docker 会从 Docker Hub 拉取镜像(约 500MB),需要等待几分钟。命令返回一长串容器 ID 表示启动成功。
3.2.4 验证 Redis Stack 是否正常运行
依次执行以下命令,确认服务一切正常:
bash
# 1. 查看容器状态
docker ps
应看到 redis-stack 容器状态为 Up,且 PORTS 列有 0.0.0.0:6379->6379/tcp 的箭头映射。
bash
# 2. 测试 Redis 是否响应
docker exec -it redis-stack redis-cli -a 123456 ping
返回 PONG 即正常。
bash
# 3. 验证 RediSearch 模块已加载
docker exec -it redis-stack redis-cli -a 123456 MODULE LIST
输出中应包含 search 模块。
bash
# 4. 测试向量索引命令
docker exec -it redis-stack redis-cli -a 123456 FT._LIST
返回空列表(不报错)即正常。如果报 ERR unknown command 'FT._LIST',说明连接到了普通 Redis,需要检查 6379 端口是否被本地 Redis 服务占用-。
3.2.5 常用管理命令
| 操作 | 命令 |
|---|---|
| 停止容器 | docker stop redis-stack |
| 启动已存在的容器 | docker start redis-stack |
| 查看日志 | docker logs redis-stack |
| 删除容器(数据丢失) | docker rm -f redis-stack |
| 进入容器内 Redis CLI | docker exec -it redis-stack redis-cli -a 123456 |
⚠️ 踩坑提醒 :如果你之前安装过普通 Redis 服务(比如通过 MSI 安装包),它可能也在占用 6379 端口。Spring Boot 会连接到那个普通 Redis 上,导致
FT._LIST报unknown command。解决方法:停掉本地 Redis 服务(在"服务"管理器中停止 Redis 服务),或把 Docker 映射到其他端口(如-p 6380:6379),并同步修改application.properties中的spring.data.redis.port。
四、Maven 依赖
现有 pom.xml 基础上,需要补充以下依赖:
bash
<!-- Spring AI Redis 向量存储自动配置 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI Tika 文档读取器 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring AI RAG Advisor(QuestionAnswerAdvisor) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>
五、核心配置
在 application.properties 中追加:
bash
# ===================== Redis 连接 =====================
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.password=123456
spring.data.redis.database=0
# ===================== Redis 向量库 =====================
# 首次启动自动创建索引
spring.ai.vectorstore.redis.initialize-schema=true
# 索引名
spring.ai.vectorstore.redis.index-name=rag-idx
# 向量数据键前缀
spring.ai.vectorstore.redis.prefix=rag:
# ===================== 对话模型(你现有的配置) =====================
spring.ai.openai.api-key=sk-你的Key
spring.ai.openai.base-url=https://llm-o4uz6dvcl1e8uyxv.cn-beijing.maas.aliyuncs.com/compatible-mode
spring.ai.openai.chat.options.model=deepseek-v4-flash
# ===================== 嵌入模型(RAG 必需) =====================
# ★ 关键:embedding 单独指定 base-url,因为你的 MaaS 端点可能不支持 /v1/embeddings
spring.ai.openai.embedding.base-url=https://dashscope.aliyuncs.com/compatible-mode
spring.ai.openai.embedding.options.model=text-embedding-v3
这里有一个非常容易踩的坑 :如果你的 chat 和 embedding 共用同一个 base-url,而那个端点恰好只支持 /chat/completions 不支持 /embeddings,调用 vectorStore.add() 时会直接报 404。解决办法就是单独为 embedding 指定一个可用的 base-url-。
六、RedisVectorStore 配置类
Spring AI 的 RedisVectorStoreAutoConfiguration 有一个硬性条件:容器中必须存在 JedisConnectionFactory Bean。Spring Boot 默认使用 Lettuce 客户端,不会自动创建 Jedis 工厂,所以需要手动补一个-。
bash
@Configuration
public class RedisVectorStoreConfig {
@Value("${spring.data.redis.host:127.0.0.1}")
private String host;
@Value("${spring.data.redis.port:6379}")
private int port;
@Value("${spring.data.redis.password:}")
private String password;
@Value("${spring.data.redis.database:0}")
private int database;
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(host);
config.setPort(port);
config.setDatabase(database);
if (password != null && !password.isEmpty()) {
config.setPassword(password);
}
return new JedisConnectionFactory(config);
}
}
注意 :不能简单写 new JedisConnectionFactory(),那样不会读取 application.properties 里的密码配置,会导致 NOAUTH Authentication required。
七、文件上传 + 文档入库
7.1 核心 Controller
bash
@RestController
@RequestMapping("/api/rag")
public class RagUploadController {
private static final Logger log = LogManager.getLogger(HelloContoller.class);
private final VectorStore vectorStore;
public RagUploadController(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 上传文件 → Tika 提取文本 → TokenTextSplitter 切割 → 存入 Redis 向量库
*/
@PostMapping("/upload")
public ResponseEntity<String> uploadAndIndex(@RequestParam("file") MultipartFile file) {
String originalName = file.getOriginalFilename();
try {
log.info("【1】开始保存临时文件");
File tempFile = File.createTempFile("rag_", "_" + originalName);
file.transferTo(tempFile);
tempFile.deleteOnExit();
log.info("【1】临时文件保存完成: {}", tempFile.getAbsolutePath());
log.info("【2】Tika 开始读取");
Resource resource = new FileSystemResource(tempFile);
TikaDocumentReader reader = new TikaDocumentReader(resource);
List<Document> documents = reader.get();
documents.forEach(doc -> doc.getMetadata().put("source", originalName));
log.info("【2】Tika 读取完成,文档数: {}", documents.size());
log.info("【3】开始切割");
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> chunks = splitter.apply(documents);
log.info("【3】切割完成,chunk 数: {}", chunks.size());
log.info("【4】开始写入向量库(最可能卡在这)");
vectorStore.add(chunks);
log.info("【4】向量库写入完成");
return ResponseEntity.ok("处理完成,chunk 数: " + chunks.size() + ",已存入 Redis");
} catch (Exception e) {
log.error("【X】上传处理失败", e);
return ResponseEntity.status(500).body(
"上传处理失败:" + e.getClass().getSimpleName() + " → " + e.getMessage()
);
}
}
}
7.2 这段代码做了什么
整个流程遵循 Spring AI 的 ETL Pipeline 模式:DocumentReader → DocumentTransformer → VectorStore-。
-
TikaDocumentReader:底层使用 Apache Tika,能从 PDF、DOCX、PPT、HTML 等上千种格式中提取纯文本-。
-
TokenTextSplitter:默认按 800 tokens 为一个块切分,块与块之间有重叠,避免语义在边界处断裂-。
-
vectorStore.add():自动调用 EmbeddingModel 把每个 chunk 转成向量,然后写入 Redis。
八、RAG 问答接口
bash
@RestController
@RequestMapping("/api/chat")
public class RagChatController {
private final ChatClient chatClient;
public RagChatController(ChatClient.Builder builder, VectorStore vectorStore) {
QuestionAnswerAdvisor ragAdvisor = QuestionAnswerAdvisor.builder(vectorStore)
.searchRequest(SearchRequest.builder()
.similarityThreshold(0.5) // 相似度阈值
.topK(5) // 返回最相似的 5 个 chunk
.build())
.build();
this.chatClient = builder
.defaultAdvisors(ragAdvisor)
.build();
}
@GetMapping("/ask")
public Map<String, String> ask(@RequestParam("message") String message) {
String answer = chatClient.prompt()
.user(message)
.call()
.content();
return Map.of("answer", answer);
}
}
注意 :similarityThreshold 和 topK 不能直接写在 QuestionAnswerAdvisor.builder() 上,它们属于 SearchRequest,需要通过 .searchRequest() 传入。SearchRequest 有默认值(topK 默认 4,similarityThreshold 默认 0.5),不传也能用。QuestionAnswerAdvisor 的工作原理:每次 prompt 时自动从 VectorStore 检索相关文档片段,拼接进 prompt 上下文后再发给模型。
九、最终完整结果测试
1.启动Docker 中 Redis stack,可以命令或者手动点击Actions中的三角按钮启动。

2.调用上传文件接口,完成PDF切割,并存入Redis向量数据库

点击上传了一篇关于知识共享的文献,大体内容如图所示

通过Redis Desktop工具,查看存储的向量数据,可以看到存储成功了。

3.调用问答接口
给出的问题,"什么是知识共享?",根据上传的PDF文献,给出了回答,说明整套流程成功。
