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

news2024/11/25 14:36:26

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

97.网游逆向分析与插件开发-网络通信封包解析-项目需求与需求拆解

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;窗口化助手与游戏窗口同步移动 项目需求&#xff1a; 为游戏的聊天功能做一个增强&#xff0c;能够使用户脱离游戏界面的情况下使用窗口化助手进行聊天&#xff0c;能够设置自动回复&#xff0c;记录…

PE 特征码定位修改程序清单 uiAccess

requestedExecutionLevel level"asInvoker" uiAccess"false" 可以修改这一行来启用禁用原程序的盾牌图标&#xff0c;似乎作用不大。以前没事写的一个小玩意&#xff0c;记录一下。 等同于这里的设置&#xff1a; 截图 代码如下&#xff1a; #include …

c语言游戏实战(4):人生重开模拟器

前言&#xff1a; 人生重开模拟器是前段时间非常火的一个小游戏&#xff0c;接下来我们将一起学习使用c语言写一个简易版的人生重开模拟器。 网页版游戏&#xff1a; 人生重开模拟器 (ytecn.com) 1.实现一个简化版的人生重开模拟器 &#xff08;1&#xff09; 游戏开始的时…

力扣[面试题 01.02. 判定是否互为字符重排(哈希表,位图)

Problem: 面试题 01.02. 判定是否互为字符重排 文章目录 题目描述思路复杂度Code 题目描述 思路 思路1&#xff1a;哈希表 1.若两个字符串长度不相等&#xff0c;则一定不符合题意&#xff1b; 2.创建一个map集合&#xff0c;先将字符串s1中的每一个字符与其对应的数量存入集合…

【LeetCode每日一题】二维前缀和基本概念与案例

二维前缀和 根据某个块块 的 左上角坐标&#xff0c;和右下角坐标 求出 块块的累加和。 304. 二维区域和检索 - 矩阵不可变 /*** param {number[][]} matrix*/ var NumMatrix function(matrix) {let row matrix.length;let col matrix[0].length;// 初始化一个二维数组&am…

网络层DoS

网络层是OSI参考模型中的第三层&#xff0c;介于传输层和数据链路层之间&#xff0c;其目的 是实现两个终端系统之间数据的透明传送&#xff0c;具体功能包括&#xff1a;寻址和路由选择、连 接的建立、保持和终止等。位于网络层的协议包括ARP 、IP和ICMP等。下面就 ICMP为例&…

linux学习之虚拟地址

在以往的学习中我们经常接触地址&#xff0c;电脑像一个小房间&#xff0c;它的空间是有限不可重叠的&#xff0c;但是可以覆盖。想象一下如果我们要放很多东西进去&#xff0c;如果没有合理的安排&#xff0c;所有东西乱放&#xff0c;那么我们需要寻找某一个东西的时候需要把…

【开源】基于JAVA+Vue+SpringBoot的公司货物订单管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 客户管理模块2.2 商品维护模块2.3 供应商管理模块2.4 订单管理模块 三、系统展示四、核心代码4.1 查询供应商信息4.2 新增商品信息4.3 查询客户信息4.4 新增订单信息4.5 添加跟进子订单 五、免责说明 一、摘要 1.1 项目…

stack和queue基本使用

stack和queue基本使用 stackqueuepriority_queuestack和queue容器底层的默认容器&#xff1a;deque stack stack是一种容器适配器&#xff08;容器适配器可以将一种接口转为用户需要的另一种接口&#xff0c;如将vector、list的接口封装转成用户需要的stack的接口&#xff09;…

Java实现音乐平台 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示 四、核心代码4.1 查询单首音乐4.2 新增音乐4.3 新增音乐订单4.4 查询音乐订单4.5 新增音乐收藏 五、免责说明 一、摘要 1.1 项目介绍 基于微信小程序JAVAVueSpringBootMySQL的音乐平台&#xff0c;包含了音乐…

【Linux系统学习】6.Linux系统软件安装

实战章节&#xff1a;在Linux上部署各类软件 前言 为什么学习各类软件在Linux上的部署 在前面&#xff0c;我们学习了许多的Linux命令和高级技巧&#xff0c;这些知识点比较零散&#xff0c;进行练习虽然可以基础掌握这些命令和技巧的使用&#xff0c;但是并没有一些具体的实…

DP读书:《openEuler操作系统》(九)从IPC到网卡到卡驱动程序

DP读书&#xff1a;《openEuler操作系统》从IPC到网卡到卡驱动程序&#xff09; 上章回顾_SPI上节回顾_TCP 网卡驱动程序简介1.设备驱动2.总线与设备3.网卡及其抽象 驱动程序的注册与注销1. 注册2. 注销 设备初始化1. 硬件初始化2. 软件初始化 设备的打开与关闭1. 设备的打开2.…

lv14 中断处理原理:接口及按键驱动 14

一、什么是中断 一种硬件上的通知机制&#xff0c;用来通知CPU发生了某种需要立即处理的事件 分为&#xff1a; 内部中断 CPU执行程序的过程中&#xff0c;发生的一些硬件出错、运算出错事件&#xff08;如分母为0、溢出等等&#xff09;&#xff0c;不可屏蔽 外部中断 外设…

OpenAI---提示词工程的6大原则

OpenAI在官方的文档里上线了Prompt engineering&#xff0c;也就是提示词工程指南&#xff0c;其中OpenAI有提到写提示词的6条大的原则&#xff0c;它们分别是&#xff1a; &#xff08;1&#xff09;Write clear instructions&#xff08;写出清晰的指令&#xff09; &#xf…

React18原理: 再聊Fiber架构下的时间分片

时间分片 react的任务可以被打断&#xff0c;其实就是基于时间分片的人眼最高能识别的帧数不超过30帧&#xff0c;电影的帧数差不多是在24浏览器的帧率一般来说是60帧&#xff0c;也就是每秒60个画面, 平均一个画面大概是16.5毫秒左右浏览器正常的工作流程是运算渲染&#xff…

DataEase

一. DataEase (一). 说明 安装文档 DataEase 是开源的数据可视化分析工具&#xff0c;帮助用户快速分析数据并洞察业务趋势&#xff0c;从而实现业务的改进与优化。DataEase 支持丰富的数据源连接&#xff0c;能够通过拖拉拽方式快速制作图表&#xff0c;并可以方便的与他人分…

私有化部署一个自己的网盘

效果 安装 1.创建目录 cd /opt mkdir -p kod/{db,site} cd /opt/kod 2.环境文件 vim db.env 内容如下 MYSQL_PASSWORD123456 MYSQL_DATABASEkodbox MYSQL_USERkodbox 3.编写docker-compose.yml vim docker-compose.yml 内容如下 version: 3.5services:db:image: mar…

C#系列-使用 Minio 做图片服务器实现图片上传 和下载(13)

1、Minio 服务器下载和安装 要在本地安装和运行 MinIO 服务器&#xff0c;你可以按照以下 步骤进行操作&#xff1a; 1. 访问 MinIO 的官方网站&#xff1a;https://min.io/&#xff0c;然后 点击页面上的”Download”按钮。 2. 在下载页面上&#xff0c;选择适合你操作系统的 …

假期作业 8

1、若有以下说明语句&#xff1a;int a[12]{1,2,3,4,5,6,7,8,9,10,11,12};char c’a’,d,g;则数值为4的表达式是&#xff08; B&#xff09;。 A&#xff09;a[g-c] B&#xff09;a[4] C&#xff09;a[‘d’-‘c’] D&#xff09;a[‘d’-c] 2、假…

二、数据结构

链表 单链表 https://www.acwing.com/problem/content/828/ #include<iostream> using namespace std; const int N 1e5 10; //head:头节点的指向 e[i]:当前节点i的值 ne[i]:当前节点i的next指针 idx:当前存储的点 int head, e[N], ne[N], idx;//初始化 void i…