RAG(检索增强生成)
LLM 的知识仅限于其训练数据。 如果你想让 LLM 了解特定领域的知识或专有数据,你可以:
- 使用 RAG,我们将在本节中介绍
- 使用你的数据对 LLM 进行微调
- 结合 RAG 和微调
什么是 RAG?
简单来说,RAG 是在将提示发送给 LLM 之前,从你的数据中查找并注入相关信息片段的方法。 这样 LLM 将获得(希望是)相关的信息,并能够利用这些信息进行回复, 从而降低产生幻觉的概率。
相关信息片段可以通过各种 信息检索 方法找到。 最流行的有:
- 全文(关键词)搜索。该方法使用 TF-IDF 和 BM25 等技术, 通过将查询(例如,用户提出的问题)中的关键词与文档数据库进行匹配来搜索文档。 它根据每个文档中这些关键词的频率和相关性对结果进行排序。
- 向量搜索,也称为“语义搜索”。 使用嵌入模型将文本文档转换为数字向量。 然后根据查询向量与文档向量之间的余弦相似度 或其他相似度/距离度量来查找和排序文档, 从而捕捉更深层的语义含义。
- 混合搜索。结合多种搜索方法(例如,全文 + 向量)通常可以提高搜索的有效性。
目前,本页主要关注向量搜索。
全文和混合搜索目前仅由 Azure AI Search 集成和 Elasticsearch 支持,
有关更多详细信息,请参 阅 AzureAiSearchContentRetriever 和 ElasticsearchContentRetriever。
我们计划在不久的将来扩展 RAG 工具箱,以包含全文和混合搜索。
RAG 阶段
RAG 过程分为两个不同的阶段:索引和检索。 LangChain4j 为这两个阶段都提供了工具。
索引
在索引阶段,文档以能够在检索阶段实现高效搜索的方式进行预处理。
此过程可能因所使用的信息检索方法而异。 对于向量搜索,这通常涉及清理文档、用额外的数据和元数据丰富文档、 将文档拆分为更小的片段(也称为分块)、嵌入这些片段,最后将它们存储在嵌入存储(也称为向量数据库)中。
索引阶段通常离线进行,这意味着不需要最终用户等待其完成。 例如,可以通过一个 cron 作业来实现,该作业在周末每周重新索引一次内部公司文档。 负责索引的代码也可以是一个仅处理索引任务的独立应用程序。
然而,在某些场景中,最终用户可能希望上传他们的自定义文档,以使其可供 LLM 访问。 在这种情况下,索引应该在线执行,并成为主应用程序的一部分。
以下是索引阶段的简化示意图:

检索
检索阶段通常在线进行,当用户提交一个应 使用索引文档来回答的问题时。
此过程可能因所使用的信息检索方法而异。 对于向量搜索,这通常涉及嵌入用户的查询(问题) 并在嵌入存储中执行相似性搜索。 然后将相关的片段(原始文档的片段)注入提示并发送给 LLM。
以下是检索阶段的简化示意图:

LangChain4j 中的 RAG 风格
LangChain4j 提供三种 RAG 风格:
- Easy RAG:开始使用 RAG 的最简单方式
- Naive RAG:使用向量搜索的基本 RAG 实现
- Advanced RAG:一个模块化的 RAG 框架,允许进行额外的步骤,例如 查询转换、从多个来源检索以及重新排序
Easy RAG
LangChain4j 具有“简易 RAG”功能,使开始使用 RAG 变得尽可能简单。 你不必了解嵌入、选择向量存储、找到合适的嵌入模型、 弄清楚如何解析和拆分文档等。 只需指向你的文档,LangChain4j 就会施展它的魔法。
如果你需要可定制的 RAG,请跳到下一节。
如果你使用 Quarkus,有一种更简单的方法来实现简易 RAG。 请阅读 Quarkus 文档。
这种“简易 RAG”的质量当然会低于定制 RAG 设置的质量。 然而,这是开始学习 RAG 和/或制作概念验证的最简单方式。 之后,你将能够顺利地从简易 RAG 过渡到更高级的 RAG, 逐步调整和定制更多方面。
- 导入
langchain4j-easy-rag依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.18.1-beta28</version>
</dependency>
- 让我们加载你的文档:
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j/documentation");
这将从指定目录加载所有文件。
底层发生了什么?
Apache Tika 库支持多种文档类型,
用于检测文档类型并解析它们。
由于我们没有明确指定使用哪个 DocumentParser,
FileSystemDocumentLoader 将通过 SPI 加载由 langchain4j-easy-rag 依赖提供的 ApacheTikaDocumentParser。
如何自定义文档加载?
如果你想从所有子目录加载文档,可以使用 loadDocumentsRecursively 方法:
List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively("/home/langchain4j/documentation");
此外,你还可以使用通配符或正则表达式来筛选文档:
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.pdf");
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j/documentation", pathMatcher);
使用 loadDocumentsRecursively 方法时,你可能希望在 glob 中使用双星号(而不是单星号):glob:**.pdf。
- 现在,我们需要对文档进行预处理,并将其存储在专门的嵌入存储(也称为向量数据库)中。 这是为了在用户提问时能够快速找到相关的信息片段。 我们可以使用 30 多种受支持的嵌入存储中的任何一种, 但为了简单起见,我们将使用内存中的一种:
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
底层发生了什么?
-
EmbeddingStoreIngestor通过 SPI 从langchain4j-easy-rag依赖中加载一个DocumentSplitter。 每个Document被拆分成 更小的片段(TextSegment),每个片段最多包含 300 个 token, 并且有 30 个 token 的重叠。 -
EmbeddingStoreIngestor通过 SPI 从langchain4j-easy-rag依赖中加载一个EmbeddingModel。 每个TextSegment使用EmbeddingModel转换为一个Embedding。
我们选择了 bge-small-en-v1.5 作为 Easy RAG 的默认嵌入模型。 它在 MTEB 排行榜 上取得了令人瞩目的成绩, 其量化版本仅占用 24 兆字节的空间。 因此,我们可以轻松地将其加载到内存中,并使用 ONNX Runtime 在同一进程中运行。
是的,你没看错,你可以完全离线地将文本转换为嵌入向量,无需任何外部服务, 就在同一个 JVM 进程中。 LangChain4j 提供了一些流行的嵌入模型 开箱即用