Spring AI - 使用向量数据库实现检索式AI对话

Spring AI - 使用向量数据库实现检索式AI对话

 Spring AI 并不仅限于针对大语言模型对话API进行了统一封装,它还可以通过简单的方式实现LangChain的一些功能。本篇将带领读者实现一个简单的检索式AI对话接口。

一、需求背景

 在一些场景下,我们想让AI根据我们提供的数据进行回复。因为对话有最大Token的限制,因此很多场景下我们是无法直接将所有的数据发给AI的,一方面在数据量很大的情况下,会突破Token的限制,另一方面,在不突破Token限制的情况下也会有不必要的对话费用开销。因此我们如何在花费最少费用的同时又能让AI更好的根据我们提供的数据进行回复是一个非常关键的问题。针对这一问题,我们可以采用数据向量化的方式来解决。

二、实现原理

将我们个人数据存储到向量数据库中。然后,在用户想AI发起对话之前,首先从向量数据库中检索一组相似的文档。然后,将这些文档作为用户问题的上下文,并与用户的对话一起发送到 AI 模型,从而实现精确性的回复。这种方式称为检索增强生成(RAG)

第一步:数据向量化

 我们有很多种方式将数据向量化,最简单的就是通过调用第三方API来实现。以OpenAI的API为例,它提供了 https://api.openai.com/v1/embeddings 接口,通过请求该接口可以获取某段文本的向量化的数据。具体可参考官方API介绍:Create embeddings。在Spring AI中,我们不必调用该接口手动进行向量化处理,在存储到向量数据库的时候,Spring AI会自动调用的。

img.png

第二步:向量存储及检索

 在Spring AI中有一个VectorStore抽象接口,该接口定义了Spring AI与向量数据库的交互操作,我们只需通过简单的向量数据库的配置即可使用该接口对向量数据库进行操作。

public interface VectorStore {

    void add(List<Document> documents);

    Optional<Boolean> delete(List<String> idList);

    List<Document> similaritySearch(String query);

    List<Document> similaritySearch(SearchRequest request);
}

 向量数据库(Vector Database)是一种特殊类型的数据库,在人工智能应用中发挥着重要作用。在向量数据库中,查询操作与传统的关系数据库不同。它们是执行相似性搜索,而不是精确匹配。当给定向量作为查询时,向量数据库返回与查询向量“相似”的向量。通过这种方式,我们就能将个人的数据与AI模型进行集成。`

 常见的向量数据库有:ChromaMilvusPgvectorRedisNeo4j等。

三、代码实现

 本篇将实现基于ChatGPT的RAG和上传PDF文件存储至向量数据库的接口,向量数据库使用Pgvector。Pgvector是基于PostgreSQL进行的扩展,可以存储和检索机器学习过程中生成的embeddings。

源码已上传至GitHub: https://github.com/NingNing0111/vector-database-demo

版本信息

  • JDK >= 17
  • Spring Boot >= 3.2.2
  • Spring AI = 0.8.0-SNAPSHOT

1. 安装Pgvector

 Pgvector将使用Docker安装。docker-compose.yml文件如下:

version: '3.7'
services:
  postgres:
    image: ankane/pgvector:v0.5.0
    restart: always
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=vector_store
      - PGPASSWORD=postgres
    logging:
      options:
        max-size: 10m
        max-file: "3"
    ports:
      - '5432:5432'
    healthcheck:
      test: "pg_isready -U postgres -d vector_store"
      interval: 2s
      timeout: 20s
      retries: 10

2. 创建Spring项目,添加依赖

 Spring 项目的创建过程略,pom.xml核心内容如下:

	<properties>
		<java.version>17</java.version>
        <!--  Spring AI的版本信息  -->
		<spring-ai.version>0.8.0-SNAPSHOT</spring-ai.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

		<!-- 使用OpenAI -->
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
			<version>${spring-ai.version}</version>
		</dependency>
		<!-- 使用PGVector作为向量数据库 -->
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
			<version>${spring-ai.version}</version>
		</dependency>
		<!-- 引入PDF解析器 -->
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-pdf-document-reader</artifactId>
			<version>${spring-ai.version}</version>
		</dependency>
	</dependencies>

	<repositories>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
			<snapshots>
				<enabled>false</enabled>
			</snapshots>
		</repository>
		<repository>
			<id>spring-snapshots</id>
			<name>Spring Snapshots</name>
			<url>https://repo.spring.io/snapshot</url>
			<releases>
				<enabled>false</enabled>
			</releases>
		</repository>
	</repositories>

3. 配置API、Key、PGVector连接信息

server:
  port: 8801

spring:
  ai:
    openai:
      base-url: https://api.example.com
      api-key: sk-aec103e6cfxxxxxxxxxxxxxxxxxxxxxxx71da57a

  datasource:
    username: postgres
    password: postgres
    url: jdbc:postgresql://localhost/vector_store

4. 创建VectorStore和文本分割器TokenTextSplitter

 这里我创建了一个ApplicationConfig配置类

package com.ningning0111.vectordatabasedemo.config;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.PgVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
public class ApplicationConfig {

    /**
     * 向量数据库进行检索操作
     * @param embeddingClient
     * @param jdbcTemplate
     * @return
     */
    @Bean
    public VectorStore vectorStore(EmbeddingClient embeddingClient, JdbcTemplate jdbcTemplate){
        return new PgVectorStore(jdbcTemplate,embeddingClient);
    }

    /**
     * 文本分割器
     * @return
     */
    @Bean
    public TokenTextSplitter tokenTextSplitter() {
        return new TokenTextSplitter();
    }
}

5. 构建PDF存储服务层

 在service层下创建一个名为PdfStoreService的类,用于将PDF文件存储到向量数据库中。

package com.ningning0111.vectordatabasedemo.service;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.ParagraphPdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

@Service
@RequiredArgsConstructor
public class PdfStoreService {

    private final DefaultResourceLoader resourceLoader;
    private final VectorStore vectorStore;
    private final TokenTextSplitter tokenTextSplitter;

    /**
     * 根据PDF的页数进行分割
     * @param url
     */
    public void saveSourceByPage(String url){
        // 加载资源,需要本地路径的信息
        Resource resource = resourceLoader.getResource(url);
        // 加载PDF文件时的配置对象
        PdfDocumentReaderConfig loadConfig = PdfDocumentReaderConfig.builder()
                .withPageExtractedTextFormatter(
                        new ExtractedTextFormatter
                                .Builder()
                                .withNumberOfBottomTextLinesToDelete(3)
                                .withNumberOfTopPagesToSkipBeforeDelete(1)
                                .build()
                )
                .withPagesPerDocument(1)
                .build();

        PagePdfDocumentReader pagePdfDocumentReader = new PagePdfDocumentReader(resource, loadConfig);
        // 存储到向量数据库中
        vectorStore.accept(tokenTextSplitter.apply(pagePdfDocumentReader.get()));
    }

    /**
     * 根据PDF的目录(段落)进行划分
     * @param url
     */
    public void saveSourceByParagraph(String url){
        Resource resource = resourceLoader.getResource(url);

        PdfDocumentReaderConfig loadConfig = PdfDocumentReaderConfig.builder()
                .withPageExtractedTextFormatter(
                        new ExtractedTextFormatter
                                .Builder()
                                .withNumberOfBottomTextLinesToDelete(3)
                                .withNumberOfTopPagesToSkipBeforeDelete(1)
                                .build()
                )
                .withPagesPerDocument(1)
                .build();

        ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(
                resource,
                loadConfig
        );
        vectorStore.accept(tokenTextSplitter.apply(pdfReader.get()));
    }

    /**
     * MultipartFile对象存储,采用PagePdfDocumentReader
     * @param file
     */
    public void saveSource(MultipartFile file){
        try {
            // 获取文件名
            String fileName = file.getOriginalFilename();
            // 获取文件内容类型
            String contentType = file.getContentType();
            // 获取文件字节数组
            byte[] bytes = file.getBytes();
            // 创建一个临时文件
            Path tempFile = Files.createTempFile("temp-", fileName);
            // 将文件字节数组保存到临时文件
            Files.write(tempFile, bytes);
            // 创建一个 FileSystemResource 对象
            Resource fileResource = new FileSystemResource(tempFile.toFile());
            PdfDocumentReaderConfig loadConfig = PdfDocumentReaderConfig.builder()
                    .withPageExtractedTextFormatter(
                            new ExtractedTextFormatter
                                    .Builder()
                                    .withNumberOfBottomTextLinesToDelete(3)
                                    .withNumberOfTopPagesToSkipBeforeDelete(1)
                                    .build()
                    )
                    .withPagesPerDocument(1)
                    .build();
            PagePdfDocumentReader pagePdfDocumentReader = new PagePdfDocumentReader(fileResource, loadConfig);
            vectorStore.accept(tokenTextSplitter.apply(pagePdfDocumentReader.get()));
        }catch (IOException e){
            e.printStackTrace();
        }

    }
}

6. 构建对话服务

 创建ChatService类,该类提供了两种对话方式:不进行检索的普通对话模式对向量数据库进行检索的对话模式

package com.ningning0111.vectordatabasedemo.service;

import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class ChatService {

    // 系统提示词
    private final static String SYSTEM_PROMPT = """
            你需要使用文档内容对用户提出的问题进行回复,同时你需要表现得天生就知道这些内容,
            不能在回复中体现出你是根据给出的文档内容进行回复的,这点非常重要。
            
            当用户提出的问题无法根据文档内容进行回复或者你也不清楚时,回复不知道即可。
                    
            文档内容如下:
            {documents}
            
            """;

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    // 简单的对话,不对向量数据库进行检索
    public String simpleChat(String userMessage) {
        return chatClient.call(userMessage);
    }

    // 通过向量数据库进行检索
    public String chatByVectorStore(String message) {
        // 根据问题文本进行相似性搜索
        List<Document> listOfSimilarDocuments = vectorStore.similaritySearch(message);
        // 将Document列表中每个元素的content内容进行拼接获得documents
        String documents = listOfSimilarDocuments.stream().map(Document::getContent).collect(Collectors.joining());
        // 使用Spring AI 提供的模板方式构建SystemMessage对象
        Message systemMessage = new SystemPromptTemplate(SYSTEM_PROMPT).createMessage(Map.of("documents", documents));
        // 构建UserMessage对象
        UserMessage userMessage = new UserMessage(message);
        // 将Message列表一并发送给ChatGPT
        ChatResponse rsp = chatClient.call(new Prompt(List.of(systemMessage, userMessage)));
        return rsp.getResult().getOutput().getContent();
    }
}

7. 构建Controller层

ChatController提供了对话接口:

package com.ningning0111.vectordatabasedemo.controller;

import com.ningning0111.vectordatabasedemo.service.ChatService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/chat")
public class ChatController {

    private final ChatService chatService;

    @GetMapping("/simple")
    public String simpleChat(
            @RequestParam String message
    ){
        return chatService.simpleChat(message);
    }

    @GetMapping("/")
    public String chat(
            @RequestParam String message
    ){
        return chatService.chatByVectorStore(message);
    }
}

PdfUploadController提供了上传文件并保存到向量数据库中的接口

package com.ningning0111.vectordatabasedemo.controller;

import com.ningning0111.vectordatabasedemo.service.PdfStoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

@Controller
@RequestMapping("/api/v1/pdf")
@RequiredArgsConstructor
public class PdfUploadController {
    private final PdfStoreService pdfStoreService;

    @PostMapping("/upload")
    public void upload(
            @RequestParam MultipartFile file
    ){
        pdfStoreService.saveSource(file);
    }
}

三、效果图

 以24年合工大软工实训的pdf文件为例,通过向chatgpt提问与文档内容相关的问题。

 询问:2024年合工大软件工程实训中较难的项目有哪些?
img.png
 询问:介绍下2024年合工大软件工程实训中知识内容共享平台的需求
img_1.png
 询问:针对运营商云管平台工单处理子模块项目简介,给出这个项目的实现方案或技术栈
img_2.png

源码已上传至GitHub:https://github.com/NingNing0111/vector-database-demo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/383214.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【python】网络爬虫与信息提取--requests库

导学 当一个软件想获得数据&#xff0c;那么我们只有把网站当成api就可以 requests库:自动爬取HTML页面&#xff0c;自动网络请求提交 robots协议&#xff1a;网络爬虫排除标准&#xff08;网络爬虫的规则&#xff09; beautiful soup库&#xff1a;解析HTML页面 工具&…

【安装记录】安装 netperf 和 perf

这是一篇发疯随笔X.X 我的环境是虚拟机debian12&#xff0c;出于种种原因&#xff0c;之前直接使用apt-get install netperf apt-get install perf指令直接安装&#xff0c;报错找不到包 然后上网搜了一堆教程&#xff0c;有说下载netperf源码编译的&#xff0c;那些教程里面有…

SPSS双变量相关分析

双变量相关分析通过计算皮尔逊简单相关系数、斯皮尔曼等级相关系数、肯德尔等级相关系数及其显著性水平展开。其中皮尔逊简单相关系数是一种线性关联度量&#xff0c;适用于变量为定量连续变量且服从正态分布、相关关系为线性时的情形。如果变量不是正态分布的&#xff0c;或具…

Windows安全中心显示页面不可用

2024年2月过年当天重装电脑之后&#xff0c;第二天&#xff08;还是第三天&#xff09;安全中心开始提示如标题所示的问题。 问题环境 Windows 11 家庭中文版23H2安装日期2024/‎2/‎10 我解决之前没有截图&#xff0c;所以此处放一个别人的图做示例。 实际解决方式 搜索了…

假期刷题打卡--Day29

1、MT1224棋盘 求一个N*N棋盘中的方块总数。 格式 输入格式&#xff1a; 输入整型N 输出格式&#xff1a; 输出整型 样例 1 输入&#xff1a; 2输出&#xff1a; 5备注 考虑到取值范围&#xff0c;可用long整型定义变量 分析过程 这个题目的意思是&#xff0c;在这…

失去中国市场的三星仍是全球第一,但中国手机无法失去海外市场

随着2023年分析机构公布全球手机市场和中国手机市场的数据&#xff0c;业界终于看清中国市场早已没有以前那么重要&#xff0c;三星、苹果这些国际品牌对中国市场的依赖没有他们想象的那么严重&#xff0c;相反中国手机对海外市场比以往任何时候都要更依赖了。 三星在2023年被苹…

【Linux】模块参数

&#x1f525;博客主页&#xff1a;PannLZ &#x1f38b;系列专栏&#xff1a;《Linux系统之路》 &#x1f94a;不要让自己再留有遗憾&#xff0c;加油吧&#xff01; 模块参数 像用户程序一样&#xff0c;内核模块也可以接受命令行参数。首先应该声明用于保存命令行参数值的变…

Spring Native 解放 JVM

一、Spring Native 是什么 Spring Native可以通过GraalVM将Spring应用程序编译成原生镜像&#xff0c;提供了一种新的方式来部署Spring应用。与Java虚拟机相比&#xff0c;原生镜像可以在许多场景下降低工作负载&#xff0c;包括微服务&#xff0c;函数式服务&#xff0c;非常…

一、西瓜书——绪论

第一章 绪论 1.独立同分布 通常 假设 样本空间 中 全 体样 本 服 从 一 个 未 知 “ 分 布 ” ( d i s t r i b u t i o n ) D , 我们获得的每个样本都是独立地从这个分布上采样获得的&#xff0c; 即 “ 独 立同 分布 ” ( i n d e p e n d e n t a n d i d e n t ic a …

配置VMware实现从服务器到虚拟机的一键启动脚本

正文共&#xff1a;1666 字 15 图&#xff0c;预估阅读时间&#xff1a;2 分钟 首先祝大家新年快乐&#xff01;略备薄礼&#xff0c;18000个红包封面来讨个开年好彩头&#xff01; 虽然之前将服务器放到了公网&#xff08;成本增加了100块&#xff0c;内网服务器上公网解决方案…

二叉树和堆(优先队列)

前言&#xff1a; 本章会讲解二叉树及其一些相关练习题&#xff0c;和堆是什么。 二叉树&#xff1a; 二叉树的一些概念&#xff1a; 一棵二叉树是有限节点的集合&#xff0c;该集合可能为空。二叉树的特点是每一个节点最多有两个子树&#xff0c;即二叉树不存在度大于2的节点…

红包封面的流量是真的大啊

你好&#xff0c;我是小生&#xff0c;一个程序员转型做自媒体副业中~ 昨天&#xff0c;是农历 2023 年最后一天&#xff0c;到下午饭后的时候&#xff0c;红包封面流量推上了高峰期&#xff0c;非常的狂暴&#xff0c;有要烟花封面的、有要金龙封面的、有要表白封面的等等。 小…

在计算机/移动设备上恢复已删除视频的 10 个数据恢复工具

视频在网上疯传&#xff0c;我们都观看或创建视频&#xff0c;并将我们最喜欢的视频保存在硬盘上。如果我们丢失了一些重要的视频&#xff0c;那将是非常令人心碎的。但是今天&#xff0c;恢复已删除的视频变得更加容易。删除的视频在被新数据覆盖之前并没有真正从您的存储驱动…

单片机在物联网中的应用

单片机&#xff0c;这个小巧的电子设备&#xff0c;可能听起来有点技术性&#xff0c;但它实际上是物联网世界中的一个超级英雄。简单来说&#xff0c;单片机就像是各种智能设备的大脑&#xff0c;它能让设备“思考”和“行动”。由于其体积小、成本低、功耗低、易于编程等特点…

Web前端-移动web开发_rem布局

文章目录 移动web开发之rem布局1.0 rem基础1.1 rem单位(重点)1.2 em单位(了解)1.3 媒体查询什么是媒体查询媒体查询语法规范 1.4 less 基础维护css弊端Less 介绍Less安装Less 使用之变量使用node编译less的指令Less 编译 vocode Less 插件Less 嵌套Less 运算Less中的Mixin混入L…

使用 MinIO 超级充电 TileDB 引擎

MinIO 是一个强大的主要 TileDB 后端&#xff0c;因为两者都是为性能和规模而构建的。MinIO 是一个单一的 Go 二进制文件&#xff0c;可以在许多不同类型的云和本地环境中启动。它非常轻量级&#xff0c;但也具有复制和加密等功能&#xff0c;并且提供与各种应用程序的集成。Mi…

这MySQL错误日志异常也太猛了吧

作者&#xff1a;田逸&#xff08;formyz&#xff09; 一台核心业务数据库&#xff0c;版本为MySQL 8.34 社区服务器版。从上线以来&#xff0c;这个数据库服务器的错误日志增增加非常迅猛&#xff08;如下图所示&#xff09;&#xff0c;每24小时能增加到10多个G的容量。 因为…

前后端分离nodejs+vue动态网站的图书借阅管理系统35ih5

读者模块 1)注册&#xff1a;读者输入账号、密码、确认密码、姓名、手机、身份证、邮箱&#xff0c;点击注册按钮&#xff0c;完成注册。 2)登录&#xff1a;普通读者成功输入读者账号和密码&#xff0c;点击登录按钮。 3)读者主页面&#xff1a;读者登录成功后&#xff0c;选择…

C++重新入门-循环

目录 1.循环类型 while循环&#xff1a; for循环 基于范围的for循环(C11) do...while 循环 2.循环控制语句 3.无限循环 有的时候&#xff0c;可能需要多次执行同一块代码。一般情况下&#xff0c;语句是顺序执行的&#xff1a;函数中的第一个语句先执行&#xff0c;接着…

使用Flash download tool进行ESP32固件烧录

背景 为方便分发固件&#xff0c;可在任意电脑上安装烧录软件&#xff0c;直接将固件烧录进 烧录内容 查看vscode上platformio的烧录过程 Writing at 0x00000000... (100 %) Wrote 15104 bytes (10401 compressed) at 0x00000000 in 0.4 seconds (effective 281.3 kbit/s).…