101-Spring AI Alibaba RAG 示例

本案例将引导您构建一个基于 Spring AI 的 RAG(检索增强生成)服务器,使用阿里云通义千问模型(OpenAI兼容模式)和 PostgreSQL + pgvector 向量存储。

1. 案例目标

我们将创建一个包含以下核心功能的 Web 应用:

  1. 文档上传与处理:支持 PDF、Word、TXT 等多格式文档的上传和向量化处理。
  2. 文本内容插入:支持直接将文本内容插入到向量数据库中。
  3. 相似性搜索:基于向量存储进行相似性搜索,检索相关文档。
  4. 智能问答:基于知识库内容的智能问答,支持阻塞式和流式两种响应方式。

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 环境准备

  1. 设置 API Key

    复制代码
    # Windows
    set AI_DASHSCOPE_API_KEY=your_api_key
    
    # Linux/Mac
    export AI_DASHSCOPE_API_KEY=your_api_key
  2. 确保 PostgreSQL 已安装 pgvector 扩展

    复制代码
    CREATE EXTENSION IF NOT EXISTS vector;
  3. 配置数据库连接 :修改 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 系统的回答质量,并根据评估结果优化系统参数。
  • 多模态支持:扩展系统以支持图像、音频等多模态内容的处理和检索。
  • 高级检索策略:实现更复杂的检索策略,如混合检索、多阶段检索等,提高检索精度。
相关推荐
小Tomkk3 小时前
用 ai 给UI 页面打分 (提示词)
人工智能·ui
乾坤瞬间3 小时前
【Java后端进行ai coding实践系列二】记住规范,记住内容,如何使用iflow进行上下文管理
java·开发语言·ai编程
迦蓝叶3 小时前
JAiRouter v1.1.0 发布:把“API 调没调通”从 10 分钟压缩到 10 秒
java·人工智能·网关·openai·api·协议归一
不知道累,只知道类3 小时前
记一次诡异的“偶发 404”排查:CDN 回源到 OSS 导致 REST API 失败
java·云原生
lang201509283 小时前
Spring数据库连接控制全解析
java·数据库·spring
why技术3 小时前
1K+Star的开源项目能给一个在校大学生带来什么?
前端·人工智能·后端
jinmo_C++3 小时前
数据结构_深入理解堆(大根堆 小根堆)与优先队列:从理论到手撕实现
java·数据结构·算法
哲此一生9843 小时前
YOLO11追踪简单应用
人工智能·pytorch·深度学习