LangChain4j 提供了一套丰富的 API,用于构建从简单到高级的检索增强生成(RAG)系统。这些 API 覆盖了从文档加载、预处理、嵌入到检索的全过程,使得开发者可以灵活地构建智能问答系统。以下是 Core RAG APIs 的核心组件及其功能的总结
这篇文章内容比较多,请分章节阅读,对做RAG非常有用
Core RAG APIs
LangChain4j 提供了一系列丰富的 API,帮助你构建从简单到高级的自定义 RAG 管道。以下是主要的领域类和 API。
1 Document(文档)
Document 类表示一个完整的文档,如单个 PDF 文件或网页。目前,Document 仅支持文本信息,但未来将支持图像和表格。
Metadata.from(Map):从一个 Map 创建 Metadata。
Metadata.put(String key, String value) / put(String, int) / 等等:向 Metadata 中添加一个条目。
Metadata.getString(String key) / getInteger(String key) / 等等:返回 Metadata 条目的值,并将其转换为所需的类型。
Metadata.containsKey(String key):检查 Metadata 是否包含指定键的条目。
Metadata.remove(String key):通过键从 Metadata 中移除一个条目。
Metadata.copy():返回 Metadata 的一个副本。
Metadata.toMap():将 Metadata 转换为一个 Map。
2 Metadata(元数据)
Metadata的概念
每个 Document 都包含 Metadata。它存储了关于文档的元信息,例如文档的名称、来源、最后更新日期、所有者,或其他任何相关的细节。
Metadata 以键值对的形式存储,其中键是 String
类型,值可以是以下类型之一:String、Integer、Long、Float、Double。
Metadata 是一种键值对存储结构,用于描述文档的元信息。这些元信息可以包括但不限于:
- 文档的名称(name)
- 文档的来源(source)
- 文档的最后更新日期(lastUpdateDate)
- 文档的所有者(owner)
- 其他任何相关的细节
Metadata 的用途有多个方面:
-
增强 LLM 的理解:
当将文档内容传递给语言模型时,Metadata 中的信息(如文档名称和来源)可以被包含在提示中,帮助语言模型更好地理解文档的上下文。
例如,如果文档的来源是一个特定的网站或作者,这些信息可以帮助语言模型生成更准确的回答。 -
过滤和搜索:
在检索相关文档时,可以通过 Metadata 条目进行过滤。例如,你可以通过 owner 字段将搜索范围限制为特定用户的所有文档。
这种过滤机制可以提高检索的效率和准确性。 -
同步更新:
当文档的来源(如网页或文件)被更新时,可以通过 Metadata 中的标识符(如 id 或 source)快速定位到对应的文档,并在嵌入存储(EmbeddingStore)中进行更新,以保持数据的一致性。
有用的元数据方法:
- Metadata.from(Map):从一个 Map 创建 Metadata。
- Metadata.put(String key, String value) / put(String, int) / 等等:向 Metadata 中添加一个条目。
- Metadata.getString(String key) / getInteger(String key) / 等等:返回 Metadata 条目的值,并将其转换为所需的类型。
- Metadata.containsKey(String key):检查 Metadata 是否包含指定键的条目。
- Metadata.remove(String key):通过键从 Metadata 中移除一个条目。
- Metadata.copy():返回 Metadata 的一个副本。
- Metadata.toMap():将 Metadata 转换为一个 Map。
Metadata 是 LangChain4j 中的一个重要组件,用于存储和管理文档的元信息。它通过提供丰富的操作方法,使得开发者可以灵活地处理文档的上下文信息,从而提高语言模型的理解能力和检索效率。通过合理使用 Metadata,开发者可以构建更加智能和高效的问答系统。
3 Document Loader(文档加载器)
LangChain4j 提供的多种文档加载器(DocumentLoader),这些加载器用于从不同的数据源加载文档。文档加载器是构建智能问答系统时非常重要的组件,因为它们负责将文档数据导入到系统中,以便后续进行处理和检索。以下是每个加载器的详细解析:
1. FileSystemDocumentLoader
从本地文件系统加载文档。它支持从指定路径加载单个文件或整个目录中的文件。
- 用途:适用于本地存储的文档,例如 PDF、TXT、HTML 等。
- 示例:
// 加载单个文件
Document document = FileSystemDocumentLoader.loadDocument("/path/to/file.txt");
// 加载整个目录中的所有文件
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/path/to/directory");
2. ClassPathDocumentLoader
从类路径(classpath)加载文档。它允许你加载存储在项目资源文件夹中的文档。
- 用途:适用于开发和测试阶段,尤其是当文档存储在项目的资源文件夹中时。
- 示例:
// 加载类路径中的文件
Document document = ClassPathDocumentLoader.loadDocument("resources/file.txt");
3. UrlDocumentLoader
从 URL 加载文档。它支持从网络地址加载文档,例如网页或在线文件。
- 用途:适用于从互联网加载文档,例如从博客、新闻网站或在线文档库加载内容。
- 示例:
// 加载网页内容
Document document = UrlDocumentLoader.loadDocument("https://example.com/page.html");
4. AmazonS3DocumentLoader
从 Amazon S3 存储桶加载文档。它支持从 AWS S3 存储桶加载文件。
- 用途:适用于使用 AWS S3 存储文档的场景。
- 示例:
// 加载 S3 存储桶中的文件
Document document = AmazonS3DocumentLoader.loadDocument("s3://bucket-name/path/to/file.txt");
5. AzureBlobStorageDocumentLoader
从 Azure Blob Storage 加载文档。它支持从 Azure Blob 存储加载文件。
- 用途:适用于使用 Azure Blob Storage 存储文档的场景。-
- 示例:
// 加载 Azure Blob 存储中的文件
Document document = AzureBlobStorageDocumentLoader
.loadDocument("azure://container-name/path/to/file.txt");
6. GitHubDocumentLoader
从 GitHub 仓库加载文档。它支持从 GitHub 仓库加载文件或整个目录。
- 用途:适用于从 GitHub 仓库加载文档,例如开源项目文档或代码注释。
- 示例:
// 加载 GitHub 仓库中的文件
Document document = GitHubDocumentLoader
.loadDocument("https://github.com/user/repo/path/to/file.txt");
7. GoogleCloudStorageDocumentLoader
从 Google Cloud Storage 加载文档。它支持从 Google Cloud Storage 存储桶加载文件。
- 用途:适用于使用 Google Cloud Storage 存储文档的场景。
- 示例:
// 加载 Google Cloud Storage 存储桶中的文件
Document document = GoogleCloudStorageDocumentLoader.loadDocument("gs://bucket-name/path/to/file.txt");
8. SeleniumDocumentLoader
使用 Selenium 从网页加载文档。它支持从动态生成的网页加载内容,例如需要 JavaScript 渲染的页面。
- 用途:适用于加载需要动态渲染的网页内容。
- 示例:
// 加载动态网页内容
Document document = SeleniumDocumentLoader.loadDocument("https://example.com/dynamic-page");
9. TencentCosDocumentLoader
从腾讯云 COS(Cloud Object Storage)加载文档。它支持从腾讯云 COS 存储桶加载文件。
- 用途:适用于使用腾讯云 COS 存储文档的场景。
- 示例:
// 加载腾讯云 COS 存储桶中的文件
Document document = TencentCosDocumentLoader
.loadDocument("cos://bucket-name/path/to/file.txt");
4 Document Parser(文档解析器)
LangChain4j 中的 DocumentParser 接口及其多种实现。DocumentParser 的作用是从各种文件格式中提取文本内容,以便后续处理和分析
DocumentParser 接口用于解析不同格式的文件,如 PDF、DOC、TXT 等。LangChain4j 提供了多种实现,例如 ApacheTikaDocumentParser 可以自动检测和解析几乎所有文件格式。
LangChain4j 提供了多种 DocumentParser 实现,支持从不同格式的文件中提取文本内容。这些解析器包括:
- TextDocumentParser:适用于纯文本文件。
- ApachePdfBoxDocumentParser:适用于 PDF 文件。
- ApachePoiDocumentParser:适用于 Microsoft Office 文件。
- ApacheTikaDocumentParser:适用于多种文件格式。
通过合理选择和使用这些解析器,开发者可以灵活地处理各种文档格式,从而为后续的文档处理和检索提供支持。此外,LangChain4j 还支持通过 SPI 自动加载默认解析器,简化了开发流程
1. TextDocumentParser
TextDocumentParser 是一个简单的解析器,用于解析纯文本格式的文件,例如 TXT、HTML 和 Markdown(MD)文件。
- 用途:适用于简单文本文件,尤其是那些不需要复杂解析的文件。
- 示例:
Document document = FileSystemDocumentLoader
.loadDocument("/path/to/file.txt", new TextDocumentParser());
2. ApachePdfBoxDocumentParser
ApachePdfBoxDocumentParser 使用 Apache PDFBox 库来解析 PDF 文件。
- 用途:适用于 PDF 文件,能够提取文本内容。
- 示例:
Document document = FileSystemDocumentLoader
.loadDocument("/path/to/file.pdf", new ApachePdfBoxDocumentParser());
3. ApachePoiDocumentParser
ApachePoiDocumentParser 使用 Apache POI 库来解析 Microsoft Office 文件格式,例如 DOC、DOCX、PPT、PPTX、XLS 和 XLSX。
- 用途:适用于 Microsoft Office 文件,能够提取文本内容。
- 示例:
Document document = FileSystemDocumentLoader.loadDocument("/path/to/file.docx", new ApachePoiDocumentParser());
4. ApacheTikaDocumentParser
ApacheTikaDocumentParser 使用 Apache Tika 库来自动检测并解析几乎所有现有的文件格式。
- 用途:适用于多种文件格式,尤其是当不确定文件类型时。
- 示例:
Document document = FileSystemDocumentLoader
.loadDocument("/path/to/file.unknown", new ApacheTikaDocumentParser());
5 Document Transformer(文档转换器)
DocumentTransformer 的实现可以执行多种文档转换操作,包括:
- 清理(Cleaning):从文档的文本中移除不必要的噪声,这可以节省令牌(tokens)并减少干扰。
- 过滤(Filtering):从搜索中完全排除特定的文档。
- 丰富(Enriching):向文档中添加额外信息,以潜在地增强搜索结果。
- 总结(Summarizing):对文档进行总结,并将其简短的总结存储在元数据中。稍后,这些总结可以被包含在每个 TextSegment 中(我们将在下面讨论),以潜在地改善搜索效果。
- 等等。
在这个阶段,也可以添加、修改或删除元数据条目。
目前,LangChain4j 提供的现成实现只有 langchain4j-document-transformer-jsoup 模块中的 HtmlToTextDocumentTransformer,它可以从未加工的 HTML 中提取所需的文本内容和元数据条目。
由于没有一种放之四海而皆准的解决方案,我们建议根据你的独特数据实现自己的 DocumentTransformer。
1. 清理(Cleaning)
功能:从文档的文本中移除不必要的噪声,例如多余的空格、HTML 标签、特殊字符等。
目的:
- 节省令牌:减少文档的大小,从而节省在语言模型中使用的令牌数量。
- 减少干扰:提高文档的可读性和语言模型的理解能力。
示例:
Document document = new Document("This is a sample document with unnecessary noise. \n\n");
document = new CleaningDocumentTransformer().transform(document);
2. 过滤(Filtering)
功能:从搜索结果中完全排除特定的文档。
目的:
- 提高相关性:确保搜索结果只包含与查询相关的文档。
- 节省资源:避免处理无关的文档,节省计算资源。
示例:
List<Document> documents = ...; // 从某个来源加载的文档列表
documents = new FilteringDocumentTransformer()
.transform(documents);
3. 丰富(Enriching)
功能:向文档中添加额外信息,例如关键词、摘要、来源等。
目的:
- 增强搜索结果:通过添加更多上下文信息,提高文档在搜索中的相关性。
- 提供更多信息:为语言模型提供更多背景信息,帮助其生成更准确的回答。
示例:
Document document = new Document("This is a sample document.");
document = new EnrichingDocumentTransformer()
.transform(document);
4. 总结(Summarizing)
功能:对文档进行总结,并将总结存储在元数据中。
目的:
- 提高效率:通过总结,语言模型可以更快地理解文档的核心内容。
- 改善搜索:总结可以被包含在每个 TextSegment 中,从而提高搜索的准确性。
示例:
Document document = new Document("This is a long document with detailed information.");
document = new SummarizingDocumentTransformer().transform(document);
String summary = document.getMetadata().getString("summary");
5. 元数据操作
在文档转换阶段,还可以添加、修改或删除元数据条目。
目的:
- 提供上下文:元数据可以为文档提供额外的上下文信息。
- 动态调整:根据需要动态调整文档的元数据。
示例:
Document document = new Document("This is a sample document.");
document.getMetadata().put("source", "file://path/to/document.txt");
document.getMetadata().put("author", "John Doe");
6. 现成实现
目前,LangChain4j 提供的现成实现只有 HtmlToTextDocumentTransformer,它使用 Jsoup 库从未加工的 HTML 中提取所需的文本内容和元数据条目。
用途:
- HTML 文档处理:适用于从网页或 HTML 文件中提取纯文本内容。
- 元数据提取:可以提取 HTML 中的元数据,例如标题、描述等。
示例:
Document document = FileSystemDocumentLoader
.loadDocument("/path/to/file.html", new HtmlToTextDocumentTransformer());
7. 自定义实现
由于没有一种通用的解决方案,LangChain4j 建议开发者根据自己的数据特点实现自己的 DocumentTransformer。
建议:
- 自定义逻辑:根据你的数据格式和需求,实现特定的转换逻辑。
- 灵活性:自定义转换器可以更好地适应你的应用场景。
示例:
public class CustomDocumentTransformer implements DocumentTransformer {
@Override
public Document transform(Document document) {
// 自定义转换逻辑
String cleanedText = document.getText().replaceAll("\\s+", " ");
document.setText(cleanedText);
document.getMetadata().put("customKey", "customValue");
return document;
}
}
6 Text Segment(文本片段)
一旦你的文档被加载,下一步就是将它们分割成更小的片段(分块)。LangChain4j 的领域模型中包含了一个 TextSegment 类,它代表文档的一个片段。顾名思义,TextSegment 只能表示文本信息。
TextSegment 是 LangChain4j 中的一个重要类,用于表示文档的一个片段。通过将文档分割成片段,可以更高效地处理和检索文档内容,同时减少令牌消耗和提高检索质量。开发者可以根据具体需求选择是否分割文档,并采用合适的策略来确保片段提供足够的上下文信息。通过合理使用
TextSegment,可以显著提高 RAG 流程的效率和效果。
1. TextSegment 的作用
TextSegment 是 LangChain4j 中的一个类,用于表示文档的一个片段。它只能包含文本信息,不支持其他类型的内容(如图像或表格)。通过将文档分割成多个片段,可以更高效地处理和检索文档内容。
2. 是否需要分割文档?
是否需要将文档分割成片段取决于多个因素,包括语言模型的上下文窗口大小、处理效率、成本以及检索质量。以下是两种常见的处理方法:
2.1 不分割文档(原子性文档)
在这种方法中,每个文档被视为一个不可分割的整体。在 RAG 流程中,检索最相关的 N 个文档并将其注入到提示中。这种方法的优点是不会丢失上下文,但缺点包括:
- 消耗更多的令牌,因为需要处理整个文档。
- 文档可能包含多个部分或主题,而这些部分并非都与查询相关。
- 向量搜索的质量可能受到影响,因为不同长度的文档被压缩成固定长度的向量。
2.2 分割文档(片段化)
在这种方法中,文档被分割成更小的片段,如章节、段落或句子。在 RAG 流程中,检索最相关的 N 个片段并将其注入到提示中。这种方法的优点包括:
- 提高向量搜索的质量,因为片段更小且更易于处理。
- 减少令牌消耗,因为只处理相关片段。
- 提高检索效率,因为片段更小且更易于检索。
然而,这种方法的挑战在于确保每个片段都提供足够的上下文信息,以便语言模型能够理解。如果上下文不足,语言模型可能会误解片段内容并产生幻觉。为了解决这个问题,可以采用以下高级技术:
- 句子窗口检索:检索片段周围的句子,提供更多的上下文。
- 自动合并检索:自动合并相关片段,提供更完整的上下文。
- 父文档检索:检索片段的父文档,提供更广泛的上下文。
3. TextSegment 的方法
TextSegment 提供了以下方法,用于操作和管理文本片段:
- TextSegment.text():返回片段的文本内容。
- TextSegment.metadata():返回片段的元数据。
- TextSegment.from(String, Metadata):从文本和元数据创建一个新的 TextSegment。
- TextSegment.from(String):从文本创建一个新的 TextSegment,使用空的元数据。
4. 示例代码
以下是一个示例,展示如何使用 TextSegment:
// 创建一个 TextSegment
Metadata metadata = Metadata.from(Map.of("source", "file1.txt", "author", "John Doe"));
TextSegment segment = TextSegment.from("This is a sample text segment.", metadata);
// 获取文本内容
String text = segment.text(); // 返回 "This is a sample text segment."
// 获取元数据
Metadata segmentMetadata = segment.metadata(); // 返回包含 source 和 author 的 Metadata
5. 分割文档时,如何选择合适的分割大小
选择合适的片段大小(Segment Size)是文档分割过程中的一个重要决策,因为它直接影响到检索效率、语言模型的处理能力和生成结果的质量。以下是一些选择合适片段大小的建议和考虑因素:
1. 语言模型的上下文窗口限制
语言模型(LLM)通常有一个固定的上下文窗口大小,即它能够处理的最大文本量。例如:
- GPT-3 的上下文窗口为 2048 个令牌(tokens)。
- GPT-4 的上下文窗口为 8192 个令牌。
如果片段大小超过语言模型的上下文窗口,那么在处理时可能会被截断,导致信息丢失。因此,片段大小应小于或等于语言模型的上下文窗口大小。
建议:
- 如果使用 GPT-3,片段大小可以设置为 1500-2000 个令牌。
- 如果使用 GPT-4,片段大小可以设置为 6000-7000 个令牌。
2. 检索效率
较小的片段可以提高检索效率,因为它们更容易被嵌入模型处理,并且在向量数据库中的相似性搜索会更快。然而,如果片段过小,可能会丢失上下文信息,导致语言模型无法理解片段的完整含义。
建议:
- 最小片段大小:至少包含一个完整的句子或段落,以确保有足够的上下文。
- 最大片段大小:不超过语言模型的上下文窗口大小。
3. 信息完整性
片段应包含足够的上下文信息,以便语言模型能够理解其内容。如果片段过小,可能会丢失重要的上下文信息,导致语言模型产生幻觉(hallucinations)。如果片段过大,则可能会包含无关信息,增加处理成本。
建议:
- 自然边界:根据文档的自然结构进行分割,例如按段落、章节或句子分割。
- 重叠:在片段之间设置一定的重叠,以确保上下文的连贯性。例如,每个片段可以有 50-100 个令牌的重叠。
4. 成本考虑
较大的片段会消耗更多的令牌,增加处理成本。较小的片段可以减少令牌消耗,但可能会降低检索质量。
建议:
- 平衡成本和质量:选择一个既能满足语言模型上下文需求,又能减少令牌消耗的片段大小。
- 实验和优化:通过实验找到最适合你数据和需求的片段大小。
5. 实践中的片段大小
在实际应用中,片段大小通常根据文档类型和语言模型的特性进行调整。以下是一些常见的片段大小范围:
- 文本文件(TXT):每个片段包含 200-500 个单词,大约 1000-2000 个令牌。
- PDF 文件:每个片段包含 1-2 个段落,大约 300-500 个令牌。
- HTML 页面:
每个片段包含一个完整的段落或一个 HTML 元素(如 <p> 或 <div>),大约 200-300 个令牌。
6. 示例
假设你使用的是 GPT-3,其上下文窗口为 2048 个令牌。你可以选择以下片段大小:
- 段落分割:每个片段包含一个完整的段落,大约 300-500 个令牌。
- 句子分割:每个片段包含 3-5 个句子,大约 200-300 个令牌。
- 章节分割:每个片段包含一个完整的章节,但不超过 2000 个令牌。
示例代码:
// 使用 DocumentSplitter 将文档分割成片段
DocumentSplitter splitter = DocumentSplitters.byParagraph(500, 50); // 每个片段最多 500 个令牌,重叠 50 个令牌
List<TextSegment> segments = splitter.split(document);
7 Document Splitter(文档分割器)
DocumentSplitter 是 LangChain4j 中用于将文档分割成更小片段(TextSegment)的工具。分割文档是 RAG(检索增强生成)流程中的一个重要步骤,因为它可以提高检索效率和语言模型的处理能力。以下是每个分割器的详细解析:
1. DocumentByParagraphSplitter
按段落分割文档,每个段落被视为一个独立的单元。
用途:适用于需要按自然段落结构分割文档的场景。
示例:
DocumentSplitter splitter = new DocumentByParagraphSplitter(500, 50); // 每个片段最多 500 个令牌,重叠 50 个令牌
List<TextSegment> segments = splitter.split(document);
2. DocumentByLineSplitter
按行分割文档,每行被视为一个独立的单元。
用途:适用于按行分割的文本文件,例如代码文件或日志文件。
示例:
DocumentSplitter splitter = new DocumentByLineSplitter(100, 10); // 每行最多 100 个字符,重叠 10 个字符
List<TextSegment> segments = splitter.split(document);
3. DocumentBySentenceSplitter
按句子分割文档,使用自然语言处理库(如 OpenNLP)检测句子边界。
用途:适用于需要按句子结构分割文档的场景,尤其是语言模型需要理解句子上下文的情况。
示例:
DocumentSplitter splitter = new DocumentBySentenceSplitter(200, 20); // 每个句子最多 200 个令牌,重叠 20 个令牌
List<TextSegment> segments = splitter.split(document);
4. DocumentByWordSplitter
按单词分割文档,每个单词被视为一个独立的单元。
用途:适用于需要按单词分割的场景,例如词频分析或关键词提取。
示例:
DocumentSplitter splitter = new DocumentByWordSplitter(50, 5); // 每个片段最多 50 个单词,重叠 5 个单词
List<TextSegment> segments = splitter.split(document);
5. DocumentByCharacterSplitter
按字符分割文档,每个字符被视为一个独立的单元。
用途:适用于需要按字符分割的场景,例如字符级别的语言模型。
示例:
DocumentSplitter splitter = new DocumentByCharacterSplitter(100, 10); // 每个片段最多 100 个字符,重叠 10 个字符
List<TextSegment> segments = splitter.split(document);
6. DocumentByRegexSplitter
使用正则表达式分割文档,可以根据自定义的模式分割文档。
用途:适用于需要根据特定模式分割文档的场景,例如分割特定格式的日志文件或代码文件。
示例:
DocumentSplitter splitter = new DocumentByRegexSplitter("\\n\\n", 500, 50); // 使用两个换行符分割,每个片段最多 500 个令牌,重叠 50 个令牌
List<TextSegment> segments = splitter.split(document);
7. 递归分割器(Recursive Splitter)
递归分割器可以处理无法放入单个 TextSegment 的较大单元。它会调用一个子分割器,将这些单元进一步分割成更细粒度的单元。
用途:适用于需要处理大型文档或复杂结构的场景。
示例:
DocumentSplitter splitter = DocumentSplitters.recursive(500, 50, new DocumentByParagraphSplitter()); // 递归分割,每个片段最多 500 个令牌,重叠 50 个令牌
List<TextSegment> segments = splitter.split(document);
8. 片段的元数据
在分割过程中,所有元数据条目都会从原始文档复制到每个 TextSegment 中,并为每个片段添加一个唯一的 “index” 元数据条目。这有助于在后续处理中跟踪片段的来源和顺序。
示例:
Metadata metadata = document.getMetadata();
for (TextSegment segment : segments) {
System.out.println(segment.getMetadata().getString("index")); // 输出每个片段的索引
}
9. 总结
DocumentSplitter 是 LangChain4j 中的一个重要接口,用于将文档分割成更小的片段(TextSegment)。通过选择合适的分割器和参数,可以优化文档的处理效率和语言模型的生成质量。以下是选择分割器时需要考虑的关键点:
- 上下文窗口:片段大小应小于或等于语言模型的上下文窗口。
- 自然边界:根据文档的自然结构进行分割,例如按段落、句子或章节。
- 重叠:在片段之间设置一定的重叠,以确保上下文的连贯性。
- 实验和优化:通过实验找到最适合你数据和需求的分割策略。
通过合理使用 DocumentSplitter,可以显著提高 RAG 流程的效率和效果。
8 Text Segment Transformer(文本片段转换器)
TextSegmentTransformer 类似于 DocumentTransformer,但用于转换文本片段。TextSegmentTransformer 是一个接口,用于对 TextSegment 进行转换和优化,以提高后续处理和检索的效果。
TextSegmentTransformer 是 LangChain4j 中的一个重要接口,用于对 TextSegment进行转换和优化。通过实现自定义的TextSegmentTransformer,开发者可以根据自己的数据特点和需求,对文本片段进行清理、丰富和格式调整。在每个TextSegment 中包含文档的标题或摘要是一种有效的技术,可以显著提高检索效果和语言模型的生成质量。
1. TextSegmentTransformer 的作用
TextSegmentTransformer 用于对 TextSegment 进行转换和优化。它可以帮助:
- 清理文本:移除不必要的噪声,节省令牌并减少干扰。
- 丰富内容:添加额外信息,例如文档标题或摘要,以提高检索效果。
- 调整格式:根据需要调整文本片段的格式,使其更适合语言模型处理。
2. 为什么需要自定义实现?
与 DocumentTransformer 类似,TextSegmentTransformer 没有通用的解决方案,因为不同的数据集和应用场景可能需要不同的处理逻辑。因此,LangChain4j 建议开发者根据自己的需求实现自定义的TextSegmentTransformer。
3. 提高检索效果的技术
一种有效的技术是在每个 TextSegment 中包含文档的标题或简短摘要。这样做的好处包括:
- 提供上下文:标题或摘要可以为语言模型提供额外的上下文信息,帮助其更好地理解片段的内容。
- 提高相关性:在检索时,这些额外信息可以帮助语言模型更准确地判断- 片段的相关性,从而提高检索质量。
- 减少幻觉:通过提供更多的上下文信息,可以减少语言模型生成无关内容的可能性。
4. 示例代码
以下是一个简单的 TextSegmentTransformer 实现示例,展示如何在每个 TextSegment 中添加文档标题和摘要:
public class TitleAndSummaryTextSegmentTransformer implements TextSegmentTransformer {
@Override
public TextSegment transform(TextSegment textSegment) {
// 获取文档的标题和摘要
String title = textSegment.getMetadata().getString("title");
String summary = textSegment.getMetadata().getString("summary");
// 构造新的文本内容,包含标题和摘要
String transformedText = title + "\n\n" + summary + "\n\n" + textSegment.text();
// 创建新的 TextSegment
return TextSegment.from(transformedText, textSegment.getMetadata());
}
}
5. 使用自定义 TextSegmentTransformer
在实际应用中,你可以将自定义的 TextSegmentTransformer 集成到文档处理流程中。例如:
写一个自定义的转换类
public class TitleAndSummaryTextSegmentTransformer implements TextSegmentTransformer {
@Override
public TextSegment transform(TextSegment textSegment) {
// 获取文档的标题和摘要
String title = textSegment.getMetadata().getString("title");
String summary = textSegment.getMetadata().getString("summary");
// 构造新的文本内容,包含标题和摘要
String transformedText = title + "\n\n" + summary + "\n\n" + textSegment.text();
// 创建新的 TextSegment
return TextSegment.from(transformedText, textSegment.getMetadata());
}
}
使用自定义转换类
// 加载文档
Document document = FileSystemDocumentLoader.loadDocument("/path/to/file.txt");
// 分割文档为文本片段
DocumentSplitter splitter = new DocumentByParagraphSplitter(500, 50);
List<TextSegment> segments = splitter.split(document);
// 应用自定义 TextSegmentTransformer
TitleAndSummaryTextSegmentTransformer transformer = new TitleAndSummaryTextSegmentTransformer();
List<TextSegment> transformedSegments = segments.stream()
.map(transformer::transform)
.collect(Collectors.toList());
// 输出转换后的片段
transformedSegments.forEach(segment -> System.out.println(segment.text()));
9 Embedding(嵌入)
Embedding 类封装了表示文本语义的数值向量。LangChain4j 提供了多种嵌入模型,用于将文本转换为嵌入。
概述
Embedding 类封装了一个数值向量,该向量表示已嵌入内容的“语义含义”(通常是文本,例如 TextSegment)。
了解更多关于向量嵌入的信息,请参考以下资源:
- What are Vector Embeddings? | A Comprehensive Vector Embeddings Guide
- What are Vector Embeddings | Pinecone
- Meet AI’s multitool: Vector embeddings
有用的方法
- Embedding.dimension():返回嵌入向量的维度(长度)。
- CosineSimilarity.between(Embedding, Embedding):计算两个嵌入之间的余弦相似度。
- Embedding.normalize():归一化嵌入向量(原地操作)。
以下是一个简单的示例,展示如何使用 Embedding 类及其方法:
import dev.langchain4j.embedding.Embedding;
import dev.langchain4j.embedding.CosineSimilarity;
// 创建两个嵌入向量
Embedding embedding1 = new Embedding(new double[]{0.1, 0.2, 0.3});
Embedding embedding2 = new Embedding(new double[]{0.2, 0.3, 0.4});
// 计算嵌入向量的维度
int dimension1 = embedding1.dimension(); // 返回 3
int dimension2 = embedding2.dimension(); // 返回 3
// 计算两个嵌入向量之间的余弦相似度
double similarity = CosineSimilarity.between(embedding1, embedding2); // 返回相似度值
// 归一化嵌入向量
embedding1.normalize(); // 原地归一化
embedding2.normalize(); // 原地归一化
深度剖析
1. Embedding 的作用
Embedding 是一种将数据(如文本、图像、音频等)转换为数值向量的技术,这些向量能够捕捉数据的语义含义和上下文信息。通过嵌入向量,机器学习算法可以更有效地处理和理解数据。
2. 嵌入向量的类型
嵌入向量有多种类型,适用于不同的数据和应用场景:
- 文本嵌入(Text Embeddings):将单词、句子或文档转换为向量。
- 图像嵌入(Image Embeddings):将图像转换为向量,用于图像分类、相似性搜索等。
- 用户嵌入(User Embeddings):将用户的行为和偏好转换为向量,用于个性化推荐。
- 产品嵌入(Product Embeddings):将产品的属性和特征转换为向量,用于推荐系统。
3. 嵌入向量的创建
嵌入向量可以通过以下方式创建:
- 预训练模型:使用现成的预训练模型(如 Word2Vec、GloVe、BERT)将文本转换为嵌入向量。
- 自定义模型:使用深度学习模型(如卷积神经网络 CNN、Transformer)对特定数据集进行训练,生成嵌入向量。
4. 嵌入向量的应用
嵌入向量在多个领域有广泛应用:
- 自然语言处理(NLP):情感分析、文本分类、机器翻译、问答系统。
- 推荐系统:基于用户和物品的嵌入向量进行个性化推荐。
- 搜索引擎:通过嵌入向量实现语义搜索。
- 图像和视频分析:图像分类、目标检测、相似性搜索。
- 异常检测:通过嵌入向量检测数据中的异常模式。
5. 嵌入向量的相似性度量
嵌入向量之间的相似性可以通过以下方法计算:
- 余弦相似度(Cosine Similarity):计算两个向量之间的夹角余弦值,值越接近 1 表示越相似。
- 欧几里得距离(Euclidean Distance):计算两个向量之间的直线距离,值越小表示越相似。
- 点积(Dot Product):计算两个向量的点积,值越大表示越相似。
10 Embedding Model(嵌入模型)
概述
EmbeddingModel 接口表示一种特殊的模型,能够将文本转换为嵌入(文本的向量表示)。目前支持的嵌入模型可以在这里找到。
有用的方法
- EmbeddingModel.embed(String):将给定的文本嵌入。
- EmbeddingModel.embed(TextSegment):将给定的 TextSegment 嵌入。
- EmbeddingModel.embedAll(List):将给定的 TextSegment 列表嵌入。
- EmbeddingModel.dimension():返回此模型生成的嵌入的维度。
示例代码
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.data.segment.TextSegment;
// 创建一个嵌入模型实例
EmbeddingModel embeddingModel = new OpenAiEmbeddingModel("your-api-key", "text-embedding-ada-002");
// 嵌入单个文本
Embedding embedding = embeddingModel.embed("This is a sample text.");
// 嵌入 TextSegment
TextSegment textSegment = TextSegment.from("This is another sample text.");
Embedding segmentEmbedding = embeddingModel.embed(textSegment);
// 嵌入多个 TextSegment
List<TextSegment> textSegments = List.of(
TextSegment.from("First sample text."),
TextSegment.from("Second sample text.")
);
List<Embedding> embeddings = embeddingModel.embedAll(textSegments);
// 获取嵌入的维度
int dimension = embeddingModel.dimension();
System.out.println("Embedding dimension: " + dimension);
EmbeddingModel 是 LangChain4j 中的一个核心接口,用于将文本或其他数据类型转换为嵌入向量。嵌入向量是一种数值表示,能够捕捉数据的语义含义和上下文信息,使得机器学习算法可以更有效地处理和理解数据
11 Embedding Store(嵌入存储)
EmbeddingStore 是 LangChain4j 中的一个核心接口,用于存储和检索嵌入向量。嵌入存储通常被称为向量数据库,它允许高效地存储和搜索相似的嵌入向量
EmbeddingStore 是 LangChain4j中的一个重要接口,用于存储和检索嵌入向量。这些向量可以单独存储,也可以与原始数据(如TextSegment)一起存储。通过向量数据库,可以高效地执行相似性搜索和嵌入管理。LangChain4j提供了多种嵌入存储的实现,开发者可以根据具体需求选择合适的存储方案。
11.1 EmbeddingStore 概述
11.1.1 EmbeddingStore 的作用
EmbeddingStore 的主要作用是存储和检索嵌入向量。这些向量可以单独存储,也可以与原始数据(如 TextSegment)一起存储。通过向量数据库,可以高效地执行以下操作:
- 存储嵌入:将嵌入向量及其对应的原始数据存储在数据库中。
- 搜索相似嵌入:通过向量相似性搜索,找到与给定向量最相似的嵌入。
- 管理嵌入:添加、移除或更新嵌入向量。
11.1.2 支持的嵌入存储
LangChain4j 支持多种嵌入存储的实现,包括但不限于:
- 内存存储(InMemoryEmbeddingStore):适用于开发和测试阶段,将嵌入存储在内存中。
- Milvus:一个高性能的向量数据库,适用于大规模生产环境。
- Pinecone:一个云原生的向量数据库,提供高效的相似性搜索。
- PGVector:基于 PostgreSQL 的向量数据库扩展,适用于需要与关系数据库集成的场景。
- Qdrant:一个高性能的向量数据库,支持多种索引类型。
11.1.3 嵌入存储的方法
- EmbeddingStore 提供了以下方法:
- add(Embedding):将给定的嵌入添加到存储中,并返回一个随机 ID。
- add(String id, Embedding):将给定的嵌入及其指定的 ID 添加到存储中。
- add(Embedding, TextSegment):将给定的嵌入及其关联的 TextSegment 添加到存储中,并返回一个随机 ID。
- addAll(List):将给定的嵌入列表添加到存储中,并返回一个随机 ID 列表。
- addAll(List, List):将给定的嵌入列表及其关联的 TextSegment 列表添加 到存储中,并返回一个随机 ID 列表。
- addAll(List ids, List, List):将给定的嵌入列表及其关联的 ID 和 TextSegment 列表添加到存储中。
- search(EmbeddingSearchRequest):搜索最相似的嵌入。
- remove(String id):通过 ID 从存储中移除单个嵌入。
- removeAll(Collection ids):通过 ID 从存储中移除多个嵌入。
- removeAll(Filter):从存储中移除所有匹配指定过滤器的嵌入。
- removeAll():从存储中移除所有嵌入。
11.1.4 示例代码
以下是一个简单的示例,展示如何使用 EmbeddingStore:
import dev.langchain4j.embedding.Embedding;
import dev.langchain4j.embedding.EmbeddingStore;
import dev.langchain4j.embedding.EmbeddingSearchRequest;
import dev.langchain4j.embedding.EmbeddingSearchResult;
// 创建一个嵌入存储实例
EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>();
// 创建嵌入向量
Embedding embedding1 = new Embedding(new double[]{0.1, 0.2, 0.3});
Embedding embedding2 = new Embedding(new double[]{0.2, 0.3, 0.4});
// 添加嵌入到存储中
String id1 = embeddingStore.add(embedding1);
String id2 = embeddingStore.add(embedding2);
// 添加嵌入及其关联的 TextSegment
TextSegment textSegment1 = TextSegment.from("This is a sample text.");
TextSegment textSegment2 = TextSegment.from("This is another sample text.");
embeddingStore.add(embedding1, textSegment1);
embeddingStore.add(embedding2, textSegment2);
// 搜索最相似的嵌入
EmbeddingSearchRequest request = new EmbeddingSearchRequest(embedding1, 5);
EmbeddingSearchResult result = embeddingStore.search(request);
List<Embedding> similarEmbeddings = result.getMatches();
// 移除嵌入
embeddingStore.remove(id1);
embeddingStore.removeAll(List.of(id2));
// 移除所有嵌入
embeddingStore.removeAll();
11.1.5 嵌入存储的应用
嵌入存储在多种应用场景中非常有用,例如:
- 语义搜索:通过嵌入向量实现语义相似性搜索。
- 推荐系统:通过嵌入向量推荐相似的项目。
- 自然语言处理:用于情感分析、文本分类等任务。
11.2 EmbeddingSearchRequest(嵌入搜索请求)
EmbeddingSearchRequest 表示在 EmbeddingStore 中进行搜索的请求。它具有以下属性:
- Embedding queryEmbedding:是一个嵌入向量,用作搜索的参考点。搜索时,嵌入存储会找到与这个参考向量最相似的嵌入向量。
- int maxResults:返回的最大结果数。这是一个可选参数,默认值为 3。
- double minScore:最小分数,范围从 0 到 1(包括 0 和 1)。只有分数大于或等于 minScore 的嵌入才会被返回。这是一个可选参数,默认值为 0。
- Filter filter:在搜索过程中应用于元数据(Metadata)的过滤器。只有元数据与过滤器匹配的 TextSegment 会被返回。
11.3 Filter(过滤器)
Filter 是一个用于在执行向量搜索时过滤元数据条目的工具。目前支持以下过滤器类型/操作:
- IsEqualTo:等于某个值。
- IsNotEqualTo:不等于某个值。
- IsGreaterThan:大于某个值。
- IsGreaterThanOrEqualTo:大于或等于某个值。
- IsLessThan:小于某个值。
- IsLessThanOrEqualTo:小于或等于某个值。
- IsIn:在某个集合中。
- IsNotIn:不在某个集合中。
- ContainsString:包含某个字符串。
- And:逻辑与。
- Not:逻辑非。
- Or:逻辑或。
注意: 并非所有嵌入存储都支持元数据过滤。具体支持情况可以参考相关文档。 一些支持元数据过滤的存储可能不支持所有过滤器类型/操作。例如,ContainsString 目前仅被 Milvus、PgVector 和
Qdrant 支持。
11.4 EmbeddingSearchResult(嵌入搜索结果)
EmbeddingSearchResult 表示在 EmbeddingStore 中搜索的结果。它包含一个 EmbeddingMatch 列表。
11.5 EmbeddingMatch(嵌入匹配)
EmbeddingMatch 表示一个匹配的嵌入,包括其相关性分数、ID 和原始嵌入数据(通常是 TextSegment)。
11.6 示例代码
搜索与存储的简单示例
以下是一个简单的示例,展示如何使用 EmbeddingSearchRequest 和 EmbeddingStore 进行搜索:
import dev.langchain4j.embedding.Embedding;
import dev.langchain4j.embedding.EmbeddingStore;
import dev.langchain4j.embedding.EmbeddingSearchRequest;
import dev.langchain4j.embedding.EmbeddingSearchResult;
import dev.langchain4j.embedding.EmbeddingMatch;
import dev.langchain4j.filter.Filter;
import dev.langchain4j.filter.IsEqualTo;
// 创建一个嵌入存储实例
EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>();
// 创建嵌入向量
Embedding queryEmbedding = new Embedding(new double[]{0.1, 0.2, 0.3});
// 创建搜索请求
EmbeddingSearchRequest request = new EmbeddingSearchRequest(
queryEmbedding,
5, // 最多返回 5 个结果
0.75 // 最小相似度分数为 0.75
);
// 应用元数据过滤器
Filter filter = new IsEqualTo("source", "file1.txt");
request.setFilter(filter);
// 执行搜索
EmbeddingSearchResult result = embeddingStore.search(request);
// 处理搜索结果
List<EmbeddingMatch> matches = result.getMatches();
for (EmbeddingMatch match : matches) {
System.out.println("Match ID: " + match.getId());
System.out.println("Score: " + match.getScore());
System.out.println("TextSegment: " + match.getTextSegment().text());
}
搜索结果与嵌入匹配示例
import dev.langchain4j.embedding.Embedding;
import dev.langchain4j.embedding.EmbeddingStore;
import dev.langchain4j.embedding.EmbeddingSearchRequest;
import dev.langchain4j.embedding.EmbeddingSearchResult;
import dev.langchain4j.embedding.EmbeddingMatch;
import dev.langchain4j.data.segment.TextSegment;
// 创建一个嵌入存储实例
EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>();
// 创建查询嵌入
Embedding queryEmbedding = new Embedding(new double[]{0.1, 0.2, 0.3});
// 创建搜索请求
EmbeddingSearchRequest request = new EmbeddingSearchRequest(queryEmbedding, 5, 0.75);
// 执行搜索
EmbeddingSearchResult result = embeddingStore.search(request);
// 处理搜索结果
List<EmbeddingMatch> matches = result.getMatches();
for (EmbeddingMatch match : matches) {
System.out.println("Match ID: " + match.getId());
System.out.println("Score: " + match.getScore());
System.out.println("Embedded Data: " + match.getTextSegment().text());
}
12 EmbeddingStoreIngestor(嵌入存储摄取器)
EmbeddingStoreIngestor 是 LangChain4j 中的一个工具类,用于将文档摄取到嵌入存储中。它提供了灵活的配置选项,使得开发者可以根据需要对文档进行预处理、分割和嵌入
EmbeddingStoreIngestor 是 LangChain4j中的一个强大工具,用于将文档摄取到嵌入存储中。它支持多种可选功能,包括文档转换、分割和片段转换,使得开发者可以根据需要对文档进行预处理。通过合理配置EmbeddingStoreIngestor,可以显著提高嵌入存储的效率和质量。
1. 基本功能
EmbeddingStoreIngestor 的核心功能是将文档嵌入并存储到嵌入存储中。它支持以下操作:
- 嵌入文档:使用指定的嵌入模型将文档转换为嵌入向量。
- 存储嵌入:将嵌入向量及其对应的文档存储到嵌入存储中。
2. IngestionResult
IngestionResult 是 EmbeddingStoreIngestor 的 ingest() 方法的返回值,包含以下信息:
- TokenUsage:显示嵌入过程中使用的令牌数量。
- 其他信息:可能包含摄取过程中的其他有用信息。
示例代码如下:
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));
3. 可选功能
EmbeddingStoreIngestor 提供了多种可选功能,用于在嵌入之前对文档进行预处理:
- DocumentTransformer:用于清理、丰富或格式化文档。
- DocumentSplitter:用于将文档分割成较小的 TextSegment,以提高相似性搜索的质量并减少发送给 LLM 的提示的大小和成本。
- TextSegmentTransformer:用于清理、丰富或格式化 TextSegment。
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// 为每个文档添加 userId 元数据条目,以便后续可以根据它进行过滤
.documentTransformer(document -> {
document.metadata().put("userId", "12345");
return document;
})
// 将每个文档分割成每个包含 1000 个令牌的 `TextSegment`,并有 200 个令牌的重叠
.documentSplitter(DocumentSplitters.recursive(1000, 200, new OpenAiTokenizer()))
// 为每个 `TextSegment` 添加文档名称,以提高搜索质量
.textSegmentTransformer(textSegment -> TextSegment.from(
textSegment.metadata("file_name") + "\n" + textSegment.text(),
textSegment.metadata()
))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
4. 示例代码
以下是一个完整的示例,展示如何配置和使用 EmbeddingStoreIngestor:
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.embedding.EmbeddingModel;
import dev.langchain4j.embedding.EmbeddingStore;
import dev.langchain4j.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.embedding.IngestionResult;
import dev.langchain4j.splitter.DocumentSplitter;
import dev.langchain4j.splitter.DocumentSplitters;
import dev.langchain4j.tokenizer.OpenAiTokenizer;
// 创建嵌入模型和嵌入存储实例
EmbeddingModel embeddingModel = ...; // 使用 OpenAI 或其他嵌入模型
EmbeddingStore embeddingStore = new InMemoryEmbeddingStore<>();
// 创建文档摄取器
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// 添加 userId 元数据条目
.documentTransformer(document -> {
document.metadata().put("userId", "12345");
return document;
})
// 将文档分割成每个包含 1000 个令牌的 TextSegment,重叠 200 个令牌
.documentSplitter(DocumentSplitters.recursive(1000, 200, new OpenAiTokenizer()))
// 为每个 TextSegment 添加文档名称
.textSegmentTransformer(textSegment -> TextSegment.from(
textSegment.metadata("file_name") + "\n" + textSegment.text(),
textSegment.metadata()
))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();
// 加载文档
Document document1 = ...; // 加载文档
Document document2 = ...;
Document document3 = ...;
// 摄取文档
ingestor.ingest(document1);
ingestor.ingest(document2, document3);
// 批量摄取文档
List<Document> documents = List.of(document1, document2, document3);
IngestionResult ingestionResult = ingestor.ingest(documents);
// 查看摄取结果
System.out.println("Tokens used: " + ingestionResult.getTokenUsage());