Spring AI Alibaba Graph框架实现RAG私有知识库问答

目录

前言

[一、先聊背景:为什么需要 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盘"。用户提问时,系统先去向量库里找最相关的文档片段,然后把这些片段和问题一起"喂"给大模型,让它基于参考资料回答。这回要做的就是把这个流程完整落地。

二、最终目标

完成后你将拥有:

  1. 一个文件上传接口,支持 PDF、DOCX、TXT 等格式

  2. 文档自动经过 Tika 解析 → Token 切割 → 向量化 → 存入 Redis Stack

  3. 一个问答接口,提问时自动检索私有知识库,由大模型基于文档内容回答

  4. 一条完整的 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
  1. 访问 Docker 官网,下载 Docker Desktop for Windows (AMD64) 安装包-。

  2. 双击 Docker Desktop Installer.exe 运行安装程序。在安装向导中,务必勾选"Use WSL 2 instead of Hyper-V (recommended)" 选项-。

  3. 按照提示完成安装,安装完成后重启电脑。

  4. 从开始菜单启动 Docker Desktop。首次启动会提示接受服务条款,点击 Accept。等待任务栏托盘中的 Docker 图标变为稳定的绿色,表示 Docker 引擎已就绪。

  5. 打开 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文献,给出了回答,说明整套流程成功。

相关推荐
打工仔折腾 AI1 小时前
鸿蒙版ToDesk远控实测:蓝牙键鼠、双模式鼠标与跨端剪贴板怎么用
后端·python·华为·性能优化·计算机外设·harmonyos·ai agent 实战
从琳开始9891 小时前
[JavaEE]多线程初阶(下篇)------多线程案例(单例模式,阻塞队列,线程池,定时器)
java·单例模式·java-ee
乌暮2 小时前
# Spring 入门与 Spring MVC 实战指南:从零理解 IoC 到 RESTful 图书管理
java·开发语言·后端·学习·spring·mvc·restful
java1234_小锋2 小时前
Spring Boot 4.0 官宣:弃用 Undertow
java·spring boot·后端
jyOverQ2 小时前
Redis 主从复制、Sentinel 和 Cluster 到底有什么区别?
java·redis·sentinel
mldong2 小时前
给 Django Admin 加审批流:一行前端代码没写,298 行接上工作流引擎
后端·python·架构
高频因子挖掘机2 小时前
量化研究中,为什么拿到 Pandas 行情数据不等于完成数据接入?
后端·github·api
青春逝如流水2 小时前
Java 线程在内存模型(JMM)与 JVM 实现:规范与落地的多元路径
java·开发语言·jvm·多线程·ai文章
高频因子挖掘机3 小时前
股票 API 接入回测框架时最容易忽略的几个问题:复权、时间对齐与数据泄露
后端·github·api