Ohhnews

分类导航

$ cd ..
Baeldung原文

Spring AI模块化RAG中的查询转换实践

#spring ai#检索增强生成#查询转换#大语言模型#向量数据库

[LOADING...]

1. 概述

检索增强生成(RAG)会从知识库(例如向量数据库)中查询相关文档,然后将这些文档作为提示的一部分提供给 LLM,以便其生成更准确的响应。

Spring AI 提供了一种模块化 RAG 架构,将 RAG 分为四个阶段:

  • 预检索
  • 检索
  • 检索后
  • 生成

在本教程中,我们将重点介绍预检索阶段的查询转换,该阶段会在检索前处理用户查询。

2. 为什么需要转换查询?

从向量存储中检索本质上是以查询嵌入作为搜索键,在数据存储嵌入上进行相似度搜索。

因此,检索到的信息的相关性取决于查询措辞。 例如,某个查询可能因为以下原因而成为低效的搜索键:

  • 缺乏清晰性——查询可能存在歧义,或者过于冗长并提及许多概念,其中很多与搜索无关。在这两种情况下,查询的嵌入与相关文档的对齐程度都较低。
  • 语言不匹配——即使内容确实存在,使用与存储文档不同语言的查询也可能无法产生匹配。如果嵌入模型并未在查询所用语言的文档上训练过,情况会更糟。
  • 缺少上下文——长对话中的追问,例如“And how does it support RAG?”,依赖于之前的消息。用户知道“it”指什么,但 RAG 系统只会使用最新消息进行搜索。在没有聊天历史的情况下,该查询会有歧义。

查询转换器使用 LLM 在检索前重写查询,期望以此提高检索数据的相关性。

3. 设置

让我们设置项目。首先,需要 Spring AI RAG 依赖来使用模块化 RAG:

$ xml
<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-rag</artifactId>
</dependency>

由于每个查询转换器都需要一个连接 LLM 的聊天模型,我们将使用 Spring AI OpenAI 依赖:

$ xml
<dependency>
   <groupId>org.springframework.ai</groupId>
   <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

现在,在 application.yml 中配置 OpenAI API 密钥和聊天模型:

$ config
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini
          temperature: 0.0

较高的温度会让模型更具创造性,因此同一个用户查询在每次调用中可能被转换成不同结果。因此,Spring AI 文档建议使用较低的温度,例如 0.0,以获得更具确定性的结果。

4. 模块化 RAG

在 Spring AI 中,RetrievalAugmentationAdvisor 将 RAG 各阶段串联起来。一旦将其注册到 ChatClient,每个提示都会经过完整流水线,例如:

$ java
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
  .queryTransformers(
    RewriteQueryTransformer.builder()
      .chatClientBuilder(chatClientBuilder)
      .build(),
    TranslationQueryTransformer.builder()
      .chatClientBuilder(chatClientBuilder)
      .targetLanguage("english")
      .build(),
    CompressionQueryTransformer.builder()
      .chatClientBuilder(chatClientBuilder)
      .build())
  .documentRetriever(VectorStoreDocumentRetriever.builder()
    .vectorStore(vectorStore)
    .topK(3)
    .build())
  .build();
ChatClient chatClient = chatClientBuilder.defaultAdvisors(ragAdvisor)
  .build();

这里,vectorStore 是一个 VectorStore 实例,保存我们已嵌入的文档,例如由 Oracle 向量数据库作为后端。

RetrievalAugmentationAdvisor 的构建器为每个 RAG 阶段至少暴露一个方法。在我们的示例中,为预检索阶段配置了 queryTransformers(),为检索阶段配置了 documentRetriever() 方法。让我们重点看前者。

queryTransformers() 方法接受多个查询转换器。 它们实现 QueryTransformer 接口,并按顺序运行,每个转换器接收前一个转换器的输出。每个转换器都会发起自己的 LLM 调用,这需要一个 ChatClient.Builder。

Spring AI 提供了三个查询转换器。为了分别演示每个转换器,我们将在测试类中为每个转换器实现一个测试方法:

$ java
@SpringBootTest
class PreRetrievalQueryTransformerLiveTest {
    private static final Logger logger = LoggerFactory.getLogger(PreRetrievalQueryTransformerLiveTest.class);
    @Autowired
    private ChatClient.Builder chatClientBuilder;
}

让我们逐一查看。

5. RewriteQueryTransformer

RewriteQueryTransformer 会过滤掉查询中的无关信息。 通过只保留相关部分,它会生成简洁文本,我们期望这样的文本能找到更好的结果。 当用户查询冗长且不清晰时,该转换器很有用。

作为一个示例,让我们展示它如何处理冗长查询:

$ java
@Test
void whenQueryIsVerbose_thenRewrite() {
    QueryTransformer queryTransformer = RewriteQueryTransformer.builder()
      .chatClientBuilder(chatClientBuilder)
      .build();
    String userQuery = "I'm studying machine learning and honestly the whole thing is confusing. What is an LLM anyway?";
    Query transformed = queryTransformer.transform(new Query(userQuery));
    logger.info("Rewrite: [{}] -> [{}]", userQuery, transformed.text());
}

执行测试时,我们可以看到以下或类似日志:

$ plaintext
Rewrite: [I'm studying machine learning and honestly the whole thing is confusing. What is an LLM anyway?] -> [What is an LLM in machine learning?]

该转换器将机器学习和 LLM 识别为需要保留的重要上下文,并丢弃用户介绍中与搜索无关的内容。

6. TranslationQueryTransformer

嵌入模型在其训练所用语言上的表现最佳。例如,如果大部分训练数据是英文,那么模型对英文文本的表示会比其他书面语言更精确。因此,即使用中文或法文等其他语言编写的查询与以英文存储的文档含义相同,也可能与文档对齐不佳。

TranslationQueryTransformer 通过将用户查询翻译成目标语言来解决这个问题:目标语言就是我们数据存储中文档所使用的语言。

为了看到这一点,让我们添加一个新的测试方法,将繁体中文查询 什麼是向量資料庫?(意为“What is a vector database?”)发送给转换器:

$ java
@Test
void whenQueryIsInTraditionalChinese_thenTranslate() {
    QueryTransformer queryTransformer = TranslationQueryTransformer.builder()
      .chatClientBuilder(chatClientBuilder)
      .targetLanguage("english")
      .build();
    String userQuery = "什麼是向量資料庫?";
    Query transformed = queryTransformer.transform(new Query(userQuery));
    logger.info("Translation: [{}] -> [{}]", userQuery, transformed.text());
}

我们使用 targetLanguage() 方法显式指定目标语言。日志显示该中文查询已被正确翻译:

$ plaintext
Translation: [什麼是向量資料庫?] -> [What is a vector database?]

如果查询已经是目标语言,或者使用的是 LLM 无法识别的语言,那么查询会原样返回。例如,如果我们使用英文查询,输出会完全相同:

$ plaintext
Translation: [What is a vector database?] -> [What is a vector database?]

同样,由未知语言编造词语组成的查询也会原样通过:

$ plaintext
Translation: [Gblorvak tensig quimbo darnel?] -> [Gblorvak tensig quimbo darnel?]

7. CompressionQueryTransformer

用户查询通常会指代之前的消息,并且自身不包含完整信息。因此,聊天中的单条查询可能有歧义,并且无法从向量存储中返回任何有用内容。

CompressionQueryTransformer 通过压缩对话历史,并使用它来重写查询,以解决可能的歧义。

与前两个转换器不同,它需要 Query 的历史记录,因此必须同时提供查询文本和历史记录:

$ java
@Test
void whenQueryIsFollowUpWithHistory_thenCompress() {
    QueryTransformer queryTransformer = CompressionQueryTransformer.builder()
      .chatClientBuilder(chatClientBuilder)
      .build();
    List<Message> history = List.of(
      new UserMessage("What is Spring AI?"),
      new AssistantMessage("Spring AI is an application framework that brings Spring's design principles to AI engineering."));
    
    String followUpQuery = "And how does it support RAG?";
    
    Query query = Query.builder()
      .text(followUpQuery)
      .history(history)
      .build();
    Query transformed = queryTransformer.transform(query);
    logger.info("Compression: [{}] -> [{}]", followUpQuery, transformed.text());
}

这里,我们显式地将对话历史构造为消息列表。执行测试应产生类似下面的日志:

$ plaintext
Compression: [And how does it support RAG?] -> [How does Spring AI support Retrieval-Augmented Generation (RAG)?]

由于“it”,原始查询单独来看并不清晰。该转换器会将其与对话历史压缩成一个新查询,其中“it”的含义变得明确。

在真实应用中,ChatClient 默认不会保留之前的消息,因此我们需要向 RAG advisor 注册一个聊天记忆 advisor。 否则,转换器没有可用的历史记录。

然而,聊天历史并不总是相关,因为用户可能在对话过程中切换主题。让我们用一个不相关的查询再次运行同一个测试:

String followUpQuery = "What's a good recipe for carbonara?";

转换器应识别出主题切换,并且不包含之前消息中的任何内容:

$ plaintext
Compression: [What's a good recipe for carbonara?] -> [Can you provide a good recipe for carbonara?]

8. 查询转换器注意事项

所有查询转换器都有以下共同特征:

  • 它们按顺序运行,每个转换器接收前一个转换器的输出。
  • 每个转换器都会增加一次 LLM 调用。
  • 温度需要较低(例如 0.0),以确保结果更具确定性。

由于额外的 LLM 调用,整体延迟会累加。因此,我们应该只启用实际需要的转换器:

  • RewriteQueryTransformer 简化冗长且不清晰的查询,使其更清晰。
  • TranslationQueryTransformer 将查询翻译成数据库所使用的语言。
  • CompressionQueryTransformer 将查询及其历史记录压缩并合并成一个新查询,其中包含来自聊天历史的相关上下文。

9. 结论

在本文中,我们解释了 Spring AI 模块化 RAG 架构预检索阶段中的查询转换。

Spring AI 提供的三个 QueryTransformer 实现会转换查询,以提高检索性能。

一如既往,完整源代码可在 GitHub 上获取。