
本案例将引导您构建一个基于 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: 9000
spring:
datasource:
url: jdbc:postgresql://127.0.0.1:5432/postgres
username: postgres
password: postgres
application:
name: rag-openai-dashscope-pgvector-example
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
ai:
openai:
api-key: ${AI_DASHSCOPE_API_KEY}
base-url: https://dashscope.aliyuncs.com/compatible-mode
chat:
options:
model: qwen-plus-latest
embedding:
options:
model: text-embedding-v3
dimensions: 1024
vectorstore:
pgvector:
table-name: mxy_rag_vector
initialize-schema: true
dimensions: 1024
index-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;
@Autowired
public 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
*/
@Override
public 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 要插入的文本内容
*/
@Override
public 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 处理结果消息
*/
@Override
public 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}
*/
@Override
public 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}
*/
@Override
public 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 知识库服务实例
*/
@Autowired
public 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-data
file=@document.pdf
响应示例:
成功处理文件 document.pdf,共生成 42 个文档片段
5.2 插入文本
直接将文本内容插入到向量库中:
POST /api/v1/knowledge-base/insert-text
Content-Type: application/x-www-form-urlencoded
content=文本内容
响应示例:
文本内容已成功插入
5.3 智能问答
基于知识库内容的阻塞式问答:
POST /api/v1/knowledge-base/chat
Content-Type: application/x-www-form-urlencoded
query=你的问题&topK=5
响应示例:
根据知识库内容,Spring AI 是一个用于 AI 工程的应用程序框架,它提供了与各种 AI 模型和服务集成的便捷方式...
5.4 流式问答
基于知识库内容的流式问答:
POST /api/v1/knowledge-base/chat-stream
Content-Type: application/x-www-form-urlencoded
query=你的问题&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 系统的回答质量,并根据评估结果优化系统参数。
- 多模态支持:扩展系统以支持图像、音频等多模态内容的处理和检索。
- 高级检索策略:实现更复杂的检索策略,如混合检索、多阶段检索等,提高检索精度。