当前位置: 首页 > news >正文

RAG(知识库ChatPDF)

目录

1.RAG原理

1.1.向量模型

1.2.向量数据库

1.1.3.SimpleVectorStore

1.1.4.VectorStore接口

1.1.5 文件读取和转换

1.1.5.RAG原理总结

1.1.6.目标

1.2.PDF上传下载、向量化

1.2.1.PDF文件管理

1.2.2.上传文件响应结果

1.2.3.文件上传、下载

1.2.4.上传大小限制

1.2.5.暴露响应头

1.3.配置ChatClient

1.4.对话接口

1.5.持久化VectorStore(选学)

1.5.1.RedisVectorStore


由于训练大模型非常耗时,再加上训练语料本身比较滞后,所以大模型存在知识限制问题:

  • 知识数据比较落后,往往是几个月之前的

  • 不包含太过专业领域或者企业私有的数据

为了解决这些问题,就需要用到RAG了。下面简单回顾下RAG原理

1.RAG原理

要解决大模型的知识限制问题,其实并不复杂。解决的思路就是给大模型外挂一个知识库,可以是专业领域知识,也可以是企业私有的数据。不过,知识库不能简单的直接拼接在提示词中。因为通常知识库数据量都是非常大的,而大模型的上下文是有大小限制的,早期的GPT上下文不能超过2000token,现在也不到200k token,因此知识库不能直接写在提示词中。怎么办?思路很简单,庞大的知识库中与用户问题相关的其实并不多。所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。

那么问题来了,该如何从知识库中找到与用户问题相关的内容呢?可能有同学会想到到全文检索,但是在这里是不合适的,因为全文检索是文字匹配,这里我们要求的是内容上的相似度。

而要从内容相似度来判断,这就不得不提到向量模型的知识了。

1.1.向量模型

先说说向量,向量是空间中有方向和长度的量,空间可以是二维,也可以是多维。向量既然是在空间中,两个向量之间就一定能计算距离。我们以二维向量为例,向量之间的距离有两种计算方法:

通常,两个向量之间欧式距离越近,我们认为两个向量的相似度越高。(余弦距离相反,越大相似度越高)所以,如果我们能把文本转为向量,就可以通过向量距离来判断文本的相似度了。现在,有不少的专门的向量模型,就可以实现将文本向量化。一个好的向量模型,就是要尽可能让文本含义相似的向量,在空间中距离更近

接下来,我们就准备一个向量模型,用于将文本向量化。阿里云百炼平台就提供了这样的模型:

这里我们选择通用文本向量-v3,这个模型兼容OpenAI,所以我们依然采用OpenAI的配置。修改application.yaml,添加向量模型配置:

spring:application:name: ai-demoai:ollama:base-url: http://localhost:11434 # ollama服务地址chat:model: deepseek-r1:7b # 模型名称,可更改options:temperature: 0.8 # 模型温度,值越大,输出结果越随机openai:base-url: https://dashscope.aliyuncs.com/compatible-modeapi-key: ${OPENAI_API_KEY}chat:options:model: qwen-max # 模型名称temperature: 0.8 # 模型温度,值越大,输出结果越随机embedding:options:model: text-embedding-v3dimensions: 1024
1.2.向量数据库

前面说了,知识库数据量很大,无法全部写入提示词。但是庞大的知识库中与用户问题相关的其实并不多。所以,我们需要想办法从庞大的知识库中找到与用户问题相关的一小部分,组装成提示词,发送给大模型就可以了。现在,利用向量大模型就可以帮助我们比较文本相似度。但是新的问题来了:向量模型是帮我们生成向量的,如此庞大的知识库,谁来帮我们从中比较和检索数据呢?

这就需要用到向量数据库了。

向量数据库的主要作用有两个:

  • 存储向量数据

  • 基于相似度检索数据

刚好符合我们的需求。

SpringAI支持很多向量数据库,并且都进行了封装,可以用统一的API去访问:

  • Azure Vector Search - The Azure vector store.

  • Apache Cassandra - The Apache Cassandra vector store.

  • Chroma Vector Store - The Chroma vector store.

  • Elasticsearch Vector Store - The Elasticsearch vector store.

  • GemFire Vector Store - The GemFire vector store.

  • MariaDB Vector Store - The MariaDB vector store.

  • Milvus Vector Store - The Milvus vector store.

  • MongoDB Atlas Vector Store - The MongoDB Atlas vector store.

  • Neo4j Vector Store - The Neo4j vector store.

  • OpenSearch Vector Store - The OpenSearch vector store.

  • Oracle Vector Store - The Oracle Database vector store.

  • PgVector Store - The PostgreSQL/PGVector vector store.

  • Pinecone Vector Store - PineCone vector store.

  • Qdrant Vector Store - Qdrant vector store.

  • Redis Vector Store - The Redis vector store.

  • SAP Hana Vector Store - The SAP HANA vector store.

  • Typesense Vector Store - The Typesense vector store.

  • Weaviate Vector Store - The Weaviate vector store.

  • SimpleVectorStore - A simple implementation of persistent vector storage, good for educational purposes.

这些库都实现了统一的接口:VectorStore,因此操作方式一模一样,大家学会任意一个,其它就都不是问题。不过,除了最后一个库以外,其它所有向量数据库都是需要安装部署的。每个企业用的向量库都不一样,这里就不一一演示了。

1.1.3.SimpleVectorStore

最后一个SimpleVectorStore向量库是基于内存实现,是一个专门用来测试、教学用的库,我们直接修改CommonConfiguration,添加一个VectorStore的Bean:

@Configuration
public class CommonConfiguration {@Beanpublic VectorStore vectorStore(OpenAiEmbeddingModel embeddingModel) {return SimpleVectorStore.builder(embeddingModel).build();}// ... 略
}
1.1.4.VectorStore接口

接下来,你就可以使用VectorStore中的各种功能了,可以参考SpringAI官方文档:

Vector Databases :: Spring AI Reference

这是VectorStore中声明的方法:

public interface VectorStore extends DocumentWriter {default String getName() {return this.getClass().getSimpleName();}// 保存文档到向量库void add(List<Document> documents);// 根据文档id删除文档void delete(List<String> idList);void delete(Filter.Expression filterExpression);default void delete(String filterExpression) { ... };// 根据条件检索文档List<Document> similaritySearch(String query);// 根据条件检索文档List<Document> similaritySearch(SearchRequest request);default <T> Optional<T> getNativeClient() {return Optional.empty();}
}

【注意】VectorStore操作向量化的基本单位是Document,我们在使用时需要将自己的知识库分割转换为一个个的Document,然后写入VectorStore.

那么问题来了,我们该如何把各种不同的知识库文件转为Document呢???

1.1.5 文件读取和转换

前面说过,知识库太大,是需要拆分成文档片段,然后再做向量化的。而且SpringAI中向量库接收的是Document类型的文档,也就是说,我们处理文档还要转成Document格式。不过,文档读取、拆分、转换的动作并不需要我们亲自完成。在SpringAI中提供了各种文档读取的工具,可以参考官网:

ETL Pipeline :: Spring AI Reference

比如PDF文档读取和拆分,SpringAI提供了两种默认的拆分原则:

  • PagePdfDocumentReader :按页拆分,推荐使用

  • ParagraphPdfDocumentReader :按pdf的目录拆分,不推荐,因为很多PDF不规范,没有章节标签

当然,大家也可以自己实现PDF的读取和拆分功能。这里我们选择使用PagePdfDocumentReader

首先,我们需要在pom.xml中引入依赖:

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>

然后就可以利用工具把PDF文件读取并处理成Document了。我们写一个单元测试(别忘了配置API_KEY):

@Test
public void testVectorStore(){Resource resource = new FileSystemResource("中二知识笔记.pdf");// 1.创建PDF的读取器PagePdfDocumentReader reader = new PagePdfDocumentReader(resource, // 文件源PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(ExtractedTextFormatter.defaults()).withPagesPerDocument(1) // 每1页PDF作为一个Document.build());// 2.读取PDF文档,拆分为DocumentList<Document> documents = reader.read();// 3.写入向量库vectorStore.add(documents);// 4.搜索SearchRequest request = SearchRequest.builder().query("论语中教育的目的是什么").topK(1).similarityThreshold(0.6).filterExpression("file_name == '中二知识笔记.pdf'").build();List<Document> docs = vectorStore.similaritySearch(request);if (docs == null) {System.out.println("没有搜索到任何内容");return;}for (Document doc : docs) {System.out.println(doc.getId());System.out.println(doc.getScore());System.out.println(doc.getText());}
}
1.1.5.RAG原理总结

OK,现在我们有了这些工具:

  • PDFReader:读取文档并拆分为片段

  • 向量大模型:将文本片段向量化

  • 向量数据库:存储向量,检索向量

让我们梳理一下要解决的问题和解决思路:

  • 要解决大模型的知识限制问题,需要外挂知识库

  • 受到大模型上下文限制,知识库不能简单的直接拼接在提示词中

  • 我们需要从庞大的知识库中找到与用户问题相关的一小部分,再组装成提示词

  • 这些可以利用文档读取器向量大模型向量数据库来解决。

所以RAG要做的事情就是将知识库分割,然后利用向量模型做向量化,存入向量数据库,然后查询的时候去检索:

第一阶段(存储知识库)

  • 将知识库内容切片,分为一个个片段

  • 将每个片段利用向量模型向量化

  • 将所有向量化后的片段写入向量数据库

第二阶段(检索知识库)

  • 每当用户询问AI时,将用户问题向量化

  • 拿着问题向量去向量数据库检索最相关的片段

第三阶段(对话大模型)

  • 将检索到的片段、用户的问题一起拼接为提示词

  • 发送提示词给大模型,得到响应

1.1.6.目标

现在RAG所需要的基本工具都有了。接下来,我们就来实现一个非常火爆的个人知识库AI应用,ChatPDF,原网站如下:

1.2.PDF上传下载、向量化

既然是ChatPDF,也就是说所有知识库都是PDF形式的,由用户提交给我们。所以,我们需要先实现一个上传PDF的接口,在接口中实现下列功能:

  • 校验文件格式是否为PDF

  • 保存文件信息

    • 保存文件(可以是oss或本地保存)

    • 保存会话ID和文件路径的映射关系(方便查询会话历史的时候再次读取文件)

  • 文档拆分和向量化(文档太大,需要拆分为一个个片段,分别向量化)

另外,将来用户查询会话历史,我们还需要返回pdf文件给前端用于预览,所以需要实现一个下载PDF接口,包含下面功能:

  • 读取文件

  • 返回文件给前端

1.2.1.PDF文件管理

由于将来要实现PDF下载功能,我们需要记住每一个chatId对应的PDF文件名称。所以,我们定义一个类,记录chatId与pdf文件的映射关系,同时实现基本的文件保存功能

先在com.itheima.ai.repository中定义接口:

package com.itheima.ai.repository;import org.springframework.core.io.Resource;public interface FileRepository {/*** 保存文件,还要记录chatId与文件的映射关系* @param chatId 会话id* @param resource 文件* @return 上传成功,返回true; 否则返回false*/boolean save(String chatId, Resource resource);/*** 根据chatId获取文件* @param chatId 会话id* @return 找到的文件*/Resource getFile(String chatId);
}

再写一个实现类:

package com.itheima.ai.repository;import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.Properties;@Slf4j
@Component
@RequiredArgsConstructor
public class LocalPdfFileRepository implements FileRepository {private final VectorStore vectorStore;// 会话id 与 文件名的对应关系,方便查询会话历史时重新加载文件private final Properties chatFiles = new Properties();@Overridepublic boolean save(String chatId, Resource resource) {// 2.保存到本地磁盘String filename = resource.getFilename();File target = new File(Objects.requireNonNull(filename));if (!target.exists()) {try {Files.copy(resource.getInputStream(), target.toPath());} catch (IOException e) {log.error("Failed to save PDF resource.", e);return false;}}// 3.保存映射关系chatFiles.put(chatId, filename);return true;}@Overridepublic Resource getFile(String chatId) {return new FileSystemResource(chatFiles.getProperty(chatId));}@PostConstructprivate void init() {FileSystemResource pdfResource = new FileSystemResource("chat-pdf.properties");if (pdfResource.exists()) {try {chatFiles.load(new BufferedReader(new InputStreamReader(pdfResource.getInputStream(), StandardCharsets.UTF_8)));} catch (IOException e) {throw new RuntimeException(e);}}FileSystemResource vectorResource = new FileSystemResource("chat-pdf.json");if (vectorResource.exists()) {SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;simpleVectorStore.load(vectorResource);}}@PreDestroyprivate void persistent() {try {chatFiles.store(new FileWriter("chat-pdf.properties"), LocalDateTime.now().toString());SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;simpleVectorStore.save(new File("chat-pdf.json"));} catch (IOException e) {throw new RuntimeException(e);}}
}

【注意】:由于我们选择了基于内存的SimpleVectorStore,重启就会丢失向量数据。所以这里我依然是将pdf文件与chatId的对应关系、VectorStore都持久化到了磁盘。实际开发中,如果你选择了RedisVectorStore,或者CassandraVectorStore,则无序自己持久化。但是chatId和PDF文件之间的对应关系,还是需要自己维护的。

1.2.2.上传文件响应结果

由于前端文件上传需要返回响应结果,我们先在com.itheima.ai.entity.vo中定义一个Result类:

package com.itheima.ai.entity.vo;import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
public class Result {private Integer ok;private String msg;private Result(Integer ok, String msg) {this.ok = ok;this.msg = msg;}public static Result ok() {return new Result(1, "ok");}public static Result fail(String msg) {return new Result(0, msg);}
}
1.2.3.文件上传、下载
package com.itheima.ai.controller;import com.itheima.ai.entity.vo.Result;
import com.itheima.ai.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/pdf")
public class PdfController {private final FileRepository fileRepository;private final VectorStore vectorStore;/*** 文件上传*/@RequestMapping("/upload/{chatId}")public Result uploadPdf(@PathVariable String chatId, @RequestParam("file") MultipartFile file) {try {// 1. 校验文件是否为PDF格式if (!Objects.equals(file.getContentType(), "application/pdf")) {return Result.fail("只能上传PDF文件!");}// 2.保存文件boolean success = fileRepository.save(chatId, file.getResource());if(! success) {return Result.fail("保存文件失败!");}// 3.写入向量库this.writeToVectorStore(file.getResource());return Result.ok();} catch (Exception e) {log.error("Failed to upload PDF.", e);return Result.fail("上传文件失败!");}}/*** 文件下载*/@GetMapping("/file/{chatId}")public ResponseEntity<Resource> download(@PathVariable("chatId") String chatId) throws IOException {// 1.读取文件Resource resource = fileRepository.getFile(chatId);if (!resource.exists()) {return ResponseEntity.notFound().build();}// 2.文件名编码,写入响应头String filename = URLEncoder.encode(Objects.requireNonNull(resource.getFilename()), StandardCharsets.UTF_8);// 3.返回文件return ResponseEntity.ok().contentType(MediaType.APPLICATION_OCTET_STREAM).header("Content-Disposition", "attachment; filename=\"" + filename + "\"").body(resource);}private void writeToVectorStore(Resource resource) {// 1.创建PDF的读取器PagePdfDocumentReader reader = new PagePdfDocumentReader(resource, // 文件源PdfDocumentReaderConfig.builder().withPageExtractedTextFormatter(ExtractedTextFormatter.defaults()).withPagesPerDocument(1) // 每1页PDF作为一个Document.build());// 2.读取PDF文档,拆分为DocumentList<Document> documents = reader.read();// 3.写入向量库vectorStore.add(documents);}
}
1.2.4.上传大小限制

SpringMVC有默认的文件大小限制,只有10M,很多知识库文件都会超过这个值,所以我们需要修改配置,增加文件上传允许的上限。

修改application.yaml文件,添加配置:

spring:servlet:multipart:max-file-size: 104857600max-request-size: 104857600
1.2.5.暴露响应头

默认情况下跨域请求的响应头是不暴露的,这样前端就拿不到下载的文件名,我们需要修改CORS配置,暴露响应头:

package com.itheima.ai.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MvcConfiguration implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOrigins("*").allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS").allowedHeaders("*").exposedHeaders("Content-Disposition");}
}

1.3.配置ChatClient

接下来就是最后的环节了,实现RAG的对话流程。理论上来说,我们每次与AI对话的完整流程是这样的:

  • 将用户的问题利用向量大模型做向量化 OpenAiEmbeddingModel

  • 去向量数据库检索相关的文档 VectorStore

  • 拼接提示词,发送给大模型

  • 解析响应结果

不过,SpringAI同样基于AOP技术帮我们完成了全部流程,用到的是一个名QuestionAnswerAdvisor的Advisor。我们只需要把VectorStore配置到Advisor即可。我们在CommonConfiguration中给ChatPDF也单独定义一个ChatClient

@Bean
public ChatClient pdfChatClient(OpenAiChatModel model,ChatMemory chatMemory,VectorStore vectorStore) {return ChatClient.builder(model).defaultSystem("请根据提供的上下文回答问题,不要自己猜测。").defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // CHAT MEMORYnew SimpleLoggerAdvisor(),new QuestionAnswerAdvisor(vectorStore, // 向量库SearchRequest.builder() // 向量检索的请求参数.similarityThreshold(0.5d) // 相似度阈值.topK(2) // 返回的文档片段数量.build())).build();
}

我们也可以自己自定义RAG查询的流程,不使用Advisor,具体可参考官网:

Retrieval Augmented Generation :: Spring AI Reference

1.4.对话接口

最后,就是对接前端,然后与大模型对话了。修改PdfController,添加一个接口:

@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(String prompt, String chatId) {chatRepository.addChatId("pdf", chatId);Resource file = fileRepository.getFile(chatId);return pdfChatClient.prompt(prompt).advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)).advisors(a -> a.param(QuestionAnswerAdvisor.FILTER_EXPRESSION, "file_name == '"+file.getFilename()+"'")).stream().content();
}

至此大模型应用开发已经全部结束~~。

1.5.持久化VectorStore(选学)

SpringAI提供了很多持久化的VectorStore,以其中两个为例来介绍:

  • RedisVectorStore : 目前测试metafiled过滤有异常

  • CassandraVectorStore

1.5.1.RedisVectorStore

首先,你需要安装一个Redis Stack,这是Redis官方提供的拓展版本,其中有向量库的功能。

可以使用Docker安装:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

安装完成后,你可以通过命令行访问:

docker exec -it redis-stack redis-cli

也可以通过浏览器访问控制台:http://localhost:8001,注意,这里的IP要换成你自己的。

后,你可以在项目中引入RedisVectorStore的依赖:

<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
</dependency>

application.yml配置Redis:

spring:ai:vectorstore:redis:index: spring_ai_index # 向量库索引名initialize-schema: true # 是否初始化向量库索引结构prefix: "doc:" # 向量库key前缀data:redis:host: 192.168.150.101 # redis地址

接下来,无需声明bean,直接就可以直接使用VectorStore了。

http://www.dtcms.com/a/349675.html

相关文章:

  • 开源大模型天花板?DeepSeek-V3 6710亿参数MoE架构深度拆解
  • 无障碍辅助模块|Highcharts引领可访问数据可视化的交流
  • 部分CSS笔试题讲解
  • Python JSON 全方位解析:序列化、反序列化与实战技巧
  • pytest+requests+Python3.7+yaml+Allure+Jenkins+docker实现接口自动化测试
  • k8sday17安全机制
  • flask Celery入门:轻松实现异步任务处理
  • 前端通过node本地转译rtsp流,配合hls实现浏览
  • 【SQL】深入理解MySQL存储过程:从入门到实战
  • CUDA 工具包 13.0 正式发布:开启新一代 GPU 计算的基石!
  • 使用EasyExcel根据模板导出文件
  • QtExcel/QXlsx
  • 深入浅出 Java 多态:从原理到实践的全面解析
  • 【RAGFlow代码详解-5】配置系统
  • 基于深度学习的翻拍照片去摩尔纹在线系统设计与实现
  • UE5 HoudiniPivotPainter1.0使用
  • NFC 滤波网络设计考虑
  • 车载通信架构---通过CANoe做 SOME/IP 模拟的配置例如
  • 库存指标怎么算?一文讲清3大库存分析指标
  • 大数据治理域——离线数据开发
  • 小白成长之路-k8s部署项目(二)
  • Legion Y7000P IRX9 DriveList
  • 【数据可视化-100】使用 Pyecharts 绘制人口迁徙图:步骤与数据组织形式
  • 程序设计---状态机
  • KVM 虚拟化技术与部署
  • ZKmall开源商城多端兼容实践:鸿蒙、iOS、安卓全平台适配的技术路径
  • 朴素贝叶斯学习笔记
  • Selenium框架Java实践截图服务
  • 面向过程与面向对象
  • 了解检验和