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

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

1.需求背景

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

2.实现原理

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

2.1 数据向量化

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

2.2 向量存储以及检索

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

public interface VectorStore {

void add(List documents);

Optional delete(List idList);

List similaritySearch(String query);

List similaritySearch(SearchRequest request);

}

2.3 向量数据库的介绍

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

常见的向量数据库有:Chroma、Milvus、Pgvector、Redis、Neo4j等。

3.代码实现

3.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

3.2创建Spring项目

1.pom.xml的配置:

17

0.8.0-SNAPSHOT

org.springframework.boot

spring-boot-starter-jdbc

org.springframework.ai

spring-ai-openai-spring-boot-starter

${spring-ai.version}

org.springframework.ai

spring-ai-pgvector-store-spring-boot-starter

${spring-ai.version}

org.springframework.ai

spring-ai-pdf-document-reader

${spring-ai.version}

spring-milestones

Spring Milestones

https://repo.spring.io/milestone

false

spring-snapshots

Spring Snapshots

https://repo.spring.io/snapshot

false

2.配置API,key,Pgvector: spring ai支持的大模型比较多,也可以用ollama里面的模型

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

3.3 创建VectorStore和文本分割器TokenTextSplitter

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();

}

}

3.4 构建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();

}

}

}

3.5 构建对话服务

创建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 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();

}

}

3.6 构建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);

}

}

3.7 构建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);

}

}

https://github.com/NingNing0111/vector-database-demo

精彩链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: