101-Spring AI Alibaba RAG 示例

本案例将引导您构建一个基于 Spring AI 的 RAG(检索增强生成)服务器,使用阿里云通义千问模型(OpenAI兼容模式)和 PostgreSQL + pgvector 向量存储。
1. 案例目标
我们将创建一个包含以下核心功能的 Web 应用:
- 文档上传与处理:支持 PDF、Word、TXT 等多格式文档的上传和向量化处理。
- 文本内容插入:支持直接将文本内容插入到向量数据库中。
- 相似性搜索:基于向量存储进行相似性搜索,检索相关文档。
- 智能问答:基于知识库内容的智能问答,支持阻塞式和流式两种响应方式。
2. 技术栈与核心依赖
- Spring Boot 3.4.5
- Spring AI 1.0.0
- Java 17
- PostgreSQL + pgvector
- 阿里云通义千问(OpenAI兼容模式)
在 pom.xml 中,需要引入以下核心依赖:
<dependencies><!-- Spring Web 用于构建 RESTful API --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>3.4.0</version></dependency><!-- Spring AI OpenAI Starter --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId></dependency><!-- pgvector 向量存储 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pgvector-store</artifactId></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-autoconfigure-vector-store-pgvector</artifactId></dependency><!-- PDF 文档读取器 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pdf-document-reader</artifactId></dependency><!-- Tika 文档读取器 --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-tika-document-reader</artifactId></dependency>
</dependencies><!-- 依赖管理 -->
<dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>3. 项目配置
在 src/main/resources/application.yml 文件中,配置应用信息、数据库连接和 AI 模型参数。
server:port: 9000spring:datasource:url: jdbc:postgresql://127.0.0.1:5432/postgresusername: postgrespassword: postgresapplication:name: rag-openai-dashscope-pgvector-exampleservlet:multipart:max-file-size: 10MBmax-request-size: 10MBai:openai:api-key: ${AI_DASHSCOPE_API_KEY}base-url: https://dashscope.aliyuncs.com/compatible-modechat:options:model: qwen-plus-latestembedding:options:model: text-embedding-v3dimensions: 1024vectorstore:pgvector:table-name: mxy_rag_vectorinitialize-schema: truedimensions: 1024index-type: hnsw重要提示:请将 AI_DASHSCOPE_API_KEY 环境变量设置为你从阿里云获取的有效 API Key。同时确保 PostgreSQL 已安装 pgvector 扩展。
4. 核心代码实现
4.1 主应用类
创建 Spring Boot 主应用类:
package com.alibaba.cloud.ai.example.rag;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class OpenAiDashscopeRagApplication {public static void main(String[] args) {SpringApplication.run(OpenAiDashscopeRagApplication.class, args);}
}4.2 服务接口
定义知识库服务接口:
package com.alibaba.cloud.ai.example.rag.service;import org.springframework.ai.document.Document;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;import java.util.List;/*** 知识库管理操作的服务接口。*/
public interface KnowledgeBaseService {/*** 将字符串内容插入到指定的向量库中。* @param content 要插入的文本内容*/void insertTextContent(String content);/*** 根据文件类型动态选择Reader加载文件到知识库。* 支持的文件类型:PDF、Word、TXT、Text等** @param file 上传的文件* @return 处理结果信息*/String loadFileByType(MultipartFile file);/*** 基于查询在指定业务类型中搜索相似文档。** @param query 查询字符串* @param topK 返回的相似文档数量* @return 相似文档列表*/List<Document> similaritySearch(String query, int topK);/*** 阻塞式LLM对话接口,根据业务类型获取相关知识库数据进行问答。** @param query 用户查询问题* @param topK 检索的相关文档数量* @return LLM生成的回答*/String chatWithKnowledge(String query, int topK);/*** 流式LLM对话接口,根据业务类型获取相关知识库数据进行问答。** @param query 用户查询问题* @param topK 检索的相关文档数量* @return 流式返回的LLM回答*/Flux<String> chatWithKnowledgeStream(String query, int topK);
}4.3 服务实现
实现知识库服务:
package com.alibaba.cloud.ai.example.rag.service.impl;import com.alibaba.cloud.ai.example.rag.service.KnowledgeBaseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.stream.Collectors;/*** 知识库服务实现类 */
@Service
public class KnowledgeBaseServiceImpl implements KnowledgeBaseService {private static final Logger logger = LoggerFactory.getLogger(KnowledgeBaseServiceImpl.class);private final VectorStore vectorStore;private final ChatClient chatClient;@Autowiredpublic KnowledgeBaseServiceImpl(VectorStore vectorStore, @Qualifier("openAiChatModel")ChatModel chatModel) {this.vectorStore = vectorStore;this.chatClient = ChatClient.builder(chatModel).defaultAdvisors(new SimpleLoggerAdvisor()).defaultOptions(OpenAiChatOptions.builder().temperature(0.7).build()).build();}/*** 相似性搜索* @param query 查询字符串* @param topK 返回的相似文档数量* @return*/@Overridepublic List<Document> similaritySearch(String query, int topK) {Assert.hasText(query, "查询不能为空");logger.info("执行相似性搜索: query={}, topK={}", query, topK);// 创建业务类型过滤器SearchRequest searchRequest = SearchRequest.builder().query(query).topK(topK).build();List<Document> results = vectorStore.similaritySearch(searchRequest);logger.info("相似性搜索完成,找到 {} 个相关文档", results.size());return results;}/*** 将文本内容插入到向量存储中。** @param content 要插入的文本内容*/@Overridepublic void insertTextContent(String content) {Assert.hasText(content, "文本内容不能为空");logger.info("插入文本内容到向量存储: contentLength={}", content.length());// 创建文档并设置ID和元数据Document document = new Document(content);// 使用文本分割器处理长文本List<Document> splitDocuments = new TokenTextSplitter().apply(List.of(document));// 添加到向量存储vectorStore.add(splitDocuments);logger.info("文本内容插入完成: 生成文档片段数: {}", splitDocuments.size());}/*** 根据文件类型加载文件到向量存储中。** @param file 要上传的文件* @return 处理结果消息*/@Overridepublic String loadFileByType(MultipartFile file) {Assert.notNull(file, "文件不能为空");logger.info("开始处理文件上传: fileName={}, fileSize={}", file.getOriginalFilename(), file.getSize());try {// 创建临时文件Path tempFile = Files.createTempFile("upload_", "_" + file.getOriginalFilename());Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);List<Document> documents;String fileName = file.getOriginalFilename();// 根据文件类型选择合适的文档读取器if (fileName.toLowerCase().endsWith(".pdf")) {// 使用PDF读取器PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(tempFile.toUri().toString());documents = pdfReader.get();logger.info("使用PDF读取器处理文件: {}", fileName);} else {// 使用Tika读取器处理其他类型文件TikaDocumentReader tikaReader = new TikaDocumentReader(tempFile.toUri().toString());documents = tikaReader.get();logger.info("使用Tika读取器处理文件: {}", fileName);}// 添加文档到向量存储vectorStore.add(documents);// 清理临时文件Files.deleteIfExists(tempFile);logger.info("文件处理完成: fileName={}, documentsCount={}", fileName, documents.size());return String.format("成功处理文件 %s,共生成 %d 个文档片段", fileName, documents.size());} catch (IOException e) {logger.error("文件处理失败: fileName={}, error={}", file.getOriginalFilename(), e.getMessage(), e);return "文件处理失败: " + e.getMessage();}}/*** {@inheritDoc}*/@Overridepublic String chatWithKnowledge(String query, int topK) {Assert.hasText(query, "查询问题不能为空");logger.info("开始知识库对话,查询: '{}'", query);// 检索相关文档List<Document> relevantDocs = similaritySearch(query, topK);if (relevantDocs.isEmpty()) {logger.warn("未找到与查询相关的文档");return "抱歉,我在知识库中没有找到相关信息来回答您的问题。";}// 构建上下文String context = relevantDocs.stream().map(Document::getText).collect(Collectors.joining("\n\n"));// 构建提示词String prompt = String.format("基于以下知识库内容回答用户问题。如果知识库内容无法回答问题,请明确说明。\n\n" + "知识库内容:\n%s\n\n" + "用户问题:%s\n\n" + "请基于上述知识库内容给出准确、有用的回答:", context, query);// 调用LLM生成回答String answer = chatClient.prompt(prompt).call().content();logger.info("知识库对话完成,查询: '{}'", query);return answer;}/*** {@inheritDoc}*/@Overridepublic Flux<String> chatWithKnowledgeStream(String query, int topK) {Assert.hasText(query, "查询问题不能为空");logger.info("开始流式知识库对话,查询: '{}'", query);try {// 检索相关文档List<Document> relevantDocs = similaritySearch(query, topK);if (relevantDocs.isEmpty()) {logger.warn("未找到与查询相关的文档");return Flux.just("抱歉,我在知识库中没有找到相关信息来回答您的问题。");}// 构建上下文String context = relevantDocs.stream().map(Document::getText).collect(Collectors.joining("\n\n"));// 构建提示词String prompt = String.format("基于以下知识库内容回答用户问题。如果知识库内容无法回答问题,请明确说明。\n\n" + "知识库内容:\n%s\n\n" + "用户问题:%s\n\n" + "请基于上述知识库内容给出准确、有用的回答:", context, query);// 调用LLM生成流式回答return chatClient.prompt(prompt).stream().content();} catch (Exception e) {logger.error("流式知识库对话失败,查询: '{}'", query, e);return Flux.just("对话过程中发生错误: " + e.getMessage());}}
}4.4 控制器
创建 REST API 控制器:
package com.alibaba.cloud.ai.example.rag.controller;import com.alibaba.cloud.ai.example.rag.service.KnowledgeBaseService;
import org.springframework.ai.document.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;import java.util.List;/*** 知识库管理操作的控制器。* 基于业务类型进行知识库管理,支持广告和AIGC两个业务类型。*/
@RestController
@RequestMapping("/api/v1/knowledge-base")
public class KnowledgeBaseController {private final KnowledgeBaseService knowledgeBaseService;/*** 构造一个新的知识库控制器。** @param knowledgeBaseService 知识库服务实例*/@Autowiredpublic KnowledgeBaseController(KnowledgeBaseService knowledgeBaseService) {this.knowledgeBaseService = knowledgeBaseService;}/*** 将字符串内容插入到向量库中。** @param content 要插入的文本内容* @return 表示成功或失败的响应实体*/@GetMapping("/insert-text")public ResponseEntity<String> insertTextContent(@RequestParam("content") String content) {if (content == null || content.trim().isEmpty()) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("文本内容是必需的");}try {knowledgeBaseService.insertTextContent(content);return ResponseEntity.ok("文本内容已成功插入");} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("插入文本内容失败: " + e.getMessage());}}/*** 根据文件类型动态选择Reader加载文件到知识库。* 支持的文件类型:PDF、Word、TXT、Text等** @param file 上传的文件* @return 表示成功或失败的响应实体*/@PostMapping("/upload-file")public ResponseEntity<String> uploadFileByType(@RequestParam("file") MultipartFile file) {if (file.isEmpty()) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("文件为空");}try {String result = knowledgeBaseService.loadFileByType(file);return ResponseEntity.ok(result);} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件上传失败: " + e.getMessage());}}/*** 在指定业务类型的知识库中执行相似性搜索。** @param query 搜索查询* @param topK 要检索的相似文档数量(默认为5)* @return 包含相似文档列表或错误消息的响应实体*/@GetMapping("/search")public ResponseEntity<?> similaritySearch(@RequestParam("query") String query,@RequestParam(value = "topK", defaultValue = "5") int topK) {if (query == null || query.trim().isEmpty()) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("查询内容是必需的");}if (topK <= 0) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("topK必须是正整数");}try {List<Document> results = knowledgeBaseService.similaritySearch(query, topK);return ResponseEntity.ok(results);} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("相似性搜索过程中发生错误: " + e.getMessage());}}/*** 阻塞式LLM对话接口,根据业务类型获取相关知识库数据进行问答。** @param query 用户查询问题* @param topK 检索的相关文档数量(默认为5)* @return LLM生成的回答*/@GetMapping("/chat")public ResponseEntity<String> chatWithKnowledge(@RequestParam("query") String query,@RequestParam(value = "topK", defaultValue = "5") int topK) {if (query == null || query.trim().isEmpty()) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("查询问题是必需的");}if (topK <= 0) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("topK必须是正整数");}try {String answer = knowledgeBaseService.chatWithKnowledge(query, topK);return ResponseEntity.ok(answer);} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("对话过程中发生错误: " + e.getMessage());}}/*** 流式LLM对话接口,根据业务类型获取相关知识库数据进行问答。** @param query 用户查询问题* @param topK 检索的相关文档数量(默认为5)* @return 流式返回的LLM回答*/@GetMapping(value = "/chat-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public ResponseEntity<Flux<String>> chatWithKnowledgeStream(@RequestParam("query") String query,@RequestParam(value = "topK", defaultValue = "5") int topK) {if (query == null || query.trim().isEmpty()) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Flux.just("查询问题是必需的"));}if (topK <= 0) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Flux.just("topK必须是正整数"));}try {Flux<String> answerStream = knowledgeBaseService.chatWithKnowledgeStream(query, topK);return ResponseEntity.ok(answerStream);} catch (Exception e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Flux.just("流式对话过程中发生错误: " + e.getMessage()));}}
}5. API接口说明
本应用提供以下 REST API 接口:
5.1 上传文档
支持上传 PDF、Word、TXT 等格式的文档到知识库:
POST /api/v1/knowledge-base/upload-file
Content-Type: multipart/form-datafile=@document.pdf响应示例:
成功处理文件 document.pdf,共生成 42 个文档片段
5.2 插入文本
直接将文本内容插入到向量库中:
POST /api/v1/knowledge-base/insert-text
Content-Type: application/x-www-form-urlencodedcontent=文本内容响应示例:
文本内容已成功插入
5.3 智能问答
基于知识库内容的阻塞式问答:
POST /api/v1/knowledge-base/chat
Content-Type: application/x-www-form-urlencodedquery=你的问题&topK=5响应示例:
根据知识库内容,Spring AI 是一个用于 AI 工程的应用程序框架,它提供了与各种 AI 模型和服务集成的便捷方式...
5.4 流式问答
基于知识库内容的流式问答:
POST /api/v1/knowledge-base/chat-stream
Content-Type: application/x-www-form-urlencodedquery=你的问题&topK=5响应示例:
data: 根据
data: 知识库
data: 内容
data: ,
data: Spring
data: AI
data: 是
data: 一个
data: 用于
data: AI
data: 工程
data: 的
data: 应用程序
data: 框架
data: ...
5.5 相似性搜索
在知识库中搜索相似文档:
GET /api/v1/knowledge-base/search?query=搜索内容&topK=5响应示例:
[{"content": "Spring AI 是一个用于 AI 工程的应用程序框架...","metadata": {...}},{"content": "Spring AI 提供了与各种 AI 模型和服务的集成...","metadata": {...}}
]6. 运行与测试
6.1 环境准备
- 设置 API Key:
# Windows set AI_DASHSCOPE_API_KEY=your_api_key# Linux/Mac export AI_DASHSCOPE_API_KEY=your_api_key - 确保 PostgreSQL 已安装 pgvector 扩展:
CREATE EXTENSION IF NOT EXISTS vector; - 配置数据库连接:修改
application.yml中的数据库连接信息。
6.2 启动应用
mvn spring-boot:run应用启动在 http://localhost:9000
6.3 测试示例
测试 1:上传文档
使用 curl 上传 PDF 文档:
curl -X POST -F "file=@document.pdf" http://localhost:9000/api/v1/knowledge-base/upload-file测试 2:插入文本
使用 curl 插入文本内容:
curl -X POST -d "content=Spring AI 是一个用于 AI 工程的应用程序框架" http://localhost:9000/api/v1/knowledge-base/insert-text测试 3:智能问答
使用 curl 进行智能问答:
curl -X POST -d "query=什么是 Spring AI?" http://localhost:9000/api/v1/knowledge-base/chat测试 4:流式问答
使用 curl 进行流式问答:
curl -X POST -d "query=什么是 Spring AI?" http://localhost:9000/api/v1/knowledge-base/chat-stream测试 5:相似性搜索
使用 curl 进行相似性搜索:
curl "http://localhost:9000/api/v1/knowledge-base/search?query=Spring AI 框架"7. 实现思路与扩展建议
7.1 实现思路
本案例的核心思想是"检索增强生成"(Retrieval-Augmented Generation, RAG)。具体实现包括:
- 文档处理:使用 Spring AI 的文档读取器(PDF、Tika)读取不同格式的文档,并使用文本分割器将长文档分割成适合处理的片段。
- 向量化存储:使用阿里云的 text-embedding-v3 模型将文本片段转换为向量,并存储在 PostgreSQL + pgvector 中。
- 相似性搜索:根据用户查询,在向量数据库中检索最相关的文档片段。
- 增强生成:将检索到的相关文档作为上下文,结合用户问题,生成更加准确和有针对性的回答。
7.2 扩展建议
- 多业务类型支持:可以扩展系统,支持多个业务类型的知识库,通过元数据区分不同类型的文档。
- 文档预处理优化:增加文档预处理步骤,如去除噪声、提取关键信息等,提高检索质量。
- 缓存机制:为频繁查询的问题添加缓存机制,减少向量数据库的查询压力,提高响应速度。
- 评估与优化:建立评估体系,定期评估 RAG 系统的回答质量,并根据评估结果优化系统参数。
- 多模态支持:扩展系统以支持图像、音频等多模态内容的处理和检索。
- 高级检索策略:实现更复杂的检索策略,如混合检索、多阶段检索等,提高检索精度。
