使用Spring AI + Redis 建立RAG應用

banq發表於2024-07-03

在本教程中,我們將使用Spring AI 框架和RAG(檢索增強生成)技術構建一個 ChatBot。藉助 Spring AI,我們將與Redis Vector 資料庫整合以儲存和檢索資料,以增強LLM(大型語言模型)的提示。一旦 LLM 收到包含相關資料的提示,它就會有效地生成包含最新資料的自然語言響應,以響應使用者查詢。

什麼是 RAG?
LLM 是在網際網路上大量資料集上預先訓練的機器學習模型。要使 LLM 在私營企業中發揮作用,我們必須使用特定於組織的知識庫對其進行微調。然而,微調通常是一個耗時的過程,需要大量的計算資源。此外,微調後的 LLM 很有可能對查詢產生不相關或誤導性的響應。這種行為通常被稱為 LLM 幻覺。

在這種情況下,RAG 是一種限制或情境化 LLM 響應的絕佳技術。向量資料庫在 RAG 架構中起著重要作用,可以為 LLM 提供上下文資訊。但是,在應用程式可以在 RAG 架構中使用它之前,必須先進行 ETL(提取、轉換和載入)過程來填充它:

  1. ETL讀取器從不同來源檢索組織的知識庫文件。
  2. 然後,ETL轉換器將檢索到的文件拆分成更小的塊,並使用嵌入模型對內容進行向量化。
  3. 最後,ETL寫入器將向量或嵌入載入到向量資料庫中。
  4. 向量資料庫是可以將這些嵌入儲存在多維空間中的專用資料庫。

在 RAG 中,如果向量資料庫定期從組織的知識庫更新,LLM 就可以響應幾乎實時的資料。

一旦向量資料庫準備好資料,應用程式就可以使用它來檢索使用者查詢的上下文資料:

  • 應用程式將使用者查詢與向量資料庫中的上下文資料相結合形成提示,並最終將其傳送給 LLM。
  • LLM在上下文資料的邊界內以自然語言生成響應並將其傳送回應用程式。

使用 Spring AI 和 Redis 實現 RAG
Redis 堆疊提供向量搜尋服務,我們將使用 Spring AI 框架與其整合並構建基於 RAG 的 ChatBot 應用程式。此外,我們將使用 OpenAI 的 GPT-3.5 Turbo LLM 模型來生成最終響應。

先決條件
對於 ChatBot 服務,為了驗證 OpenAI 服務,我們需要 API 金鑰。在建立OpenAI 帳戶後,我們將建立一個。

我們還將建立一個Redis Cloud帳戶來訪問免費的 Redis Vector DB。

為了與 Redis Vector DB 和 OpenAI 服務整合,我們將使用 Spring AI 庫更新Maven 依賴項:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M1</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>
    <version>1.0.0-M1</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-redis-spring-boot-starter</artifactId>
    <version>1.0.0-M1</version>
</dependency>
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
    <version>1.0.0-M1</version>
</dependency>

將資料載入到 Redis 中的關鍵類
在 Spring Boot 應用程式中,我們將建立用於從 Redis Vector DB 載入和檢索資料的元件。例如,我們將員工手冊 PDF 文件載入到 Redis DB 中。

  • DocumentReader是用於讀取文件的 Spring AI 介面。
  • 我們將使用開箱即用的PagePdfDocumentReader實現DocumentReader。

同樣,

  • DocumentWriter和VectorStore是用於將資料寫入儲存系統的介面。
  • RedisVectorStore是VectorStore的眾多開箱即用實現之一,我們將使用它在 Redis Vector DB 中載入和搜尋資料。

我們將使用迄今為止討論過的 Spring AI 框架類編寫DataLoaderService。

實現資料載入器服務
讓我們瞭解一下DataLoaderService類中的load()方法:

@Service
public class DataLoaderService {
    private static final Logger logger = LoggerFactory.getLogger(DataLoaderService.class);
    @Value("classpath:/data/Employee_Handbook.pdf")
    private Resource pdfResource;
    @Autowired
    private VectorStore vectorStore;
    public void load() {
        PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(this.pdfResource,
            PdfDocumentReaderConfig.builder()
              .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
                .withNumberOfBottomTextLinesToDelete(3)
                .withNumberOfTopPagesToSkipBeforeDelete(1)
                .build())
            .withPagesPerDocument(1)
            .build());
        var tokenTextSplitter = new TokenTextSplitter();
        this.vectorStore.accept(tokenTextSplitter.apply(pdfReader.get()));
    }
}

load ()方法使用PagePdfDocumentReader類讀取 PDF 檔案並將其載入到 Redis Vector DB。Spring AI 框架使用名稱空間spring.ai.vectorstore中的配置屬性自動配置VectoreStore介面:

spring:
  ai:
    vectorstore:
      redis:
        uri: redis://:PQzkkZLOgOXXX@redis-19438.c330.asia-south1-1.gce.redns.redis-cloud.com:19438
        index: faqs
        prefix: "faq:"
        initialize-schema: true

該框架將RedisVectorStore物件( VectorStore介面的實現)注入到DataLoaderService中。

TokenTextSplitter類分割文件,最後VectorStore類將塊載入到 Redis Vector DB 中。

生成最終響應的關鍵類
一旦 Redis Vector DB 準備就緒,我們就可以檢索與使用者查詢相關的上下文資訊。之後,此上下文用於形成 LLM 的提示以生成最終響應。讓我們看看關鍵的類:

  • DataRetrievalService類中的searchData ()方法接收查詢,然後從 VectorStore 檢索上下文資料。
  • ChatBotService使用此資料透過PromptTemplate類形成提示,然後將其傳送到 OpenAI 服務。
  • Spring Boot 框架從application.yml檔案中讀取與 OpenAI 相關的相關屬性,然後自動配置OpenAIChatModel物件。

讓我們直接進入實現部分來詳細瞭解。

實現聊天機器人服務
我們來看看ChatBotService類:

@Service
public class ChatBotService {
    @Autowired
    private ChatModel chatClient;
    @Autowired
    private DataRetrievalService dataRetrievalService;
    private final String PROMPT_BLUEPRINT = """
      Answer the query strictly referring the provided context:
      {context}
      Query:
      {query}
      In case you don't have any answer from the context provided, just say:
      I'm sorry I don't have the information you are looking for.
    """;
    public String chat(String query) {
        return chatClient.call(createPrompt(query, dataRetrievalService.searchData(query)));
    }
    private String createPrompt(String query, List<Document> context) {
        PromptTemplate promptTemplate = new PromptTemplate(PROMPT_BLUEPRINT);
        promptTemplate.add("query", query);
        promptTemplate.add("context", context);
        return promptTemplate.render();
    }
}

SpringAI 框架使用名稱空間spring.ai.openai中的OpenAI配置屬性建立ChatModel bean :

spring:
  ai:
    vectorstore:
      redis:
        # Redis vector store related properties...
    openai:
      temperature: 0.3
      api-key: ${SPRING_AI_OPENAI_API_KEY}
      model: gpt-3.5-turbo
      embedding-base-url: https://api.openai.com
      embedding-api-key: ${SPRING_AI_OPENAI_API_KEY}
      embedding-model: text-embedding-ada-002

該框架還可以從環境變數SPRING_AI_OPENAI_API_KEY中讀取 API 金鑰,這是一個非常安全的選項。我們可以啟用以文字嵌入開頭的金鑰來建立 OpenAiEmbeddingModel bean,該 bean 用於從知識庫文件中建立向量嵌入。

OpenAI 服務的提示必須明確。因此,我們在提示藍圖PROMPT_BLUEPRINT中嚴格指示僅從上下文資訊中形成響應。

在chat()方法中,我們檢索與 Redis Vector DB 中的查詢匹配的文件。然後,我們使用這些文件和使用者查詢在 createPrompt ()方法中生成提示。最後,我們呼叫ChatModel類的call()方法來接收來自 OpenAI 服務的響應。

現在,讓我們透過向聊天機器人服務詢問之前載入到 Redis Vector DB 中的員工手冊中的一個問題來檢查聊天機器人服務的實際執行情況:

@Test
void whenQueryAskedWithinContext_thenAnswerFromTheContext() {
    String response = chatBotService.chat("How are employees supposed to dress?");
    assertNotNull(response);
    logger.info("Response from LLM: {}", response);
}

然後,我們將看到輸出:

Response from LLM: Employees are supposed to dress appropriately for their individual work responsibilities and position.

輸出與載入到 Redis Vector DB 中的員工手冊 PDF 文件一致。

讓我們看看如果我們詢問員工手冊中沒有的內容會發生什麼:

@Test
void whenQueryAskedOutOfContext_thenDontAnswer() {
    String response = chatBotService.chat("What should employees eat?");
    assertEquals("I'm sorry I don't have the information you are looking for.", response);
    logger.info("Response from the LLM: {}", response);
}

以下是最終的輸出:

Response from the LLM: I'm sorry I don't have the information you are looking for.

LLM 在提供的上下文中找不到任何內容,因此無法回答查詢。

相關文章