使用Spring AI + Redis 创建RAG应用

在本教程中,我们将使用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 在提供的上下文中找不到任何内容,因此无法回答查询。