使用 Quarkus 构建本地 RAG 应用程序

这篇博文演示了如何使用 Quarkus、LangChain4j、InfinispanGranite LLM 构建一个注入 AI 的聊天机器人应用程序。在这篇文章中,我们将创建一个**完全**本地的解决方案,无需任何云服务,包括 LLM。

我们的聊天机器人利用 Granite LLM,这是一种根据用户提示生成上下文相关文本的语言模型。要在本地运行 Granite,我们将使用 InstructLab,尽管 Podman AI Lab 也是一个可行的选择。

我们应用程序的核心基于 RAG(检索增强生成)模式。这种方法通过在生成响应之前从向量数据库(在本例中为 Infinispan)检索相关信息来增强聊天机器人的响应。

架构

我们的聊天机器人应用程序由四个主要组件组成

  1. WebSocket 端点:此组件充当聊天机器人后端与前端界面之间的通信桥梁。此组件使用新的 Quarkus WebSocket-Next 扩展来高效处理 WebSocket 连接。它依赖于 AI 服务与 LLM 进行交互。

  2. Ingestor:Ingestor 负责用相关数据填充数据库。它处理一组本地文档,将它们分割成文本片段,计算它们的向量表示,并将它们存储到 Infinispan 中。

  3. Retriever:Retriever 允许在 Infinispan 中查找相关的文本片段。当用户输入查询时,Retriever 会搜索向量数据库以查找最相关的信息片段。

  4. AI 服务:这是聊天机器人的核心组件,结合了 Retriever 和 Granite LLM 的功能。AI 服务接收 Retriever 获取的相关信息,并使用 Granite LLM 生成适当的响应。

下图说明了高层架构

Architecture of the application

RAG 模式的简单解释

RAG(检索增强生成)模式是最受欢迎的 AI 模式之一,它结合了检索机制和生成机制,以提供更准确、更相关的响应。

RAG 模式主要分两个步骤进行

  1. Ingestion:应用程序摄取一组文档,处理它们,并将它们存储在向量数据库中。

  2. Retrieval:当用户输入查询时,应用程序从向量数据库中检索最相关的信息。

下图概述了传统 RAG 模式

The traditional RAG pattern

RAG 模式有更高级的版本,但在此应用程序中,我们将坚持基本原理。

Ingestion

让我们先看看 ingestion 步骤。ingestion 过程包括读取一组文档,将它们分割成文本片段,计算它们的向量表示,并将它们存储在 Infinispan 中。

有效的 RAG 实现的秘诀在于文本片段的计算方式。在我们的应用程序中,我们将遵循一种直接的方法,但有更高级的技术可用,并且通常是必需的。根据文档,您可以使用多种技术将文本分割成片段,例如段落分割、句子分割或更高级的技术,如recursive 分割器。此外,如果文档具有特定结构,您可以使用该结构将文本分割成片段(如节、章等)。

嵌入模型负责将文本转换为向量表示。为简单起见,我们使用了一个进程内嵌入模型(BGE-small)。也有其他选项,例如 Universal Angle Embedding,但为了简单起见,我们将坚持使用 BGE-small。

Retrieval

第二步是检索过程。当用户输入查询时,应用程序会在向量数据库中搜索以查找最相关的文本片段。

为了实现这一点,应用程序使用**相同的**嵌入模型计算用户查询的向量表示,并将其与 Infinispan 中存储的文本片段的向量表示进行比较。它根据查询向量和文本片段向量之间的相似性来选择最相关的文本片段。

然后,它使用检索到的文本片段增强用户查询,并将其发送给 LLM。请注意,直到这一步,LLM 才会被使用。

实现聊天机器人

理论讲够了——让我们深入了解实现。您可以在 GitHub 上找到最终版本。

使用的扩展和依赖项

为了实现我们的聊天机器人,我们依赖以下 Quarkus 扩展

  • quarkus-langchain4j-openai:使用 OpenAI API 集成 LLM 提供程序,适用于 InstructLab 和 Podman AI Lab。

  • quarkus-websockets-next:提供 WebSocket 通信支持。

  • quarkus-langchain4j-infinispan:将 Infinispan 与 LangChain4j 集成,允许我们存储和检索文本片段的向量表示。

  • quarkus-web-bundler:将前端资源与 Quarkus 应用程序打包。

我们还需要一个特定的依赖项来使用 BGE-small 嵌入模型

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-embeddings-bge-small-en-q</artifactId>
</dependency>

配置

我们需要一些配置来确保我们的应用程序使用 Granite 并正确设置 Infinispan 数据库

# Configure the Infinispan vectors:
quarkus.langchain4j.infinispan.dimension=384 (1)

# Configure the OpenAI service to use instruct lab:
quarkus.langchain4j.openai.base-url=https://:8000/v1 (2)
quarkus.langchain4j.openai.timeout=60s

quarkus.langchain4j.embedding-model.provider=dev.langchain4j.model.embedding.BgeSmallEnQuantizedEmbeddingModel (3)
1 配置存储在 Infinispan 中的向量的维度,这取决于嵌入模型(在本例中为 BGE-small)。
2 配置 OpenAI 服务以使用 InstructLab。如果您愿意,可以将基础 URL 替换为 Podman AI Lab 的 URL。实际上,InstructLab 和 Podman AI Lab 都暴露了一个与 OpenAI 兼容的 API。
3 将默认嵌入模型设置为 BGE-small。

Ingestor

配置完成后,让我们来实现 ingestion 部分(Ingestion.java)。Ingestor 从 `documents` 目录读取文档,将它们分割成文本片段,计算它们的向量表示,并将它们存储在 Infinispan 中。

@Singleton
@Startup (1)
public class Ingestion {

    public Ingestion(EmbeddingStore<TextSegment> store, EmbeddingModel embedding) { (2)

        EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
                .embeddingStore(store)
                .embeddingModel(embedding)
                .documentSplitter(recursive(1024, 0))  (3)
                .build();

        Path dir = Path.of("documents");
        List<Document> documents = FileSystemDocumentLoader.loadDocuments(dir);
        Log.info("Ingesting " + documents.size() + " documents");

        ingestor.ingest(documents);

        Log.info("Document ingested");
    }

}
1 @Startup 注解确保 ingestion 过程在应用程序启动时开始。
2 Ingestion 类使用(自动注入的)EmbeddingStore<TextSegment>(Infinispan)和 EmbeddingModel(BGE-small)。
3 我们使用简单的文档分割器(recursive(1024, 0))将文档分割成文本片段。可以使用更高级的技术来提高 RAG 模型的准确性。

Retriever

接下来,让我们来实现 retriever(Retriever.java)。Retriever 根据用户查询在 Infinispan 中查找最相关的文本片段。

@Singleton
public class Retriever implements Supplier<RetrievalAugmentor> {

    private final DefaultRetrievalAugmentor augmentor;

    Retriever(EmbeddingStore<TextSegment> store, EmbeddingModel model) {
        EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
                .embeddingModel(model)
                .embeddingStore(store)
                .maxResults(2) // Large segments
                .build();
        augmentor = DefaultRetrievalAugmentor
                .builder()
                .contentRetriever(contentRetriever)
                .build();
    }

    @Override
    public RetrievalAugmentor get() {
        return augmentor;
    }

}

为了实现 retriever,公开一个实现 Supplier<RetrievalAugmentor> 接口的 bean。Retriever 类使用 EmbeddingStore<TextSegment>(Infinispan)和 EmbeddingModel(BGE-small)来构建 retriever。

EmbeddingStoreContentRetriever builder 中的 maxResults 方法指定要检索的文本片段数量。由于我们的片段很大,因此我们只检索两个片段。

AI 服务

AI 服务(ChatBot.java)是我们聊天机器人的核心组件,它结合了 retriever 和 Granite LLM 的功能来生成适当的响应。

使用 Quarkus,实现 AI 服务非常简单

@RegisterAiService(retrievalAugmentor = Retriever.class) (1)
@SystemMessage("You are Mona, a chatbot answering question about a museum. Be polite, concise and helpful.") (2)
@SessionScoped (3)
public interface ChatBot {

    String chat(String question); (4)

}
1 @RegisterAiService 注解指定要使用的 retrieval augmentor,在本例中是我们之前定义的 Retriever bean。
2 @SystemMessage 注解为 AI 模型提供了主要的指令。
3 @SessionScoped 注解确保 AI 服务是有状态的,在用户交互之间保持上下文,以获得更具吸引力的对话。
4 ChatBot 接口定义了一个名为 chat 的单个方法,该方法接受用户问题作为输入并返回聊天机器人的响应。

WebSocket 端点

最后一块是 WebSocket 端点(ChatWebSocket.java),它充当聊天机器人后端与前端界面之间的通信桥梁。

@WebSocket(path = "/chat") (1)
public class ChatWebSocket {

    @Inject ChatBot bot; // Inject the AI service

    @OnOpen (2)
    String welcome() {
        return "Welcome, my name is Mona, how can I help you today?";
    }

    @OnTextMessage (3)
    String onMessage(String message) {
        return bot.chat(message);
    }

}
1 @WebSocket 注解指定 WebSocket 路径。
2 @OnOpen 方法在用户连接到WebSocket 时发送欢迎消息。
3 @OnTextMessage 方法使用注入的 AI 服务处理用户的消息并返回聊天机器人的响应。

就是这样!我们的聊天机器人现在已准备好与用户聊天,根据 RAG 模式提供上下文相关的响应。

运行应用程序

让我们运行应用程序,看看我们的聊天机器人在起作用。首先,克隆 repository 并运行以下命令

./mvnw quarkus:dev

此命令以开发模式启动 Quarkus 应用程序。请确保您正在运行 InstructLab 或 Podman AI Lab 以使用 Granite LLM。您还需要 Docker 或 Podman 来自动启动 Infinispan。

Podman AI Lab 还是 InstructLab?

您可以使用 Podman AI Lab 或 InstructLab 在本地运行 Granite LLM。根据操作系统,Podman 可能没有 GPU 支持。因此,响应时间可能很高。在这种情况下,InstructLab 是更好的选择,以获得更快的响应时间。通常,在 Mac 上,您会使用 InstructLab,而在 Linux 上,Podman AI Lab 会显示出出色的性能。

应用程序启动并运行后,打开您的浏览器并导航到 https://:8080。您应该会看到聊天机器人界面,您可以在其中开始与 Mona 聊天。

The Mon chatbot

总结

就是这样!只需几行代码,我们就使用 RAG 模式实现了一个聊天机器人,它结合了 Granite LLM、Infinispan 和 Quarkus 的功能。此应用程序完全在本地运行,无需任何云服务,并解决了隐私问题。

这只是使用 Quarkus LangChain4j 扩展可以实现的一个示例。您可以轻松地通过添加更高级的功能来扩展此应用程序,例如复杂的文档分割器、嵌入模型或检索机制。Quarkus LangChain4J 还支持高级 RAG、许多其他 LLM 和嵌入模型以及向量存储。在 Quarkus LangChain4J 上查找更多信息。