1. 概述

本教程,我们将学习使用 Java Spring AI 框架RAG (检索增强生成) 技术实现一个聊天机器人。 借助 Spring AI 我们集成 Redis 向量数据库 保存和检索数据,从而增强 LLM(大型语言模型)的提示词(prompt)。

2. 什么是 Rag?

大型语言模型(LLM)是使用互联网上的海量数据进行预训练的机器学习模型,它是一个基础模型是通用的。要想将其应用到企业自身,必须使用行业特定领域的知识库进行微调。然而,微调通常非常耗时,且需要大量的计算资源。大语言模型还面临下面这些挑战:

  • 在没有答案的情况下容易胡编乱造,提供虚假信息
  • 当用户需要特定的当前响应时,提供过时或通用的信息
  • 从非权威来源创建响应
  • 由于术语混淆,不同的培训来源使用相同的术语来谈论不同的事情,因此会产生不准确的响应

RAG 正是用来解决这些挑战。

RAG 是 Retrieval-Augmented Generation 的缩写,中文翻译为 “检索增强生成”。主要引入了知识库的概念,在生成之前先会引用传统的网页检索结果、企业知识库和数据库等外部数据源,从而生成更准确且更符合语境的回答。

其中向量数据库 和 ETL(数据转换和加载) 在这过程中扮演着重要的角色。ETL流程图如下:

RAG ETL

上图中 Reader 负责从企业知识库中检索文档,它们可能来自不同的数据源。然后,Transformer 将检索到的文档拆分成更小的块,并使用嵌入(embedding)模型将内容提取为向量。 最后 Writer 将向量插入到向量数据库中。

下面是RAG流程图:

RAG Architecture

首先通过向量数据库进行语义搜索,然后结合用户输入 + 向量数据库返回结果进行预处理,生成提示词(prompt)。此整合增强了 LLM 的上下文,使其能够更全面地理解主题,生成贴合上下文的答案。

3. 使用 Spring AI 和 Redis 实现 RAG

市面上的向量数据库有很多,Redis 也提供了向量搜索的功能,本文我们将使用 Spring AI 将其集成构建一个基于 RAG 的 ChatBot 应用。大语言模型接口使用OpenAI的GPT-3.5 Turbo。

3.1. 前置条件

第一步,先注册一个 OpenAI 账号,获取API密钥

OpenAI Key

向量数据库我们使用Redis,Redis Cloud 提供了免费的向量库:

RedisDB

集成Redis Vector DB 和 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

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

涉及的类:

RAG Loader cld

DocumentReader 是 Spring AI 中用于读取文档的接口。 我们使用 PagePdfDocumentReader 作为其实现类。类似的, DocumentWriterVectorStore 是用于写数据的接口。我们使用其 RedisVectorStore 实现类来加载和搜索数据

3.3. 数据加载实现

DataLoaderService 类:

@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 框架读取属性配置文件自动装配 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 将数据写到向量库里。

3.4. 生成最终回答

向量库准备就绪,我们就可以检索与用户查询相关的上下文信息。之后,此上下文用于形成 LLM 的提示词以生成最终响应。让我们看看关键的类:

RAG retriever cld

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 framework 读取 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

PROMPT_BLUEPRINT 是提示词模板,限制大模型仅从给定上下文信息中生成回答。

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

现在让我们测试看看:

@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.

可以看到输出的内容与加载到向量库中的员工手册 PDF 一致。

如果问一个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 DB 是存储和对文档向量执行相似性搜索的绝佳解决方案。此外,对文档进行分块对于获取正确的记录和限制提示令牌的成本同样重要。

本文中的完整代码可从 GitHub上获取。