在本教程中,我们将使用Spring AI 框架和RAG(检索增强生成)技术构建一个 ChatBot。借助 Spring AI,我们将与Redis Vector 数据库集成以存储和检索数据,以增强LLM(大型语言模型)的提示。一旦 LLM 收到包含相关数据的提示,它就会有效地生成包含最新数据的自然语言响应,以响应用户查询。
什么是 RAG?
LLM 是在互联网上大量数据集上预先训练的机器学习模型。要使 LLM 在私营企业中发挥作用,我们必须使用特定于组织的知识库对其进行微调。然而,微调通常是一个耗时的过程,需要大量的计算资源。此外,微调后的 LLM 很有可能对查询产生不相关或误导性的响应。这种行为通常被称为 LLM 幻觉。
在这种情况下,RAG 是一种限制或情境化 LLM 响应的绝佳技术。向量数据库在 RAG 架构中起着重要作用,可以为 LLM 提供上下文信息。但是,在应用程序可以在 RAG 架构中使用它之前,必须先进行 ETL(提取、转换和加载)过程来填充它:
- ETL读取器从不同来源检索组织的知识库文档。
- 然后,ETL转换器将检索到的文档拆分成更小的块,并使用嵌入模型对内容进行向量化。
- 最后,ETL写入器将向量或嵌入加载到向量数据库中。
- 向量数据库是可以将这些嵌入存储在多维空间中的专用数据库。
在 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 在提供的上下文中找不到任何内容,因此无法回答查询。