Spring AI - 使用向量数据库实现检索式AI对话
Spring AI并不仅限于针对大语言模型对话API进行了统一封装,它还可以通过简单的方式实现LangChain的一些功能。本篇将带领读者实现一个简单的检索式AI对话接口。 LangChain相关知识
1.需求背景
在一些场景下,我们想让AI
根据我们提供的数据
进行
回复
。因为对话有最大Token的限制,因此很多场景下我们是无法直接将所有的数据发给AI的,一方面在数据量很大的情况下,会突破Token的限制,另一方面,在不突破Token限制的情况下也会有不必要的对话费用开销。因此我们如何在花费最少费用的同时又能让A
I更好的根据我们提供的数据
进行回复是一个非常关键的问题。针对这一问题,我们可以采用
数据向量化
的方式来解决。
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
与
向量数据库
的交互操作,我们只需通过
简单的向量数据库
的
配置
即可使用该接口对向量数据库进行操作。
publicinterfaceVectorStore{voidadd(List<Document> documents);Optional<Boolean>delete(List<String> idList);List<Document>similaritySearch(String query);List<Document>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的配置:
<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>
2.配置API,key,Pgvector:
spring ai支持的大模型比较多,也可以用ollama里面的模型
server:port:8801spring: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
packagecom.ningning0111.vectordatabasedemo.config;importorg.springframework.ai.embedding.EmbeddingClient;importorg.springframework.ai.transformer.splitter.TokenTextSplitter;importorg.springframework.ai.vectorstore.PgVectorStore;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.jdbc.core.JdbcTemplate;@ConfigurationpublicclassApplicationConfig{/**
* 向量数据库进行检索操作
* @param embeddingClient
* @param jdbcTemplate
* @return
*/@BeanpublicVectorStorevectorStore(EmbeddingClient embeddingClient,JdbcTemplate jdbcTemplate){returnnewPgVectorStore(jdbcTemplate,embeddingClient);}/**
* 文本分割器
* @return
*/@BeanpublicTokenTextSplittertokenTextSplitter(){returnnewTokenTextSplitter();}}
3.4 构建PDF存储服务层
在
service层
下创建一个名为
PdfStoreService
的类,用于将
PDF文件
存储到
向量数据库
中。
packagecom.ningning0111.vectordatabasedemo.service;importlombok.RequiredArgsConstructor;importorg.springframework.ai.reader.ExtractedTextFormatter;importorg.springframework.ai.reader.pdf.PagePdfDocumentReader;importorg.springframework.ai.reader.pdf.ParagraphPdfDocumentReader;importorg.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;importorg.springframework.ai.transformer.splitter.TokenTextSplitter;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.core.io.DefaultResourceLoader;importorg.springframework.core.io.FileSystemResource;importorg.springframework.core.io.Resource;importorg.springframework.stereotype.Service;importorg.springframework.web.multipart.MultipartFile;importjava.io.IOException;importjava.nio.file.Files;importjava.nio.file.Path;@Service@RequiredArgsConstructorpublicclassPdfStoreService{privatefinalDefaultResourceLoader resourceLoader;privatefinalVectorStore vectorStore;privatefinalTokenTextSplitter tokenTextSplitter;/**
* 根据PDF的页数进行分割
* @param url
*/publicvoidsaveSourceByPage(String url){// 加载资源,需要本地路径的信息Resource resource = resourceLoader.getResource(url);// 加载PDF文件时的配置对象PdfDocumentReaderConfig loadConfig =PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(newExtractedTextFormatter
.Builder().withNumberOfBottomTextLinesToDelete(3).withNumberOfTopPagesToSkipBeforeDelete(1).build()).withPagesPerDocument(1).build();PagePdfDocumentReader pagePdfDocumentReader =newPagePdfDocumentReader(resource, loadConfig);// 存储到向量数据库中
vectorStore.accept(tokenTextSplitter.apply(pagePdfDocumentReader.get()));}/**
* 根据PDF的目录(段落)进行划分
* @param url
*/publicvoidsaveSourceByParagraph(String url){Resource resource = resourceLoader.getResource(url);PdfDocumentReaderConfig loadConfig =PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(newExtractedTextFormatter
.Builder().withNumberOfBottomTextLinesToDelete(3).withNumberOfTopPagesToSkipBeforeDelete(1).build()).withPagesPerDocument(1).build();ParagraphPdfDocumentReader pdfReader =newParagraphPdfDocumentReader(
resource,
loadConfig
);
vectorStore.accept(tokenTextSplitter.apply(pdfReader.get()));}/**
* MultipartFile对象存储,采用PagePdfDocumentReader
* @param file
*/publicvoidsaveSource(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 =newFileSystemResource(tempFile.toFile());PdfDocumentReaderConfig loadConfig =PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(newExtractedTextFormatter
.Builder().withNumberOfBottomTextLinesToDelete(3).withNumberOfTopPagesToSkipBeforeDelete(1).build()).withPagesPerDocument(1).build();PagePdfDocumentReader pagePdfDocumentReader =newPagePdfDocumentReader(fileResource, loadConfig);
vectorStore.accept(tokenTextSplitter.apply(pagePdfDocumentReader.get()));}catch(IOException e){
e.printStackTrace();}}}
3.5 构建对话服务
创建
ChatService
类,该类提供了
两种对话方式
:
不进行检索
的普通对话模式和
对向量数据库进行检索
的对话模式
packagecom.ningning0111.vectordatabasedemo.service;importlombok.RequiredArgsConstructor;importorg.springframework.ai.chat.ChatClient;importorg.springframework.ai.chat.ChatResponse;importorg.springframework.ai.chat.messages.Message;importorg.springframework.ai.chat.messages.UserMessage;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.chat.prompt.SystemPromptTemplate;importorg.springframework.ai.document.Document;importorg.springframework.ai.vectorstore.VectorStore;importorg.springframework.stereotype.Service;importjava.util.List;importjava.util.Map;importjava.util.stream.Collectors;@Service@RequiredArgsConstructorpublicclassChatService{// 系统提示词privatefinalstaticStringSYSTEM_PROMPT="""
你需要使用文档内容对用户提出的问题进行回复,同时你需要表现得天生就知道这些内容,
不能在回复中体现出你是根据给出的文档内容进行回复的,这点非常重要。
当用户提出的问题无法根据文档内容进行回复或者你也不清楚时,回复不知道即可。
文档内容如下:
{documents}
""";privatefinalChatClient chatClient;privatefinalVectorStore vectorStore;// 简单的对话,不对向量数据库进行检索publicStringsimpleChat(String userMessage){return chatClient.call(userMessage);}// 通过向量数据库进行检索publicStringchatByVectorStore(String message){// 根据问题文本进行相似性搜索List<Document> listOfSimilarDocuments = vectorStore.similaritySearch(message);// 将Document列表中每个元素的content内容进行拼接获得documentsString documents = listOfSimilarDocuments.stream().map(Document::getContent).collect(Collectors.joining());// 使用Spring AI 提供的模板方式构建SystemMessage对象Message systemMessage =newSystemPromptTemplate(SYSTEM_PROMPT).createMessage(Map.of("documents", documents));// 构建UserMessage对象UserMessage userMessage =newUserMessage(message);// 将Message列表一并发送给ChatGPTChatResponse rsp = chatClient.call(newPrompt(List.of(systemMessage, userMessage)));return rsp.getResult().getOutput().getContent();}}
3.6 构建ChatController提供对话接口:
packagecom.ningning0111.vectordatabasedemo.controller;importcom.ningning0111.vectordatabasedemo.service.ChatService;importlombok.RequiredArgsConstructor;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.bind.annotation.RestController;@RestController@RequiredArgsConstructor@RequestMapping("/api/v1/chat")publicclassChatController{privatefinalChatService chatService;@GetMapping("/simple")publicStringsimpleChat(@RequestParamString message
){return chatService.simpleChat(message);}@GetMapping("/")publicStringchat(@RequestParamString message
){return chatService.chatByVectorStore(message);}}
3.7 构建PdfUploadController提供了上传文件并保存到向量数据库中接口
packagecom.ningning0111.vectordatabasedemo.controller;importcom.ningning0111.vectordatabasedemo.service.PdfStoreService;importlombok.RequiredArgsConstructor;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RequestParam;importorg.springframework.web.multipart.MultipartFile;@Controller@RequestMapping("/api/v1/pdf")@RequiredArgsConstructorpublicclassPdfUploadController{privatefinalPdfStoreService pdfStoreService;@PostMapping("/upload")publicvoidupload(@RequestParamMultipartFile file
){
pdfStoreService.saveSource(file);}}
版权归原作者 Fairy要carry 所有, 如有侵权,请联系我们删除。