第 06 步 - 解构 RAG 模式
在上一步中,我们使用 EasyRag 在我们的 AI 服务中实现了 RAG(检索增强生成)模式。EasyRag 隐藏了大部分复杂性。
在这一步中,我们将解构 RAG 模式,以了解其内部工作原理。我们将了解如何自定义它并使用我们自己的知识库和嵌入模型。
如果您想查看此步骤的最终结果,可以查看 step-06
目录。否则,让我们开始吧!
稍作清理
让我们先做一些清理工作。首先,打开 src/main/resources/application.properties
文件并删除以下配置:
quarkus.langchain4j.easy-rag.path=src/main/resources/rag
quarkus.langchain4j.easy-rag.max-segment-size=100
quarkus.langchain4j.easy-rag.max-overlap-size=25
quarkus.langchain4j.easy-rag.max-results=3
然后,打开 pom.xml
文件并删除以下依赖项
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-easy-rag</artifactId>
</dependency>
嵌入模型
RAG 模式的核心组件之一是嵌入模型。嵌入模型用于将文本转换为数值向量。这些向量用于比较文本并查找最相关的片段。
选择一个好的嵌入模型至关重要。在上一步中,我们使用了 OpenAI 提供的默认嵌入模型。但是,您也可以使用自己的嵌入模型。
在这一步中,我们将使用 bge-small-en-q 嵌入模型。
将以下依赖项添加到您的 pom.xml
文件中
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-embeddings-bge-small-en-q</artifactId>
</dependency>
此依赖项提供了 bge-small-en-q
嵌入模型。它将在您的机器本地运行。因此,您不必将文档发送到远程服务来计算嵌入。
此嵌入模型生成大小为 384 的向量。它是一个小型模型,但足以满足我们的用例。
要使用该模型,我们将通过将以下内容添加到 src/main/resources/application.properties
来使用 Quarkus 自动创建的 dev.langchain4j.model.embedding.onnx.bgesmallenq.BgeSmallEnQuantizedEmbeddingModel
CDI Bean
quarkus.langchain4j.embedding-model.provider=dev.langchain4j.model.embedding.onnx.bgesmallenq.BgeSmallEnQuantizedEmbeddingModel
向量存储
现在我们有了嵌入模型,我们需要存储嵌入。在上一步中,我们使用了内存存储。现在我们将使用持久化存储来在重新启动之间保留嵌入。
有许多存储嵌入的选项,例如 Redis、Infinispan、专用数据库(如 Chroma)等。在这里,我们将使用 PostgreSQL pgVector 存储,这是一个流行的关系数据库。如果您无法使用 Docker 或 Podman 运行开发服务,则可以使用 内存嵌入存储。
将以下依赖项添加到您的 pom.xml
文件中
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-pgvector</artifactId>
</dependency>
此嵌入存储(像许多其他一样)需要提前知道要存储的嵌入的大小。打开 src/main/resources/application.properties
文件并添加以下配置:
该值是 bge-small-en-q
嵌入模型生成的向量的大小。
现在我们将能够使用 io.quarkiverse.langchain4j.pgvector.PgVectorEmbeddingStore
bean 来存储和检索嵌入。
将文档摄入向量存储
当您编辑 src/main/resources/application.properties
文件时,请添加以下配置
这是我们将用于指定要摄入到向量存储中的文档位置的自定义配置属性。它取代了上一步中的 quarkus.langchain4j.easy-rag.path
属性。
现在让我们创建我们的摄入器。请记住,摄入器的作用是读取文档并将其嵌入存储到向量存储中。
创建 dev.langchain4j.quarkus.workshop.RagIngestion
类,其中包含以下内容
package dev.langchain4j.quarkus.workshop;
import static dev.langchain4j.data.document.splitter.DocumentSplitters.recursive;
import java.nio.file.Path;
import java.util.List;
import dev.langchain4j.model.embedding.onnx.HuggingFaceTokenCountEstimator;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.logging.Log;
import io.quarkus.runtime.StartupEvent;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
@ApplicationScoped
public class RagIngestion {
/**
* Ingests the documents from the given location into the embedding store.
*
* @param ev the startup event to trigger the ingestion when the application starts
* @param store the embedding store the embedding store (PostGreSQL in our case)
* @param embeddingModel the embedding model to use for the embedding (BGE-Small-EN-Quantized in our case)
* @param documents the location of the documents to ingest
*/
public void ingest(@Observes StartupEvent ev,
EmbeddingStore store, EmbeddingModel embeddingModel,
@ConfigProperty(name = "rag.location") Path documents) {
store.removeAll(); // cleanup the store to start fresh (just for demo purposes)
List<Document> list = FileSystemDocumentLoader.loadDocumentsRecursively(documents);
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel)
.documentSplitter(recursive(100, 25,
new HuggingFaceTokenCountEstimator()))
.build();
ingestor.ingest(list);
Log.info("Documents ingested successfully");
}
}
此类从 rag.location
位置将文档加载到向量存储中。它在应用程序启动时运行(归功于 @Observes StartupEvent ev
参数)。
此外,它接收
PgVectorEmbeddingStore
bean 以存储嵌入,BgeSmallEnQuantizedEmbeddingModel
bean 以生成嵌入,rag.location
配置属性以知道文档的位置。
FileSystemDocumentLoader.loadDocumentsRecursively(documents)
方法从给定位置加载文档。
EmbeddingStoreIngestor
类用于将文档摄入到向量存储中。这是摄入过程的基石。正确配置它对于 RAG 模式的准确性至关重要。在这里,我们使用递归文档分割器,片段大小为 100 个 token,重叠大小为 25 个 token(与上一步相同)。
重要提示
分割器、片段大小和重叠大小对于 RAG 模式的准确性至关重要。它取决于您拥有的文档和正在处理的用例。没有放之四海而皆准的解决方案。您可能需要尝试不同的配置来找到最适合您用例的配置。
最后,我们触发摄入过程并在完成后记录一条消息。
内存嵌入存储(适用于无法使用开发服务的用户)
如果您无法使用 Docker 或 Podman 运行开发服务,请随时使用 LangChain4j 提供的内存嵌入存储。
重要提示
这只是一个应急解决方案。如果您能够运行开发服务,请这样做。
如果您遵循了上一节,请删除 pgVector 更改。也就是说,您需要从 pom.xml
中删除 pgVector 依赖项,以便可以使用内存嵌入存储
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-pgvector</artifactId>
</dependency>
并在新类 InMemoryEmbeddingStoreProvider
中创建一个 EmbeddingStore
producer
package dev.langchain4j.quarkus.workshop;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class InMemoryEmbeddingStoreProvider {
@Produces
@ApplicationScoped
EmbeddingStore embeddingStore() {
return new InMemoryEmbeddingStore<>();
}
}
RagIngestion
现在将使用内存嵌入存储。
检索器和增强器
现在我们已经将文档摄入到向量存储中,我们需要实现检索器。检索器负责查找给定查询最相关的片段。增强器负责用检索到的片段扩展提示。
创建 dev.langchain4j.quarkus.workshop.RagRetriever
类,其中包含以下内容
package dev.langchain4j.quarkus.workshop;
import java.util.List;
import dev.langchain4j.data.message.ChatMessage;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.DefaultRetrievalAugmentor;
import dev.langchain4j.rag.RetrievalAugmentor;
import dev.langchain4j.rag.content.Content;
import dev.langchain4j.rag.content.injector.ContentInjector;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
public class RagRetriever {
@Produces
@ApplicationScoped
public RetrievalAugmentor create(EmbeddingStore store, EmbeddingModel model) {
var contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(model)
.embeddingStore(store)
.maxResults(3)
.build();
return DefaultRetrievalAugmentor.builder()
.contentRetriever(contentRetriever)
.build();
}
}
create
方法处理检索和提示增强。它使用 PgVectorEmbeddingStore
bean 来检索嵌入,并使用 BgeSmallEnQuantizedEmbeddingModel
bean 来生成嵌入。
EmbeddingStoreContentRetriever
类用于检索最相关的片段。我们将最多结果数配置为 3(与上一步相同)。请记住,更多的结果意味着更大的提示。这里不是问题,但某些 LLM 对提示(上下文)大小有限制。
内容检索器还可以配置为使用过滤器(应用于片段元数据)、要求最低分数等。
有了这个检索器,我们现在可以构建提示增强。我们创建一个带有内容检索器的 DefaultRetrievalAugmentor
。它将
- 检索给定查询最相关的片段(使用内容检索器),
- 用这些片段增强提示。
增强器还有其他选项,例如修改提示的方式、使用多个检索器的方式等。但目前我们先保持简单。
测试应用程序
让我们看看一切是否按预期工作。如果已停止应用程序,请使用以下命令重新启动它:
Podman 或 Docker
该应用程序需要 Podman 或 Docker 来自动启动 PostgreSQL 数据库。因此,请确保您已安装并运行其中一个。
应用程序启动时,它会将文档摄入到向量存储中。
您可以使用开发 UI 来验证摄入,就像我们在上一步中所做的那样。这次,让我们用聊天机器人来测试:打开您的浏览器并转到 https://:8080
。向聊天机器人提问,看看它是否检索了相关的片段并构建了一个连贯的答案:
高级 RAG
在这一步中,我们解构了 RAG 模式,以了解其内部工作原理。RAG 模式比我们目前所见的要强大得多。
您可以使用不同的嵌入模型、不同的向量存储、不同的检索器等。该过程也可以扩展,特别是检索和增强步骤。
您可以使用多个检索器、过滤器、要求最低分数等。在使用多个检索器时,您可以组合结果、使用最佳结果等。
举个例子,我们将自定义内容注入器,即片段如何注入到提示中。现在,您会得到类似这样的内容:
我们将将其更改为
编辑 RagRetriever
类中的 create
方法,改为
package dev.langchain4j.quarkus.workshop;
import java.util.List;
import dev.langchain4j.data.message.ChatMessage;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.DefaultRetrievalAugmentor;
import dev.langchain4j.rag.RetrievalAugmentor;
import dev.langchain4j.rag.content.Content;
import dev.langchain4j.rag.content.injector.ContentInjector;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
public class RagRetriever {
@Produces
@ApplicationScoped
public RetrievalAugmentor create(EmbeddingStore store, EmbeddingModel model) {
var contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(model)
.embeddingStore(store)
.maxResults(3)
.build();
return DefaultRetrievalAugmentor.builder()
.contentRetriever(contentRetriever)
.contentInjector(new ContentInjector() {
@Override
public UserMessage inject(List<Content> list, ChatMessage chatMessage) {
StringBuffer prompt = new StringBuffer(((UserMessage)chatMessage).singleText());
prompt.append("\nPlease, only use the following information:\n");
list.forEach(content -> prompt.append("- ").append(content.textSegment().text()).append("\n"));
return new UserMessage(prompt.toString());
}
})
.build();
}
}
现在,如果您向聊天机器人提问,您将得到一个不同的提示。您可以通过检查最新的日志来查看这一点
INFO [io.qua.lan.ope.OpenAiRestApi$OpenAiClientLogger] (vert.x-eventloop-thread-0) Request:
- method: POST
- url: https://api.openai.com/v1/chat/completions
- headers: [Accept: text/event-stream], [Authorization: Be...1f], [Content-Type: application/json], [User-Agent: langchain4j-openai], [content-length: 886]
- body: {
"model" : "gpt-4o",
"messages" : [ {
"role" : "system",
"content" : "You are a customer support agent of a car rental company 'Miles of Smiles'.\nYou are friendly, polite and concise.\nIf the question is unrelated to car rental, you should politely redirect the customer to the right department.\n"
}, {
"role" : "user",
"content" : "What can you tell me about your cancellation policy?\nPlease, only use the following information:\n- 4. Cancellation Policy\n- 4. Cancellation Policy 4.1 Reservations can be cancelled up to 11 days prior to the start of the\n- booking period.\n4.2 If the booking period is less than 4 days, cancellations are not permitted.\n"
} ],
"temperature" : 0.3,
"top_p" : 1.0,
"stream" : true,
"stream_options" : {
"include_usage" : true
},
"max_tokens" : 1000,
"presence_penalty" : 0.0,
"frequency_penalty" : 0.0
}
这个注入器只是一个简单的例子。它不会改变 RAG 模式的行为。但它向您展示了如何自定义 RAG 模式以满足您的需求。
结论
在这一步中,我们解构了 RAG 模式,以了解其内部工作原理。我们使用了自己的嵌入模型和向量存储。我们已经看到了该过程的各个方面以及如何自定义它们。
在下一步,让我们转向使用 LLM 的另一个非常流行的模式:函数调用和工具。