使用 Redis 和 Spring AI 创建 RAG(检索增强生成)应用

1、概览

在本教程中,我们将使用 Spring AI 框架和 RAG(检索增强生成)技术构建一个 ChatBot(聊天机器人)。在 Spring AI 的加持下,我们将与 Redis Vector(向量)数据库集成,以存储和检索数据,从而增强 LLM(大型语言模型)的提示功能。一旦 LLM 接收到包含相关数据的提示,它就会有效地用自然语言生成带有最新数据的响应,以回应用户的查询。

2、RAG 是什么?

LLM 是根据互联网上的大量数据集预先训练的机器学习模型。要使 LLM 在私营企业中发挥作用,我们必须根据特定组织的知识库对其进行微调。然而,微调通常是一个耗时的过程,需要大量的计算资源。此外,经过微调的 LLM 很有可能会对查询生成不相关或误导性的响应。这种行为通常被称为 LLM 幻觉(LLM Hallucinations)。

在这种情况下,RAG 是一种优秀的技术,用于限制或将 LLM 的响应置于特定上下文中。向量数据库在 RAG 架构中发挥着重要作用,为 LLM 提供上下文信息。但是,在 RAG 架构中使用矢量(向量)数据库之前,应用必须通过 ETL(提取、转换和加载)流程对其进行填充:

RAG 流程

Reader 从不同源检索组织的知识库文档。然后,Transformer(转换器)将检索到的文档分割成小块,并使用嵌入模型对内容进行矢量化。最后,Writer 将向量或 Embedding 加载到向量数据库。向量数据库是专门的数据库,可以在多维空间中存储这些 Embedding

在 RAG 中,如果矢量数据库定期从组织的知识库中更新,那么 LLM 就能对几乎实时的数据做出响应。

一旦矢量数据库中的数据准备就绪,应用就可以使用它来检索用户查询的上下文数据:

矢量数据库

应用将用户查询与矢量数据库中的上下文数据相结合形成提示,最后将其发送给 LLM。LLM 在上下文数据的范围内用自然语言生成回复,并将其发送回应用。

3、使用 Spring AI 和 Redis 实现 RAG

Redis Stack 提供矢量搜索服务,我们将使用 Spring AI 框架与之集成,并构建一个基于 RAG 的 ChatBot(聊天机器人)应用。此外,我们还要使用 OpenAI 的 GPT-3.5 Turbo LLM 模型来生成最终响应。

3.1、先决条件

对于 ChatBot,需要 API Key 来认证 OpenAI 服务。在创建 OpenAI 账号 之后,创建一个API Key。

还要创建一个 Redis Cloud 账户,以访问免费的 Redis Vector DB:

Redis Cloud

我们使用 Spring AI 整合 Redis 向量数据库和 OpenAI 服务。

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>

3.2、将数据加载 Redis 的关键类

在 Spring Boot 应用中,我们要创建用于从 Redis Vector DB 加载和检索数据的组件。例如,将把员工手册 PDF 文档加载到 Redis 数据库中。

所涉及的关键类如下:

将数据加载 Redis 的关键类

DocumentReader 是 Spring AI 用于读取文档的接口。我们使用 DocumentReader 开箱即用的实现,PagePdfDocumentReader。同样,DocumentWriterVectorStore 也是将数据写入存储系统的接口。RedisVectorStoreVectorStore 的众多开箱即用实现之一,我们用它来加载和搜索 Redis Vector DB 中的数据。

我们使用前面介绍过的 Spring AI 框架类来编写 DataLoaderService

3.3、实现 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 向量数据库。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 向量数据库中。

3.4、生成最终回复的关键类

Redis 向量数据库准备就绪后,我们就可以检索与用户查询相关的上下文信息。之后,这些上下文信息将用于形成提示,供 LLM 生成最终响应。

来看看关键的类:

LLM 生成最终回复的关键类

DataRetrievalService 类中的 searchData() 方法会接收查询,然后从 VectorStore 中检索上下文数据。ChatBotService 利用这些数据,使用 PromptTemplate 类形成提示,然后将其发送给 OpenAI 服务。Spring Boot 框架会从 application.yml 文件中读取与 OpenAI 相关的属性,然后自动配置 OpenAIChatModel 对象。

下面来详细了解一下实现过程。

3.5、实现 ChatBotService

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 向量存储相关属性...
    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 KEY,这是一种更为安全的方式。我们可以启用以 “embedding” 文本开头的 KEY 来创建 OpenAiEmbeddingModel Bean,该 Bean 用于将知识库文档转换为向量 Embedding

对于 OpenAI 服务的提示必须是明确的。因此,在提示蓝图 PROMPT_BLUEPRINT 中,我们严格要求仅从上下文信息中形成响应

chat() 方法中,我们从 Redis 向量数据库中检索与查询匹配的文档。然后,使用这些文档和用户查询在 createPrompt() 方法中生成提示。最后,调用 ChatModel 类的 call() 方法来接收 OpenAI 服务的响应。

现在,让我们通过向之前加载到 Redis Vector 数据库中的员工手册问一个问题,来检查聊天机器人服务的运行情况:

@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 向量数据库中的员工手册 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 在所提供的上下文中找不到任何内容,因此无法回答询问。

4、总结

本文介绍了使用 Spring AI 框架实现基于 RAG 架构的应用。利用上下文信息形成提示对于从 LLM 生成正确的响应至关重要。因此,Redis Vector(向量)数据库是存储和执行文档向量相似性搜索的绝佳解决方案。此外,对文档进行分块处理对于获取正确的记录和限制提示 Token 的成本也同样重要。


Ref:https://www.baeldung.com/spring-ai-redis-rag-app