langchain4j-11 (RAG (检索增强生成))
简单来说,RAG 是通过在发送到LLM之前,从你的数据中找到并注入相关信息的片段到提示中。这样LLM就能获得(希望是)相关信息,并能够使用这些信息进行回复,这应该会降低幻觉发生的概率。相关信息可以通过各种信息检索方法找到。全文(关键词)搜索。这种方法使用 TF-IDF 和 BM25 等技术,通过将查询中的关键词(例如用户提出的问题)与文档数据库进行匹配来搜索文档。它根据这些关键词在每个文档中的频率
RAG (Retrieval-Augmented Generation)
LLM的知识仅限于其训练数据。如果您想使LLM了解特定领域的知识或专有数据,您可以:
- Use RAG, which we will cover in this section
使用 RAG,我们将在本节中介绍 - Fine-tune the LLM with your data
使用您的数据微调LLM - Combine both RAG and fine-tuning
结合 RAG 和微调
https://gorilla.cs.berkeley.edu/blogs/9_raft.html
什么是 RAG?
简单来说,RAG 是通过在发送到LLM之前,从你的数据中找到并注入相关信息的片段到提示中。这样LLM就能获得(希望是)相关信息,并能够使用这些信息进行回复,这应该会降低幻觉发生的概率。
相关信息可以通过各种信息检索方法找到。最流行的方法有:
- 全文(关键词)搜索。这种方法使用 TF-IDF 和 BM25 等技术,通过将查询中的关键词(例如用户提出的问题)与文档数据库进行匹配来搜索文档。它根据这些关键词在每个文档中的频率和相关性对结果进行排序。
- 向量搜索,也称为“语义搜索”。使用嵌入模型将文本文档转换为数字向量。然后根据查询向量与文档向量之间的余弦相似度或其他相似度/距离度量来查找和排序文档,从而捕捉更深层次的语义含义。
- 混合。结合多种搜索方法(例如全文+向量)通常可以提高搜索的有效性。
目前,本页主要关注向量搜索。全文和混合搜索目前仅由 Azure AI Search 集成支持,有关更多详细信息,请参阅 AzureAiSearchContentRetriever 。我们计划在不久的将来将 RAG 工具箱扩展到包括全文和混合搜索。
RAG 阶段
RAG 流程分为两个不同的阶段:索引和检索。LangChain4j 为这两个阶段提供了工具。
Indexing 索引
在索引阶段,文档以支持检索阶段高效搜索的方式进行预处理。
这个过程可能因所使用的信息检索方法而异。对于向量搜索,这通常涉及清理文档、添加额外的数据和元数据、将文档分割成更小的片段(即分块)、嵌入这些片段,最后将它们存储在嵌入存储(即向量数据库)中。
索引阶段通常在离线进行,这意味着不需要最终用户等待其完成。例如,可以通过 cron 作业在周末每周重新索引公司内部文档来实现。负责索引的代码也可以是一个单独的应用程序,它只处理索引任务。
然而,在某些场景中,最终用户可能希望上传他们的自定义文档以使其对LLM可访问。在这种情况下,索引应在线进行,并成为主应用程序的一部分。
下面是索引阶段的简化图示:
Retrieval 检索
检索阶段通常在用户提交应使用索引文档回答的问题时在线进行。
这个过程可能因所使用的信息检索方法而异。对于向量搜索,这通常涉及将用户的查询(问题)嵌入并执行嵌入存储中的相似性搜索。然后,相关的片段(原始文档的片段)被注入到提示中并发送到LLM。
下面是检索阶段的简化图:
LangChain4j 中的 RAG(检索增强生成)
LangChain4j 提供了三种 RAG 版本:
- 简单 RAG:开始使用 RAG 的最简单方式
- 基于向量搜索的朴素 RAG:RAG 的基本实现
- 高级 RAG:一个模块化的 RAG 框架,允许进行查询转换、从多个来源检索和重新排序等附加步骤
简单 RAG
LangChain4j 拥有“简易 RAG”功能,使其尽可能容易地开始使用 RAG。您无需了解嵌入向量、选择向量存储、找到合适的嵌入模型、弄清楚如何解析和拆分文档等。只需指向您的文档,LangChain4j 就会施展魔法。
如果您需要可定制的 RAG,请跳到下一节。
如果你正在使用 Quarkus,那么进行 Easy RAG 的方式会更加简单。请阅读 Quarkus 文档。
https://docs.quarkiverse.io/quarkus-langchain4j/dev/easy-rag.html
注意:当然,这种“Easy RAG”的质量会比定制化的 RAG 设置低。然而,这是开始了解 RAG 和/或制作原型概念的最简单方式。以后,你将能够顺利地从 Easy RAG 过渡到更高级的 RAG,调整和定制越来越多的方面。
- 导入 langchain4j-easy-rag 依赖项:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 让我们加载您的文档:
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j/documentation");
这将加载指定目录下的所有文件。
- 内部发生了什么?
Apache Tika 库支持多种文档类型,用于检测文档类型并解析它们。由于我们没有明确指定使用哪个 DocumentParser , FileSystemDocumentLoader 将加载由 langchain4j-easy-rag 依赖项通过 SPI 提供的 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:**.pdf 。
- 现在,我们需要在专门的嵌入存储中预处理和存储文档,这种存储也称为向量数据库。这是在用户提问时快速找到相关信息所必需的。我们可以使用我们支持的 15+种嵌入存储中的任何一种,但为了简单起见,我们将使用内存中的存储:
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
- 发生了什么?
-
EmbeddingStoreIngestor 从 langchain4j-easy-rag 依赖项通过 SPI 加载 DocumentSplitter 。每个 Document 被分割成更小的片段
-
( TextSegment ),每个片段包含不超过 300 个标记,并且有 30 个标记的重叠。
注意:
我们选择了 bge-small-en-v1.5 作为 Easy RAG 的默认嵌入模型。它在 MTEB 排行榜上取得了令人印象深刻的分数,其量化版本仅占用 24 兆字节的存储空间。因此,我们可以轻松将其加载到内存中,并使用 ONNX Runtime 在同一进程中运行。
是的,没错,您可以在不使用任何外部服务的情况下,完全离线将文本转换为嵌入,在相同的 JVM 进程中。LangChain4j 提供了 5 个流行的嵌入模型。 -
所有 TextSegment - Embedding 对都存储在 EmbeddingStore 中。
-
- 最后一步是创建一个 AI 服务,该服务将作为我们的 API,服务于LLM:
interface Assistant {
String chat(String userMessage);
}
ChatLanguageModel chatModel = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(chatModel)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.build();
这里,我们配置 Assistant 使用 OpenAI LLM来回答用户问题,记住对话中的最后 10 条消息,并从包含我们的文档的 EmbeddingStore 中检索相关内容。
5. And now we are ready to chat with it!
现在我们准备好与它聊天了!
String answer = assistant.chat("How to do Easy RAG with LangChain4j?");
核心 RAG API
LangChain4j 提供了一套丰富的 API,使您能够轻松构建自定义 RAG 管道,从简单的到高级的。在本节中,我们将介绍主要领域类和 API。
Document 文档
一个 Document 类代表整个文档,例如单个 PDF 文件或网页。目前, Document 只能表示文本信息,但未来的更新将使其能够支持图像和表格。
有用方法:
- Document.text() 返回 Document 的文本
- Document.metadata() 返回 Metadata 的 Document (见“元数据”部分)
- Document.toTextSegment() 将 Document 转换为 TextSegment (见“文本段”部分)
- Document.from(String, Metadata) 从文本和 Metadata 创建 Document
- Document.from(String) 从文本创建 Document 并 Metadata 为空
Metadata 元数据
每个 Document 包含 Metadata 。它存储有关 Document 的元信息,例如其名称、来源、最后更新日期、所有者或任何其他相关细节。
Metadata 存储为一个键值映射,其中键为 String 类型,值可以是以下类型之一: String , Integer , Long , Float , Double 。
Metadata 有几个原因:
- 当在提示 LLM 时包含 Document 的内容,也可以包括元数据条目,为 LLM 提供额外的信息以供考虑。例如,提供 Document 的名称和来源可以帮助 LLM 更好地理解内容。
- 当搜索要包含在提示中的相关内容时,可以按 Metadata 条目进行筛选。例如,可以将语义搜索的范围缩小到仅属于特定所有者的 Document 。
- 当 Document 的来源更新(例如,文档的特定页面)时,可以通过其元数据条目(例如,“id”,“source”等)轻松找到相应的 Document ,并在 EmbeddingStore 中更新它以保持同步。
有用方法:
- 来自 langchain4j 模块的 FileSystemDocumentLoader
- 来自 langchain4j 模块的 ClassPathDocumentLoader
- 来自 langchain4j 模块的 UrlDocumentLoader
- 来自 langchain4j-document-loader-amazon-s3 模块的 AmazonS3DocumentLoader
- 来自 langchain4j-document-loader-azure-storage-blob 模块的 AzureBlobStorageDocumentLoader
- 来自 langchain4j-document-loader-github 模块的 GitHubDocumentLoader
- 来自 langchain4j-document-loader-google-cloud-storage 模块的
- 来自 langchain4j-document-loader-selenium 模块的 SeleniumDocumentLoader
- 来自 langchain4j-document-loader-tencent-cos 模块的 TencentCosDocumentLoader
Document Parser 文档解析器
Document 可以表示各种格式的文件,如 PDF、DOC、TXT 等。为了解析这些格式中的每一个,库中包含了一个 DocumentParser 接口及其多个实现:
- 来自 langchain4j 模块的 TextDocumentParser ,可以解析纯文本格式的文件(例如 TXT、HTML、MD 等)
- 来自 langchain4j-document-parser-apache-pdfbox 模块的 ApachePdfBoxDocumentParser ,可以解析 PDF 文件
- 来自 langchain4j-document-parser-apache-poi 模块的 ApachePoiDocumentParser ,可以解析 MS Office 文件格式(例如 DOC、DOCX、PPT、PPTX、XLS、XLSX 等)
- 来自 langchain4j-document-parser-apache-tika 模块的 ApacheTikaDocumentParser ,可以自动检测和解析几乎所有现有的文件格式
下面是一个从文件系统加载一个或多个 Document 的示例:
// Load a single document
Document document = FileSystemDocumentLoader.loadDocument("/home/langchain4j/file.txt", new TextDocumentParser());
// Load all documents from a directory
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j", new TextDocumentParser());
// Load all *.txt documents from a directory
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.txt");
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j", pathMatcher, new TextDocumentParser());
// Load all documents from a directory and its subdirectories
List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively("/home/langchain4j", new TextDocumentParser());
您也可以不显式指定 DocumentParser 来加载文档。在这种情况下,将使用默认的 DocumentParser 。默认情况下,通过 SPI 加载(例如,从 langchain4j-document-parser-apache-tika 或 langchain4j-easy-rag 中,如果其中一个被导入)。如果没有找到任何 DocumentParser ,则使用 TextDocumentParser 作为后备。
Document Transformer 文档转换器
DocumentTransformer 的实现可以执行各种文档转换,例如:
- 清洗:这涉及从 Document 的文本中移除不必要的噪音,这样可以节省令牌并减少干扰。
- 过滤:完全排除特定的 Document 从搜索中。
- 丰富:可以向 Document 添加更多信息,以潜在地增强搜索结果。
- 摘要: Document 可以进行总结,其简短摘要可以存储在 Metadata 中,以便稍后包含在每个 TextSegment (我们将在下面介绍)中,以潜在地改进搜索。
- 等等
Metadata 条目也可以在此阶段添加、修改或删除。
目前提供的唯一开箱即用的实现是在 langchain4j-document-transformer-jsoup 模块中的 HtmlToTextDocumentTransformer ,它可以从原始 HTML 中提取所需文本内容和元数据条目。
由于没有一种适合所有情况的解决方案,我们建议实现你自己的 DocumentTransformer ,以适应你独特的数据。
Text Segment 文本片段
当你的 Document 加载完成后,就是时候将它们(块)分割成更小的片段(部分)。LangChain4j 的领域模型包括一个 TextSegment 类,它代表一个 Document 的片段。正如其名所示, TextSegment 只能表示文本信息。
- 分割还是不分?
有多个原因可能让你只想在提示中包含几个相关段落,而不是整个知识库:- LLMs 的上下文窗口有限,因此整个知识库可能无法全部适应
- 你在提示中提供的信息越多,LLM 处理信息并响应所需的时间就越长
- 你在提示中提供的信息越多,你付出的代价就越大
- 提示中的无关信息可能会分散 LLM 的注意力,并增加幻觉发生的可能性
- 提供的提示信息越多,根据哪些信息进行响应就越难以解释
我们可以通过将知识库分割成更小、更易于消化的部分来解决这个问题。这些部分应该有多大?这是一个很好的问题。和往常一样,这取决于。
目前有 2 种广泛使用的方法:
-
每个文档(例如,PDF 文件、网页等)都是原子性和不可分割的。在 RAG 管道的检索过程中,检索出 N 个最相关的文档并注入到提示中。在这种情况下,你很可能需要使用长上下文LLM,因为文档可能相当长。如果检索完整的文档很重要,例如当你不能承受错过一些细节时,这种方法是合适的。
优点:不会丢失上下文。
缺点:- 消耗的 token 更多。
- 有时,文档可能包含多个部分/主题,并非所有部分都与查询相关。
- 向量搜索质量受损,因为各种大小的完整文档被压缩成一个单一、固定长度的向量。
-
文档被分割成更小的片段,如章节、段落,有时甚至是句子。在 RAG 管道的检索过程中,检索出 N 个最相关的片段并注入到提示中。挑战在于确保每个片段都提供了足够的信息和上下文,以便LLM能够理解它。缺少上下文可能导致LLM误解所提供的片段并产生幻觉。一种常见的策略是将文档分割成有重叠的片段,但这并不能完全解决问题。一些高级技术可以帮助解决这个问题,例如“句子窗口检索”、“自动合并检索”和“父文档检索”。这里不会详细介绍,但本质上,这些方法有助于获取更多关于检索片段的上下文,在检索片段之前和之后为LLM提供额外的信息。
优点:- 消耗的 token 更多。
- 有时,文档可能包含多个部分/主题,并非所有部分都与查询相关
缺点: 部分上下文可能仍然会丢失。
有用方法:
- TextSegment.text() 返回 TextSegment 的文本
- TextSegment.metadata() 返回 TextSegment 的 Metadata
- TextSegment.from(String, Metadata) 从文本和 Metadata 创建 TextSegment
- TextSegment.from(String) 从文本创建 TextSegment 并 Metadata 为空
Document Splitter 文档分割器
LangChain4j 具有与多个现成实现相关的 DocumentSplitter 接口:
- DocumentByParagraphSplitter
- DocumentByLineSplitter
- DocumentBySentenceSplitter
- DocumentByWordSplitter
- DocumentByCharacterSplitter
- DocumentByRegexSplitter
- Recursive: DocumentSplitters.recursive(…) 递归: DocumentSplitters.recursive(…)
它们都按以下方式工作:
- 您实例化一个 DocumentSplitter ,指定 TextSegment 的期望大小,并可选择性地指定字符或标记的重叠。
- 您调用 DocumentSplitter 的 split(Document) 或 splitAll(List) 方法。
- DocumentSplitter 将给定的 Document 划分为更小的单元,其性质随分割器的不同而变化。例如, DocumentByParagraphSplitter 将文档划分为段落(由两个或多个连续换行符定义),而 DocumentBySentenceSplitter 使用 OpenNLP 库的句子检测器将文档划分为句子,等等。
- 然后 DocumentSplitter 将这些较小的单元(段落、句子、单词等)组合成 TextSegment ,尝试在不超过步骤 1 中设置的限制的情况下包含尽可能多的单元在一个 TextSegment 中。如果某些单元仍然太大而无法放入一个 TextSegment 中,它将调用子分割器。这是另一个 DocumentSplitter ,能够分割不适合更细粒度单元的单元。所有 Metadata 条目都从 Document 复制到每个 TextSegment 。为每个文本段添加一个唯一的元数据条目 “index”。第一个 TextSegment 将包含 index=0 ,第二个 index=1 ,依此类推。
Text Segment Transformer 文本段转换器
TextSegmentTransformer 与 DocumentTransformer (如上所述)类似,但它转换 TextSegment 。
与 DocumentTransformer 一样,没有一种适合所有情况的解决方案,所以我们建议您实施自己的 TextSegmentTransformer ,以适应您独特的数据。
提高检索效果的一种有效技术是在每个 Document 标题或简短摘要中包含 TextSegment 。
Embedding 嵌入
Embedding 类封装了一个表示嵌入内容“语义意义”的数值向量(通常为文本,如 TextSegment )。
在这里了解更多关于向量嵌入的内容:
- https://www.elastic.co/what-is/vector-embedding
- https://www.pinecone.io/learn/vector-embeddings/
- https://cloud.google.com/blog/topics/developers-practitioners/meet-ais-multitool-vector-embeddings
有用方法:
- Embedding.dimension() 返回嵌入向量的维度(其长度)
- CosineSimilarity.between(Embedding, Embedding) 计算两个 Embedding 的余弦相似度
- Embedding.normalize() 标准化嵌入向量(就地操作)
Embedding Model 嵌入模型
EmbeddingModel 接口代表一种特殊的模型,它将文本转换为 Embedding。
当前支持的嵌入模型可以在此处找到https://docs.langchain4j.dev/category/embedding-models/。
有用方法:
- EmbeddingModel.embed(String) 将给定的文本嵌入
- EmbeddingModel.embed(TextSegment) 包含给定的 TextSegment
- EmbeddingModel.embedAll(List) 包含所有给定的 TextSegment
- EmbeddingModel.dimension() 返回此模型产生的 Embedding 的维度
Embedding Store 嵌入存储
EmbeddingStore 接口代表一个存储 Embedding 的仓库,也称为向量数据库。它允许存储和高效搜索在嵌入空间中相似的(接近) Embedding 。
当前支持的嵌入存储可以在以下位置找到 https://docs.langchain4j.dev/integrations/embedding-stores/
EmbeddingStore 可以单独存储 Embedding 或与相应的 TextSegment 一起存储:
- 它只能按 ID 存储 Embedding ,原始嵌入数据可以存储在其他地方,并通过 ID 关联。
- 它可以存储 Embedding 和嵌入的原数据(通常是 TextSegment )。
有用方法:
- EmbeddingStore.add(Embedding) 向存储中添加给定的 Embedding 并返回一个随机 ID
- 将指定 ID 的给定 Embedding 添加到存储中
- 将给定 Embedding 及其关联的 TextSegment 添加到存储中,并返回一个随机 ID
- 将给定 Embedding 的列表添加到存储中,并返回一个随机 ID 列表
- 将给定 Embedding 的列表及其关联的 TextSegment 添加到存储中,并返回一个随机 ID 列表
- 将给定的 Embedding 列表及其关联的 ID 和 TextSegment 添加到存储中
- 搜索最相似的 Embedding s
- 通过 ID 从存储中删除单个 Embedding
- 通过 ID 从存储中删除多个 Embedding s
- EmbeddingStore.removeAll(Filter) 从存储中删除所有匹配指定的 Embedding 的 Filter
- EmbeddingStore.removeAll() 从存储中删除所有 Embedding
EmbeddingSearchRequest 嵌入搜索请求
EmbeddingSearchRequest 表示在 EmbeddingStore 中搜索的请求。它具有以下属性:
- Embedding queryEmbedding :用作参考的嵌入。
- int maxResults :返回结果的最大数量。这是一个可选参数。默认值:3。
- double minScore :最小分数,范围为 0 到 1(含)。仅返回分数 >= minScore 的嵌入。此参数为可选参数。默认值:0。
- Filter filter :搜索时应用于 Metadata 的过滤器。仅返回 Metadata 与 Filter 匹配的 TextSegment 。
Filter 筛选
执行矢量搜索时, Filter 允许按 Metadata 条目进行过滤。
目前,支持以下 Filter 类型/操作:
- IsEqualTo
- IsNotEqualTo
- IsGreaterThan
- IsGreaterThanOrEqualTo
- IsLessThan
- IsLessThanOrEqualTo
- IsIn
- IsNotIn
- ContainsString
- And
- Not
- Or
注意:并非所有嵌入存储都支持按 Metadata 过滤,请参阅此处的 “按元数据过滤”列。
一些支持按 Metadata 过滤的存储并不支持所有可能的 Filter 类型/操作。例如, ContainsString 目前仅 Milvus、PgVector 和 Qdrant 支持。
关于 Filter 的更多详细信息请参见此处 https://github.com/langchain4j/langchain4j/pull/610 。
EmbeddingSearchResult 嵌入搜索结果
EmbeddingSearchResult 表示在 EmbeddingStore 中搜索的结果。它包含 EmbeddingMatch 的列表。
Embedding Match 嵌入匹配
EmbeddingMatch 表示匹配的 Embedding 及其相关性分数、ID 和原始嵌入数据(通常是 TextSegment )。
Embedding Store Ingestor 嵌入商店摄取器
EmbeddingStoreIngestor 代表一个摄取管道,负责将 Document 摄取到 EmbeddingStore 中。
在最简单的配置中, EmbeddingStoreIngestor 使用指定的 EmbeddingModel 嵌入提供的 Document ,并将它们与它们的 Embedding 一起存储在指定的 EmbeddingStore 中:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
ingestor.ingest(document1);
ingestor.ingest(document2, document3);
IngestionResult ingestionResult = ingestor.ingest(List.of(document4, document5, document6));
EmbeddingStoreIngestor 中的所有 ingest() 方法都会返回一个 IngestionResult 。 IngestionResult 包含一些有用的信息,例如 TokenUsage ,它显示了用于嵌入的令牌数量。
可选地, EmbeddingStoreIngestor 可以使用指定的 DocumentTransformer 来转换 Document 。如果你想在嵌入 Document 之前对其进行清理、丰富或格式化,这将非常有用。
或者, EmbeddingStoreIngestor 可以使用指定的 DocumentSplitter 将 Document 拆分为 TextSegment 。如果 Document 很大,并且您想将其拆分为较小的 TextSegment ,以提高相似性搜索的质量并减少发送给 LLM 的提示的大小和成本,那么此功能非常有用。
可选地, EmbeddingStoreIngestor 可以使用指定的 TextSegmentTransformer 来转换 TextSegment 。如果您想在嵌入 TextSegment 之前对其进行清理、丰富或格式化,这将非常有用。
一个例子:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// adding userId metadata entry to each Document to be able to filter by it later
.documentTransformer(document -> {
document.metadata().put("userId", "12345");
return document;
})
// splitting each Document into TextSegments of 1000 tokens each, with a 200-token overlap
.documentSplitter(DocumentSplitters.recursive(1000, 200, new OpenAiTokenizer()))
// adding a name of the Document to each TextSegment to improve the quality of search
.textSegmentTransformer(textSegment -> TextSegment.from(
textSegment.metadata().getString("file_name") + "\n" + textSegment.text(),
textSegment.metadata()
))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
Naive RAG 天真的 RAG
一旦我们的文档被提取(参见前面的部分),我们就可以创建一个 EmbeddingStoreContentRetriever 来启用简单的 RAG 功能。
使用 AI 服务时,可按如下方式配置初始 RAG:
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.75)
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(model)
.contentRetriever(contentRetriever)
.build();
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_2_naive/Naive_RAG_Example.java
Advanced RAG 高级 RAG
高级 RAG 可以通过 LangChain4j 实现,其核心组件如下:
- QueryTransformer
- QueryRouter
- ContentRetriever
- ContentAggregator
- ContentInjector
下图显示了这些组件如何协同工作:
流程如下:
- 用户生成 UserMessage ,然后将其转换为 Query
- QueryTransformer 将 Query 转换为一个或多个 Query
- 每个 Query 由 QueryRouter 路由到一个或多个 ContentRetriever
- 每个 ContentRetriever 检索每个 Query 的相关 Content
- ContentAggregator 将所有检索到的 Content 组合成一个最终排序列表
- 此 Content 列表被注入到原始 UserMessage 中
- 最后,包含原始查询以及注入的相关内容的 UserMessage 被发送到 LLM
请参阅每个组件的 Javadoc 以了解更多详细信息。
Retrieval Augmentor 检索增强器
RetrievalAugmentor 是 RAG 管道的入口点。它负责使用从各种来源检索到的相关 Content 来扩充 ChatMessage 。
可以在创建 AI 服务期间指定 RetrievalAugmentor 的实例:
Assistant assistant = AiServices.builder(Assistant.class)
...
.retrievalAugmentor(retrievalAugmentor)
.build();
每次调用 AI 服务时,指定的 RetrievalAugmentor 将被调用来增强当前的 UserMessage 。
您可以使用 RetrievalAugmentor 的默认实现 (如下所述)或实现一个自定义的。
Default Retrieval Augmentor 默认检索增强器
LangChain4j 提供了 RetrievalAugmentor 接口的开箱即用实现: DefaultRetrievalAugmentor 应该适用于大多数 RAG 用例。它的灵感来自这篇文章 以及本文 。建议查阅这些资源,以便更好地理解这一概念。
https://blog.langchain.dev/deconstructing-rag/
https://arxiv.org/abs/2312.10997
Query 询问
Query 表示 RAG 管道中的用户查询。它包含查询文本和查询元数据。
Query Metadata 查询元数据
Query 中的 Metadata 包含可能对 RAG 管道的各个组件有用的信息,例如:
- Metadata.userMessage() - 需要扩充的原始 UserMessage
- Metadata.chatMemoryId() - 带有 @MemoryId 注解的方法参数的值。更多详情请见此处 。这可用于识别用户,并在检索过程中应用访问限制或过滤器。
- Metadata.chatMemory() - 所有之前的 ChatMessage 。这有助于理解 Query 的上下文。
Query Transformer 查询转换器
QueryTransformer 将给定的 Query 转换为一个或多个 Query 。目标是通过修改或扩展原始 Query 来提高检索质量。
一些已知的改进检索的方法包括:
- Query compression 查询压缩
- Query expansion 查询扩展
- Query re-writing 查询重写
- Step-back prompting 后退提示
- Hypothetical document embeddings (HyDE) 假设文档嵌入(HyDE)
更多详细信息请参见此处 。https://blog.langchain.dev/query-transformations/
Default Query Transformer 默认查询转换器
DefaultQueryTransformer 是 DefaultRetrievalAugmentor 中使用的默认实现。它不会对 Query 进行任何修改,只是将其传递过去。
Compressing Query Transformer 压缩查询转换器
CompressingQueryTransformer 使用 LLM 来压缩给定的 Query 并将之前的对话合并为一个独立的 Query 。当用户可能会询问涉及之前问题或答案的后续问题时,此功能非常有用。
User: Tell me about John Doe
AI: John Doe was a ...
User: Where did he live?
查询 Where did he live? 本身无法检索到所需的信息,因为没有明确提及 John Doe,因此不清楚 he 指的是谁。
使用 CompressingQueryTransformer 时,LLM 将读取整个对话并将 Where did he live? 转换为 Where did John Doe live?
Expanding Query Transformer 扩展查询转换器
ExpandingQueryTransformer 使用 LLM 将给定的 Query 扩展为多个 Query 。这很有用,因为 LLM 可以通过各种方式重新表述和重新表述 Query ,从而有助于检索更相关的内容。
Content 内容
Content 表示与用户 Query 相关的内容。目前仅限于文本内容(即 TextSegment ),未来可能会支持其他模态(例如,图像、音频、视频等)。
Content Retriever 内容检索器
ContentRetriever 使用给定的 Query 从底层数据源检索 Content 。底层数据源几乎可以是任何内容:
- Embedding store 嵌入存储
- Full-text search engine 全文搜索引擎
- Hybrid of vector and full-text search
- 向量与全文搜索的混合
- Web Search Engine 网络搜索引擎
- Knowledge graph 知识图谱
- SQL database SQL 数据库
- etc. ETC。
ContentRetriever 返回的 Content 列表按相关性从高到低排序。
Embedding Store Content Retriever 嵌入商店内容检索器
EmbeddingStoreContentRetriever 使用 EmbeddingModel 从 EmbeddingStore 检索相关 Content 以嵌入 Query 。
以下是一个例子:
EmbeddingStore embeddingStore = ...
EmbeddingModel embeddingModel = ...
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
// maxResults can also be specified dynamically depending on the query
.dynamicMaxResults(query -> 3)
.minScore(0.75)
// minScore can also be specified dynamically depending on the query
.dynamicMinScore(query -> 0.75)
.filter(metadataKey("userId").isEqualTo("12345"))
// filter can also be specified dynamically depending on the query
.dynamicFilter(query -> {
String userId = getUserId(query.metadata().chatMemoryId());
return metadataKey("userId").isEqualTo(userId);
})
.build();
Web Search Content Retriever 网页搜索内容检索器
WebSearchContentRetriever 使用 WebSearchEngine 从网络上检索相关 Content 。
所有受支持的 WebSearchEngine 集成都可以在这里找到 。https://docs.langchain4j.dev/category/web-search-engines/
以下是一个例子:
WebSearchEngine googleSearchEngine = GoogleCustomWebSearchEngine.builder()
.apiKey(System.getenv("GOOGLE_API_KEY"))
.csi(System.getenv("GOOGLE_SEARCH_ENGINE_ID"))
.build();
ContentRetriever contentRetriever = WebSearchContentRetriever.builder()
.webSearchEngine(googleSearchEngine)
.maxResults(3)
.build();
完整的示例可以在这里找到。
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_08_Advanced_RAG_Web_Search_Example.java
SQL Database Content Retriever SQL 数据库内容检索器
SqlDatabaseContentRetriever 是 ContentRetriever 的一个实验性实现 可以在 langchain4j-experimental-sql 模块中找到。
它使用 DataSource 和 LLM 来生成并执行给定自然语言 Query 的 SQL 查询。
有关更多信息,请参阅 SqlDatabaseContentRetriever 的 javadoc。
这是一个例子 。
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_10_Advanced_RAG_SQL_Database_Retreiver_Example.java
Azure AI 搜索内容检索器
AzureAiSearchContentRetriever 是与 Azure AI Search 。它支持全文搜索、向量搜索、混合搜索以及重排序功能。它位于 langchain4j-azure-ai-search 模块中。更多信息,请参阅 AzureAiSearchContentRetriever Javadoc。
https://azure.microsoft.com/en-us/products/ai-services/ai-search
Neo4j Content Retriever Neo4j 内容检索器
Neo4jContentRetriever 与 Neo4j 图形数据库集成。它将自然语言查询转换为 Neo4j Cypher 查询,并通过在 Neo4j 中运行这些查询来检索相关信息。它位于 langchain4j-community-neo4j-retriever 模块中。
Query Router 查询路由器
QueryRouter 负责将 Query 路由到适当的 ContentRetriever 。
Default Query Router 默认查询路由器
DefaultQueryRouter 是 DefaultRetrievalAugmentor 中使用的默认实现。它将每个 Query 路由到所有已配置的 ContentRetriever 。
Language Model Query Router 语言模型查询路由器
LanguageModelQueryRouter 使用 LLM 来决定将给定的 Query 路由到何处。
Content Aggregator 内容聚合器
ContentAggregator 负责聚合来自以下来源的多个 Content 排序列表:
- 多个 Query
- 多个 ContentRetriever
- both 两个都
Default Content Aggregator 默认内容聚合器
DefaultContentAggregator 是 ContentAggregator 的默认实现,它执行两阶段的倒数排序融合 (RRF)。更多详情,请参阅 DefaultContentAggregator Javadoc。
Re-Ranking Content Aggregator 重新排名内容聚合器
ReRankingContentAggregator 使用 ScoringModel (如 Cohere)来执行重新排名。 可以找到支持的评分(重新排名)模型的完整列表 此处 。请参阅 ReRankingContentAggregator Javadoc 了解更多详情。
https://docs.langchain4j.dev/category/scoring-reranking-models/
Content Injector 内容注入器
ContentInjector 负责将 ContentAggregator 返回的 Content 注入到 UserMessage 中。
Default Content Injector 默认内容注入器
DefaultContentInjector 是 ContentInjector 的默认实现,它只是将 Content 附加到带有前缀 Answer using the following information: 的 UserMessage 末尾。
您可以通过 3 种方式自定义将 Content 注入 UserMessage 方式:
- 覆盖默认的 PromptTemplate :
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("{{userMessage}}\n{{contents}}"))
.build())
.build();
请注意, PromptTemplate 必须包含 {{userMessage}} 和 {{contents}} 变量。
- 扩展 DefaultContentInjector 并重写其中一种 format 方法
- 实现自定义 ContentInjector
DefaultContentInjector 还支持从检索到的 Content.textSegment() 注入 Metadata 条目:
DefaultContentInjector.builder()
.metadataKeysToInclude(List.of("source"))
.build()
在这种情况下, TextSegment.text() 会添加 "content: " 前缀,并且 Metadata 中的每个值都会添加一个键。最终的 UserMessage 如下所示:
How can I cancel my reservation?
Answer using the following information:
content: To cancel a reservation, go to ...
source: ./cancellation_procedure.html
content: Cancellation is allowed for ...
source: ./cancellation_policy.html
Parallelization 并行化
当只有一个 Query 和一个 ContentRetriever 时, DefaultRetrievalAugmentor 在同一线程中执行查询路由和内容检索。否则,将使用 Executor 来并行处理。默认情况下,修改后的 keepAliveTime 为 1 秒,而不是 60 秒。 Executors.newCachedThreadPool() 被使用,但是您可以在创建 DefaultRetrievalAugmentor 时提供自定义 Executor 实例:
DefaultRetrievalAugmentor.builder()
...
.executor(executor)
.build;
Accessing Sources 访问源
如果您希望在使用 AI 服务时访问源(用于扩充消息的检索 Content ),则可以通过将返回类型包装在 Result 类中来轻松实现此目的:
interface Assistant {
Result<String> chat(String userMessage);
}
Result<String> result = assistant.chat("How to do Easy RAG with LangChain4j?");
String answer = result.content();
List<Content> sources = result.sources();
流式传输时,可以使用 onRetrieved() 方法指定 Consumer<List> :
interface Assistant {
TokenStream chat(String userMessage);
}
assistant.chat("How to do Easy RAG with LangChain4j?")
.onRetrieved((List<Content> sources) -> ...)
.onPartialResponse(...)
.onCompleteResponse(...)
.onError(...)
.start();
Examples 示例
- Easy RAG 简易RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_1_easy/Easy_RAG_Example.java - Naive RAG 天真的 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_2_naive/Naive_RAG_Example.java - Advanced RAG with Query Compression 具有查询压缩的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_01_Advanced_RAG_with_Query_Compression_Example.java - Advanced RAG with Query Routing 具有查询路由的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_02_Advanced_RAG_with_Query_Routing_Example.java - Advanced RAG with Re-Ranking 具有重新排序功能的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_03_Advanced_RAG_with_ReRanking_Example.java - Advanced RAG with Including Metadata 包含元数据的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_04_Advanced_RAG_with_Metadata_Example.java - Advanced RAG with Metadata Filtering 具有元数据过滤功能的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_05_Advanced_RAG_with_Metadata_Filtering_Examples.java - Advanced RAG with multiple Retrievers 配备多只猎犬的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_07_Advanced_RAG_Multiple_Retrievers_Example.java - Advanced RAG with Web Search 具有 Web 搜索功能的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_08_Advanced_RAG_Web_Search_Example.java - Advanced RAG with SQL Database 带有 SQL 数据库的高级 RAG
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_10_Advanced_RAG_SQL_Database_Retreiver_Example.java - Skipping Retrieval 跳过检索
https://github.com/langchain4j/langchain4j-examples/blob/main/rag-examples/src/main/java/_3_advanced/_06_Advanced_RAG_Skip_Retrieval_Example.java - RAG + Tools 抹布+工具
https://github.com/langchain4j/langchain4j-examples/blob/main/customer-support-agent-example/src/test/java/dev/langchain4j/example/CustomerSupportAgentIT.java - Loading Documents 加载文档
https://github.com/langchain4j/langchain4j-examples/blob/main/other-examples/src/main/java/DocumentLoaderExamples.java
更多推荐

所有评论(0)