SpringAI+DeepSeek大模型應用開發——5 ChatPDF

ChatPDF 知識庫

RAG檢索增強

由于訓練大模型非常耗時,再加上訓練語料本身比較滯后,所以大模型存在知識限制問題:

  • 知識數據比較落后,往往是幾個月之前的;不包含太過專業領域或者企業私有的數據;

  • 為了解決這些問題,就需要用到RAG了。

RAG原理

RAG 的核心原理是將檢索技術與生成模型相結合,結合外部知識庫來檢索相關信息來增強模型的輸出,其實就是給大模型掛一個知識庫

其核心工作流程分為三個階段:

  1. 接收請求: 首先,系統接收到用戶的請求(例如提出一個問題)
  2. 信息檢索?: 系統從一個大型文檔庫中檢索出與查詢最相關的文檔片段。這一步的目標是找到那些可能包含答案或相關信息的文檔。這里不一定是從向量數據庫中檢索,但是向量數據庫能反應相似度最高的幾個文檔(比如說法不同,意思相同),而不是精確查找
  3. 生成增強(A): 將檢索到的文檔片段與原始查詢一起輸入到大模型(如chatGPT)中,注意使用合適的提示詞,比如原始的問題是XXX,檢索到的信息是YY,給大模型的輸入應該類似于: 請基于YYY回答XXXX。
  4. 輸出生成(G): 大模型LLM 基于輸入的査詢和檢索到的文檔片段生成最終的文本答案,并返回給用戶

注意:知識庫不能寫在提示詞中,因為通常知識庫數據量都是非常大的,而大模型的上下文是有大小限制的,那怎么辦呢?

只要想辦法從龐大的知識庫中找到與用戶問題相關的一小部分,組裝成提示詞,發送給大模型就可以了;那么該如何從知識庫中找到與用戶問題相關的內容呢?

  • 全文檢索?但在這里是不合適的,因為全文檢索是文字匹配,而這里要求的是內容上的相似度;
  • 而要從內容相似度來判斷,這就不得不提到向量模型的知識了。

向量模型

向量是空間中有方向和長度的量,空間可以是二維,也可以是多維;向量既然是在空間中,那么兩個向量之間就一定能計算距離;

向量之間的距離一般有兩種計算方法:

歐幾里得距離

在n維空間中,兩點間的直線距離。它是兩點間最直接的距離測量方式。很適合用于RGB色彩空間中衡量兩種顏色之間的差異

顏色可以用 RGB 值表示,然后通過計算兩種顏色 RGB 值之間的歐幾里得距離來判斷它們的相似度。

在這里插入圖片描述

  • R G B: 兩個顏色的 RGB 分量(紅色、綠色、藍色)
  • d: 兩個顏色之間的歐幾里得距離。
  • 距離越小,表示顏色越相似; 距離越大,表示顏色越不同
余弦相似度

通過比較兩個向量之間的夾角余弦值來衡量它們的方向是否相似,如果夾角余弦值越小,說明它們越相似,但這種方法不能考慮到向量的大小。

在顏色分析中,它可以用來比較顏色 色調的相似性,但是它對于亮度和飽和度的變化不敏感。
在這里插入圖片描述
綜上,如果能把文本轉為向量,就可以通過向量距離來判斷文本的相似度了;

現在有不少的專門的向量模型,就可以實現將文本向量化。一個好的向量模型,就是要盡可能讓文本含義相似的向量,在空間中距離更近
在這里插入圖片描述
阿里云百煉平臺就提供了這樣的模型,用于將文本向量化:
在這里插入圖片描述
這里選擇通用文本向量-v3,這個模型兼容OpenAI,所以我們依然采用OpenAI的配置;修改yml配置

spring:application:name: chart-robotai:ollama:# Ollama服務地址base-url: http://localhost:11434chat:# 模型名稱,可更改model: deepseek-r1:14boptions:# 模型溫度,值越大,輸出結果越隨機temperature: 0.8openai:base-url: https://dashscope.aliyuncs.com/compatible-modeapi-key: ${OPENAI_API_KEY} #API keychat:options:# 可選擇的模型列表 https://help.aliyun.com/zh/model-studio/getting-started/modelsmodel: qwen-plusembedding:options:model: text-embedding-v3  #通用文本向量-v3dimensions: 1024
向量模型測試

文本向量化以后,就可以通過向量之間的距離來判斷文本相似度;接下來,我們來測試下阿里百煉提供的向量大模型;

在項目中寫一個工具類,用以計算向量之間的歐氏距離和**余弦距離。**新建一個ai.util包,在其中新建一個VectorDistanceUtils類:

public class VectorDistanceUtils {// 私有構造函數:防止該工具類被實例化。private VectorDistanceUtils() {}// 浮點數計算精度閾值,用于判斷浮點數是否接近零。private static final double EPSILON = 1e-12;/*** 計算歐氏距離(Euclidean Distance)* 歐氏距離是兩個向量之間的直線距離,常用于衡量多維空間中兩點的距離。* @param vectorA 向量A(非空且與B等長)* @param vectorB 向量B(非空且與A等長)*/public static double euclideanDistance(float[] vectorA, float[] vectorB) {// 校驗輸入向量的合法性validateVectors(vectorA, vectorB);double sum = 0.0; // 用于累加差值平方for (int i = 0; i < vectorA.length; i++) {double diff = vectorA[i] - vectorB[i]; // 計算對應維度上的差值sum += diff * diff; // 累加差值的平方}return Math.sqrt(sum); // 返回平方和的平方根,即歐氏距離}/*** 計算余弦距離(Cosine Distance)* 余弦距離基于余弦相似度計算,表示兩個向量在方向上的差異。距離范圍為[0, 2],* 其中0表示完全相同,2表示完全相反。*/public static double cosineDistance(float[] vectorA, float[] vectorB) {// 校驗輸入向量的合法性validateVectors(vectorA, vectorB);double dotProduct = 0.0; // 點積double normA = 0.0;      // 向量A的模double normB = 0.0;      // 向量B的模// 遍歷向量的每個維度,計算點積和模的平方for (int i = 0; i < vectorA.length; i++) {dotProduct += vectorA[i] * vectorB[i]; // 點積累加normA += vectorA[i] * vectorA[i];     // A模的平方累加normB += vectorB[i] * vectorB[i];     // B模的平方累加}// 計算向量的模normA = Math.sqrt(normA);normB = Math.sqrt(normB);// 如果任意一個向量為零向量,則無法計算余弦距離,拋出異常if (normA < EPSILON || normB < EPSILON) {throw new IllegalArgumentException("Vectors cannot be zero vectors");}// 計算余弦相似度,確保結果在[-1, 1]范圍內(處理浮點誤差)double similarity = dotProduct / (normA * normB);similarity = Math.max(Math.min(similarity, 1.0), -1.0);// 余弦距離 = 1 - 相似度,范圍為[0, 2]return 1.0 - similarity;}/*** 參數校驗統一方法* 確保輸入向量滿足以下條件:* 1. 不為空(null);* 2. 長度相等;* 3. 非空數組。*/private static void validateVectors(float[] a, float[] b) {if (a == null || b == null) {throw new IllegalArgumentException("Vectors cannot be null");}if (a.length != b.length) {throw new IllegalArgumentException("Vectors must have same dimension");}if (a.length == 0) {throw new IllegalArgumentException("Vectors cannot be empty");}}
}

由于SpringBoot的自動裝配能力,剛才配置的向量模型可以直接使用;

@SpringBootTest
...
// 自動注入向量模型
@Autowired
private OpenAiEmbeddingModel embeddingModel;
@Test
void contextLoads() {// 1.測試數據// 1.1.用來查詢的文本,國際沖突String query = "global conflicts";// 1.2.用來做比較的文本String[] texts = new String[]{"哈馬斯稱加沙下階段停火談判仍在進行 以方尚未做出承諾","土耳其、芬蘭、瑞典與北約代表將繼續就瑞典“入約”問題進行談判","日本航空基地水井中檢測出有機氟化物超標","國家游泳中心(水立方):恢復游泳、嬉水樂園等水上項目運營","我國首次在空間站開展艙外輻射生物學暴露實驗",};// 2.向量化// 2.1.先將查詢文本向量化float[] queryVector = embeddingModel.embed(query);// 2.2.再將比較文本向量化,放到一個數組List<float[]> textVectors = embeddingModel.embed(Arrays.asList(texts));// 3.比較歐氏距離// 3.1.把查詢文本自己與自己比較,肯定是相似度最高的System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, queryVector));// 3.2.把查詢文本與其它文本比較for (float[] textVector : textVectors) {System.out.println(VectorDistanceUtils.euclideanDistance(queryVector, textVector));}System.out.println("------------------");// 4.比較余弦距離// 4.1.把查詢文本自己與自己比較,肯定是相似度最高的System.out.println(VectorDistanceUtils.cosineDistance(queryVector, queryVector));// 4.2.把查詢文本與其它文本比較for (float[] textVector : textVectors) {System.out.println(VectorDistanceUtils.cosineDistance(queryVector, textVector));}
}

運行結果:
在這里插入圖片描述
可以看到,向量相似度確實符合我們的預期。有了比較文本相似度的辦法,知識庫的問題就可以解決了;前面說了,知識庫數據量很大,無法全部寫入提示詞,而且龐大的知識庫中與用戶問題相關的其實并不多

所以,我們需要想辦法從龐大的知識庫中找到與用戶問題相關的一小部分,組裝成提示詞,發送給大模型就可以了

現但是新的問題來了:向量模型是生成向量的,如此龐大的知識庫,誰來從中比較和檢索數據呢? 這就需要用到向量數據庫

向量數據庫
文本向量化

由于需要將已拆分的知識片段文本存儲向量庫,以便后續可以進行檢索,而向量庫存儲的數據是向量不是文本

因此需要將文本進行向量化,即將一個字符串轉換為一個N維數組,這個過程在自然語言處理(NLP)領域稱為文本嵌入

不同的LLM對于文本嵌入的實現是不同的,ChatGPT的實現是基于transformer架構的,相關實現存儲在服務端,每次嵌入都需要訪問OpenAI的HTTP接口。

通過下面的例子可以看到OpenAi使用的模型是:text-embedding-ada-002,向量的維度是:1536

OpenAiEmbeddingModel embeddingModel = new OpenAiEmbeddingModel.OpenAiEmbeddingModelBuilder().apiKey(API_KEY).baseUrl(BASE_URL).build();
log.info("當前的模型是: {}", embeddingModel.modelName());
String text = "兩只眼睛";
Embedding embedding = embeddingModel.embed(text).content();
log.info("文本:{}的嵌入結果是:\n{}", text, embedding.vectorAsList());
log.info("它是{}維的向量", embedding.dimension());
向量庫存儲

向量數據庫,也稱為向量存儲或向量搜索引擎,是一種專門設計用于存儲和管理向量(固定長度的數字列表)及其他數據項的數據庫。

這些向量是數據點在高維空間中的數學表示,其中每個維度對應數據的一個特征。向量數據庫的主要目的是通過近似最近鄰(ANN)算法實現高效的相似性搜索。

向量數據庫的主要作用有兩個:

  • 存儲向量數據
  • 基于相似度檢索數據

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,因此操作方式一模一樣,只要學會任意一個,其它就都不是問題;

注意:除了最后一個庫,其它所有向量數據庫都是需要安裝部署的,而且每個企業用的向量庫都不一樣。

SimpleVectorStore
  • 最后一個SimpleVectorStore向量庫是基于內存實現,是一個專門用來測試、教學用的庫,非常適合此處案例的使用;
  • 修改CommonConfiguration,添加一個VectorStore的Bean
@Bean
public VectorStore vectorStore(OpenAiEmbeddingModel embeddingModel) {return SimpleVectorStore.builder(embeddingModel).build();
}
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) {SearchRequest searchRequest = SearchRequest.builder().filterExpression(filterExpression).build();Filter.Expression textExpression = searchRequest.getFilterExpression();Assert.notNull(textExpression, "Filter expression must not be null");this.delete(textExpression);}// 根據條件檢索文檔@NullableList<Document> similaritySearch(String query);// 根據條件檢索文檔@NullableList<Document> similaritySearch(SearchRequest request);default <T> Optional<T> getNativeClient() {return Optional.empty();}
}

注意,VectorStore操作向量化的基本單位是Document,在使用時需要將自己的知識庫分割轉換為一個個的Document,然后寫入VectorStore

那么問題來了,該如何把各種不同的知識庫文件轉為Document呢?

文件讀取和轉換

由于知識庫太大,所以要將知識庫拆分成文檔片段,然后再做向量化。而且SpringAI中向量庫接收的是Document類型的文檔,即我們處理文檔還要轉成Document格式

不過,文檔讀取、拆分、轉換的動作并不需要我們親自完成。在SpringAI中提供了各種文檔讀取的工具,可以參考官網:Spring AI Reference

比如PDF文檔讀取和拆分,SpringAI提供了兩種默認的拆分原則:

  • PagePdfDocumentReader:按頁拆分,推薦使用;
  • ParagraphPdfDocumentReader :按pdf的目錄拆分,不推薦,因為很多PDF不規范,沒有章節標簽;

此處選擇使用PagePdfDocumentReader。首先,在pom.xml中引入依賴:

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

然后就可以利用工具把PDF文件讀取并處理成Document了;

編寫一個單元測試

@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)  //返回最相關的前 1 個結果.similarityThreshold(0.6) //只有相似度大于等于 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());}
}

注意:啟動測試之前,要將中二知識筆記.pdf文件放到工程目錄結構下;結果如下
在這里插入圖片描述
在這里插入圖片描述

RAG原理總結

目前已經有了以下這些工具

  • PDFReader:讀取文檔并拆分為片段;
  • 向量大模型:將文本片段向量化;
  • 向量數據庫:存儲向量,檢索向量;

接下來梳理一下要解決的問題和解決思路:

  1. 要解決大模型的知識限制問題,需要外掛知識庫
  2. 受到大模型上下文限制,知識庫不能直接拼接在提示詞中;
  3. 需要從龐大的外掛知識庫中找到與用戶問題相關的一小部分,再組裝成提示詞;
  4. 這些可以利用文檔讀取器、向量大模型、向量數據庫來解決;
  5. RAG要做的事情就是將知識庫分割==>利用向量模型做向量化==>存入向量數據庫==>查詢的時候去檢索;
  6. 每當用戶詢問AI時,將用戶問題向量化==>拿著問題向量==>去向量數據庫檢索最相關的片段
  7. 對話大模型:將檢索到的片段、用戶的問題一起拼接為提示詞==> 發送提示詞給大模型,得到響應。

目標

接下來就來實現一個非常火爆的個人知識庫AI應用——ChatPDF,原網站如下:
在這里插入圖片描述
這個網站其實就是把個人的PDF文件作為知識庫,讓AI基于PDF內容來回答問題,對于大學生、研究人員、專業人士來說,非常方便。

PDF上傳下載向量化

既然是ChatPDF,即所有知識庫都是PDF形式的,由用戶提交給服務器。所以,需要先實現一個上傳PDF的接口,在接口中實現下列功能:

  • 校驗文件格式是否為PDF;
  • 保存文件信息;
    • 保存文件(可以是oss或本地保存);
    • 保存會話ID和文件路徑的映射關系(方便查詢會話歷史的時候再次讀取文件);
  • 文檔拆分和向量化(文檔太大,需要拆分為一個個片段,分別向量化);

另外,將來用戶查詢會話歷史,還需要返回pdf文件給前端用于預覽,所以需要實現一個下載PDF接口,包含下面功能:

  • 讀取文件
  • 返回文件給前端

PDF文件管理

由于將來要實現PDF下載功能,就需要記住每一個chatId對應的PDF文件名稱

所以定義一個類,記錄chatId與pdf文件的映射關系,同時實現基本的文件保存功能。在repository包中定義FileRepository接口

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);
}
@Slf4j
@Component
@RequiredArgsConstructor
public class LocalPdfFileRepository implements FileRepository {private final VectorStore vectorStore; // 向量存儲組件// 會話id 與 文件名的對應關系,方便查詢會話歷史時重新加載文件private final Properties chatFiles = new Properties();/*** 保存資源到本地磁盤,并記錄會話 ID 與文件名的映射關系。*/@Overridepublic boolean save(String chatId, Resource resource) {// 1. 獲取文件名并檢查是否已存在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;}}// 2. 保存會話 ID 與文件名的映射關系chatFiles.put(chatId, filename);return true;}/*** 根據會話 ID 獲取對應的文件資源。*/@Overridepublic Resource getFile(String chatId) {// 根據會話 ID 查找文件名String filename = chatFiles.getProperty(chatId);if (filename == null) {log.warn("No file found for chatId: {}", chatId);return null;}return new FileSystemResource(filename);}/*** 初始化方法,在 Spring 容器啟動時執行。* 加載 `chat-pdf.properties` 文件中的會話 ID 映射關系,* 并加載 `chat-pdf.json` 中的向量數據。*/@PostConstructprivate void init() {// 加載會話 ID 映射關系FileSystemResource pdfResource = new FileSystemResource("chat-pdf.properties");if (pdfResource.exists()) {  //如果文件存在try (BufferedReader reader = new BufferedReader(new InputStreamReader(pdfResource.getInputStream(), StandardCharsets.UTF_8))) {chatFiles.load(reader);  //加載chatFiles到本地文件} catch (IOException e) {throw new RuntimeException("Failed to load chat-pdf.properties", e);}}// 加載向量存儲數據FileSystemResource vectorResource = new FileSystemResource("chat-pdf.json");if (vectorResource.exists()) {SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;try {simpleVectorStore.load(vectorResource);} catch (Exception e) {throw new RuntimeException("Failed to load chat-pdf.json", e);}}}/*** 銷毀方法,在 Spring 容器關閉時執行。* 持久化會話 ID 映射關系和向量存儲數據到磁盤。*/@PreDestroyprivate void persistent() {try {// 持久化會話 ID 映射關系try (FileWriter writer = new FileWriter("chat-pdf.properties")) {chatFiles.store(writer, "Persisted at " + LocalDateTime.now());}// 持久化向量存儲數據SimpleVectorStore simpleVectorStore = (SimpleVectorStore) vectorStore;simpleVectorStore.save(new File("chat-pdf.json"));} catch (IOException e) {throw new RuntimeException("Failed to persist data", e);}}
}

此處選擇了基于內存的SimpleVectorStore,重啟就會丟失向量數據。所以這里是將pdf文件與chatId的對應關系、VectorStore都持久化到了磁盤;

實際開發中,如果選擇了RedisVectorStore,或者CassandraVectorStore,則無需自己持久化。但是chatId和PDF文件之間的對應關系,還是需要自己維護的。

上傳文件相應結果

由于前端文件上傳給后端后,后端需要返回響應結果,在ai.entity.vo中定義一個Result類:

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

文件上傳下載

ai.controller中創建一個PdfController

@Slf4j
@RequiredArgsConstructor  //配合final實現自動注入
@RestController
@RequestMapping("/ai/pdf")
public class PdfController {private final FileRepository fileRepository;  //文件存儲組件private final VectorStore vectorStore; //向量存儲組件private final ChatClient pdfChatClient;  //問答模型客戶端private final ChatHistoryRepository chatHistoryRepository; //會話歷史記錄/*** 對話*/@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")public Flux<String> chat(String prompt, String chatId) {// 1.找到會話文件Resource file = fileRepository.getFile(chatId);if (!file.exists()) {// 文件不存在,不回答throw new RuntimeException("會話文件不存在!");}// 2.保存會話idchatHistoryRepository.save("pdf", chatId);// 3.請求模型return pdfChatClient.prompt().user(prompt).advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)).advisors(a -> a.param(FILTER_EXPRESSION, "file_name == '" + file.getFilename() + "'")).stream().content();}/*** 文件上傳*/@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);}
}

上傳大小限制

SpringMVC有默認的文件大小限制,只有10M,很多知識庫文件都會超過這個值,所以我們需要修改配置,增加文件上傳允許的上限;

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

spring:servlet:multipart:# 單個文件的最大大小為100MBmax-file-size: 104857600# 整個請求的最大大小為100MBmax-request-size: 104857600

配置ChatClient

理論上來說,每次與AI對話的完整流程是這樣的:

  1. 將用戶的問題利用向量大模型做向量化OpenAiEmbeddingModel
  2. 去向量數據庫檢索相關的文檔 VectorStore
  3. 拼接提示詞,發送給大模型;
  4. 解析響應結果;
    在這里插入圖片描述
    不過,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 SimpleLoggerAdvisor(),new MessageChatMemoryAdvisor(chatMemory),  // 會話記憶new QuestionAnswerAdvisor(  vectorStore,    // 向量庫SearchRequest.builder()  // 向量檢索的請求參數.similarityThreshold(0.6)   // 相似度閾值.topK(2)     // 返回的文檔片段數量.build())).build();
}

也可以自己自定義RAG查詢的流程,不使用Advisor,具體可參考官網

對話接口

最后,對接前端與大模型對話。修改PdfController,添加一個接口:

/*** 對話
*/
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat(String prompt, String chatId) {// 1.找到會話文件Resource file = fileRepository.getFile(chatId);if (!file.exists()) {// 文件不存在,不回答throw new RuntimeException("會話文件不存在!");}// 2.保存會話idchatHistoryRepository.save("pdf", chatId);// 3.請求模型return pdfChatClient.prompt().user(prompt).advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)).advisors(a -> a.param(FILTER_EXPRESSION, "file_name == '" + file.getFilename() + "'")).stream().content();
}

測試

在這里插入圖片描述

持久化VectorStore

SpringAI提供了很多持久化的VectorStore,下面以其中兩個為例來介紹:

  • RedisVectorStore : 目前測試metafiled過濾有異常;
  • CassandraVectorStore。

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: XXX # redis地址

接下來,無需聲明bean,直接就可以直接使用VectorStore了。

CassandraVectorStore

首先,需要安裝一個Cassandra訪問,使用Docker安裝:

docker run -d --name cas -p 9042:9042  cassandra

在項目中添加cassandra依賴:

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

配置Cassandra地址:

spring:cassandra:contact-points: xxx:9042local-datacenter: datacenter1

配置VectorStore:

public CassandraVectorStore vectorStore(OpenAiEmbeddingModel embeddingModel, CqlSession cqlSession) {return CassandraVectorStore.builder(embeddingModel).session(cqlSession).addMetadataColumn(new CassandraVectorStore.SchemaColumn("file_name", DataTypes.TEXT, CassandraVectorStore.SchemaColumnTags.INDEXED)).build();
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/77814.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/77814.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/77814.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SSH 互信被破壞能導致 RAC 異常關閉嗎

一、 SSH 互信和 RAC 的關系 1、SSH 互信對 RAC 的作用 Oracle 11g R2 在安裝 Grid Infrastructure 的時候&#xff0c;能夠通過安裝程序配置節 點間的 SSH 用戶等效性&#xff0c;之所以要在安裝之前配置 SSH 用戶等效性&#xff0c;是為了能 夠在安裝前使用 C…

【數字圖像處理】立體視覺信息提取

雙目立體視覺原理 設一個為參考平面&#xff0c;一個為目標平面。增加了一個攝像頭后&#xff0c;P與Q在目標面T上有分別的成像點 雙目立體視覺&#xff1a;從兩個不同的位置觀察同一物體&#xff0c;用三角測量原理計算攝像機到該物體的距離的 方法 原理&#xff1a;三角測量…

基于springboot+vue的校園二手物品交易平臺

開發語言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服務器&#xff1a;tomcat7數據庫&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;數據庫工具&#xff1a;Navicat11開發軟件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

【Redis】Redis基本命令(1)

KEYS 返回所有滿足樣式&#xff08;pattern&#xff09;的key。 KEY * 返回所有key&#xff0c;不簡易使用 性能問題&#xff1a;當 Redis 存儲百萬級鍵時&#xff0c;會消耗大量 CPU 和內存資源&#xff0c;Redis 是單線程模型&#xff0c;KEYS * 執行期間會阻塞其他所有命令…

C#通用常見面試題-精心整理

以下是優化后的版本,在原有內容基礎上補充了應用場景,其他結構保持不變: 上位機面試題解答(技術詳解+示例) C#-IOC框架 1. 值類型和引用類型的本質區別 解釋 值類型:存儲在棧中,直接保存數據值(如 int, struct)。引用類型:存儲在堆中,變量保存對象地址(如 class,…

K8S節點出現Evicted狀態“被驅逐”

在Kubernetes集群中&#xff0c;Pod狀態為“被驅逐&#xff08;evicted&#xff09;”表示Pod無法在當前節點上繼續運行&#xff0c;已被集群從節點上移除。 問題分析&#xff1a; 節點磁盤空間不足 &#xff0c;使用df -h查看磁盤使用情況 可以看到根目錄 / 已100%滿&#x…

[密碼學基礎]國密算法深度解析:中國密碼標準的自主化之路

國密算法深度解析&#xff1a;中國密碼標準的自主化之路 國密算法&#xff08;SM系列算法&#xff09;是中國自主研發的密碼技術標準體系&#xff0c;旨在打破國際密碼技術壟斷&#xff0c;保障國家信息安全。本文將從技術原理、應用場景和生態發展三個維度&#xff0c;全面解…

Linux 網絡基礎(三) TCP/IP協議

一、TCP 與 IP 的關系 IP 層的核心作用是定位主機&#xff0c;具有將數據從主機 A 發送到主機 B 的能力&#xff0c;但是能力并不能保證一定能夠做到&#xff0c;所以這時就需要 TCP 起作用了&#xff0c;TCP 可以通過超時重傳、擁塞控制等策略來保證數據能夠發送到 B 主機。 所…

基于 Vue 的Tiptap 富文本編輯器使用指南

目錄 &#x1f9f0; 技術棧 &#x1f4e6; 所需依賴 &#x1f4c1; 文件結構 &#x1f9f1; 編輯器組件實現&#xff08;components/Editor.vue&#xff09; ? 常用操作指令 &#x1f9e0; 小貼士 &#x1f9e9; Tiptap 擴展功能使用說明&#xff08;含快捷鍵與命令&am…

統計圖表ECharts

統計某個時間段&#xff0c;觀看人數 ①、數據表 ②、業務代碼 RestController RequstMapping(value"/admin/vod/videoVisitor") CrossOrigin public class VideoVisitorController{Autowriedprivate VideoVisitorService videoVisitorService;//課程統計的接口…

ubuntu 安裝 redis server

ubuntu 安裝 redis server sudo apt update sudo apt install redis-server The following NEW packages will be installed:libhiredis0.14 libjemalloc2 liblua5.1-0 lua-bitop lua-cjson redis-server redis-toolssudo systemctl start redis-server sudo systemctl ena…

【白雪講堂】[特殊字符]內容戰略地圖|GEO優化框架下的內容全景布局

&#x1f4cd;內容戰略地圖&#xff5c;GEO優化框架下的內容全景布局 1?? 頂層目標&#xff1a;GEO優化戰略 目標關鍵詞&#xff1a; 被AI理解&#xff08;AEO&#xff09; 被AI優先推薦&#xff08;GEO&#xff09; 在關鍵場景中被AI復讀引用 2?? 三大引擎邏輯&#x…

NVIDIA 自動駕駛技術見解

前言 參與 NVIDIA自動駕駛開發者實驗室 活動&#xff0c;以及解讀了 NVIDIA 安全報告 自動駕駛 白皮書&#xff0c;本文是我的一些思考和見解。自動駕駛技術的目標是為了改善道理安全、減少交通堵塞&#xff0c;重塑更安全、高效、包容的交通生態。在這一領域&#xff0c;NVI…

OpenCV day6

函數內容接上文&#xff1a;OpenCV day4-CSDN博客 , OpenCV day5-CSDN博客 目錄 平滑&#xff08;模糊&#xff09; 25.cv2.blur()&#xff1a; 26.cv2.boxFilter(): 27.cv2.GaussianBlur()&#xff1a; 28.cv2.medianBlur(): 29.cv2.bilateralFilter()&#xff1a; 銳…

Function calling, 模態上下文協議(MCP),多步能力協議(MCP) 和 A2A的區別

背景闡述 本文大部分內容都是基于openAI 的 chatGPT自動生成。作者進行了一些細微的調整。 LLM 帶來了很多思維的活躍&#xff0c;基于LLM&#xff0c;產生了很多應用&#xff0c;很多應用也激活了LLM的新的功能。 Function calling&#xff0c;MCP&#xff08;Modal Contex…

火山RTC 5 轉推CDN 布局合成規則

實時音視頻房間&#xff0c;轉推CDN&#xff0c;文檔&#xff1a; 轉推直播--實時音視頻-火山引擎 一、轉推CDN 0、前提 * 在調用該接口前&#xff0c;你需要在[控制臺](https://console.volcengine.com/rtc/workplaceRTC)開啟轉推直播功能。<br> * 調…

力扣面試150題--插入區間和用最少數量的箭引爆氣球

Day 28 題目描述 思路 初次思路&#xff1a;借鑒一下昨天題解的思路&#xff0c;將插入的區間與區間數組作比較&#xff0c;插入到升序的數組中&#xff0c;其他的和&#xff08;合并區間&#xff09;做法一樣。 注意需要特殊處理一下情況&#xff0c;插入區間比數組中最后一…

【Java面試筆記:基礎】4.強引用、軟引用、弱引用、幻象引用有什么區別?

1. 引用類型及其特點 強引用(Strong Reference): 定義:最常見的引用類型,通過new關鍵字直接創建。回收條件:只要強引用存在,對象不會被GC回收。示例:Object obj = new Object(); // 強引用特點: 強引用是導致內存泄漏的常見原因(如未及時置為null)。手動斷開引用:…

ycsb性能測試的優缺點

YCSB&#xff08;Yahoo Cloud Serving Benchmark&#xff09;是一個開源的性能測試框架&#xff0c;用于評估分布式系統的讀寫性能。它具有以下優點和缺點&#xff1a; 優點&#xff1a; 簡單易用&#xff1a;YCSB提供了簡單的API和配置文件&#xff0c;使得性能測試非常容易…

基于SpringBoot的校園賽事直播管理系統-項目分享

基于SpringBoot的校園賽事直播管理系統-項目分享 項目介紹項目摘要管理員功能圖用戶功能圖項目預覽首頁總覽個人中心禮物管理主播管理 最后 項目介紹 使用者&#xff1a;管理員、用戶 開發技術&#xff1a;MySQLJavaSpringBootVue 項目摘要 隨著互聯網和移動技術的持續進步&…