sidebar_position: 8
—# LangChain4j从入门到精通-10-RAG (Retrieval-Augmented Generation)
本文是《LangChain4j从入门到精通》系列的第十篇,全面解析了如何在Java生态中利用LangChain4j框架实现检索增强生成(RAG),突破大语言模型的知识局限。文章详细介绍了RAG的核心价值:通过“索引-检索-生成”三段式流程,将外部知识库动态注入AI推理过程,有效解决模型幻觉、知识过时及专业领域盲区三大痛点。框架提供Easy RAG(开箱即用)、Naive RAG(基础向量检索)与Advanced RAG(模块化高级流程)三种渐进式方案,并深度剖析了文档加载、分块策略、向量化存储、查询路由、结果重排等核心组件的API设计。通过完整代码示例演示了从文档解析到智能问答的端到端实现,助力开发者快速构建具备专业知识库的AI应用。

#Java #人工智能 #LangChain4j #RAG #大模型应用开发

大语言模型的知识仅限于其训练数据。若要让大语言模型掌握特定领域知识或专有数据,您可以:

  • 使用RAG(检索增强生成)技术,本节将详细介绍
  • 用您的数据对模型进行微调
  • 结合RAG和微调

什么是RAG?

简单来说,RAG(检索增强生成)是一种在将提示发送给大语言模型之前,从数据中查找并注入相关信息片段的方法。这样一来,大语言模型就能(希望如此)获得相关信息,并能够利用这些信息进行回复,从而降低产生幻觉(错误信息)的概率。
可以通过多种方式找到相关信息
信息检索方法 .
最受欢迎的有:

  • 全文(关键词)搜索。该方法采用TF-IDF和BM25等技术,通过将查询中的关键词(例如用户提出的问题)与文档数据库进行匹配来搜索文档。它根据这些关键词在每个文档中的出现频率和相关性对结果进行排序。

  • 向量搜索,也称为“语义搜索”。文本文件通过嵌入模型被转换为数字向量。然后根据查询向量与文档向量之间的余弦相似度或其他相似性/距离度量来查找和排序文档,从而捕捉更深层次的语义含义。

  • 混合搜索。结合多种搜索方法(例如全文+向量)通常能提高搜索效果。
    目前,该页面主要关注向量搜索功能。
    全文搜索和混合搜索目前仅通过Azure AI搜索集成提供支持,详情请参阅 AzureAiSearchContentRetriever。我们计划在不久的将来扩展RAG工具箱,加入全文搜索和混合搜索功能。

RAG 阶段

RAG流程分为两个不同的阶段:索引和检索。LangChain4j为这两个阶段都提供了工具支持。

索引

在索引阶段,文档会经过预处理,以便在检索阶段实现高效搜索。
具体处理流程会根据使用的信息检索方法而有所不同。对于向量搜索而言,通常包括文档清洗、补充数据和元数据、将文档分割为更小的片段(即分块)、对这些片段进行向量嵌入,最后将它们存储到向量数据库(即嵌入存储)中。

索引阶段通常离线进行,这意味着终端用户无需等待其完成。例如,可以通过一个定时任务(如每周周末执行一次)来重新索引公司内部文档。负责索引的代码也可以是一个独立的应用程序,专门处理索引任务。

然而,在某些情况下,终端用户可能希望上传自定义文档,以便大语言模型能够访问这些内容。此时,索引操作需要在线进行,并成为主应用程序的一部分。以下是索引阶段的简化示意图:

在这里插入图片描述

检索

检索阶段通常在线进行,即当用户提交一个需要基于索引文档回答的问题时启动。

这一过程会因采用的信息检索方法不同而有所差异。对于向量搜索而言,通常需要将用户的查询(问题)转化为嵌入向量,并在嵌入存储库中执行相似性搜索。随后,相关片段(原始文档的段落)会被注入提示词并发送给大语言模型。以下是检索阶段的简化示意图:

在这里插入图片描述

LangChain4j中RAG实现

LangChain4j提供了三种RAG变体:

  • Easy RAG:开始使用RAG的最简单方法
  • Naive RAG: 使用向量搜索实现RAG的基本方案
  • Advanced RAG: 一个模块化的RAG框架,支持添加额外步骤,例如查询转换、从多源检索和重新排序

Easy RAG

LangChain4j拥有一项“Easy RAG”功能,让RAG入门变得极其简单。

您无需学习嵌入技术、选择向量数据库、寻找合适的嵌入模型,也无需研究如何解析和分割文档等等。只需指定您的文档即可使用。如果你需要一个可定制的RAG,请参考到next section.

如果你在使用Quarkus,有一种更简单的方法来实现Easy RAG。
请参考Quarkus文档.

:::注意
这种“Easy RAG”的质量当然比不上定制化的RAG方案。但这是学习RAG和/或进行概念验证的最简单方法。之后,你可以轻松地从简易RAG过渡到更高级的RAG,逐步调整和自定义更多方面。

:::

  1. 导入 langchain4j-easy-rag依赖项:
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-easy-rag</artifactId>
    <version>1.10.0-beta18</version>
</dependency>
  1. 让我们加载您的文档:
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
:::

  1. Now, we need to preprocess and store documents in a specialized embedding store, also known as vector database.
    This is necessary to quickly find relevant pieces of information when a user asks a question.
    We can use any of our 30+ supported embedding stores,
    but for simplicity, we will use an in-memory one:
    现在,我们需要对文档进行预处理并存储到专门的嵌入存储中,也称为向量数据库。这是为了在用户提问时能快速找到相关信息。

我们可以使用30多种嵌入存储支持的嵌入存储中的任意一种,但为了方便起见,我们将使用内存中的存储方式:

InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
引擎下发生了什么?
  1. EmbeddingStoreIngestor通过 SPI 从 langchain4j-easy-rag依赖项加载 DocumentSplitter。每个 Document被分割成较小的片段(TextSegment),每个片段不超过 300 个 token,并且有 30 个 token 的重叠部分。
  2. EmbeddingStoreIngestor通过SPI从langchain4j-easy-rag依赖项加载EmbeddingModel。每个TextSegment都会使用EmbeddingModel转换为Embedding。

:::注意
我们已选择bge-small-en-v1.5作为Easy RAG的默认嵌入模型。
它取得了令人印象深刻的成绩MTEB leaderboard,
而其量化版本仅占用24兆字节的空间。因此,我们可以轻松地将其加载到内存中,并在同一进程中运行。 ONNX Runtime.

是的,没错,你可以完全离线将文本转换为嵌入向量,无需任何外部服务,就在同一个JVM进程中完成。LangChain4j提供了一些流行的嵌入模型
out-of-the-box.
:::

  1. 所有 TextSegment-Embedding配对都存储在 EmbeddingStore中
  1. 最后一步是创建 AI Service 这将作为我们与LLM的API接口:
interface Assistant {

    String chat(String userMessage);
}

ChatModel chatModel = OpenAiChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(chatModel)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
    .build();

在这里,我们将Assistant配置为使用OpenAI的LLM来回答用户问题,记住对话中的最近10条消息,并从包含我们文档的EmbeddingStore中检索相关内容。

  1. 现在我们准备好和它聊天了!
String answer = assistant.chat("How to do Easy RAG with LangChain4j?");

核心RAG API

LangChain4j提供了一套丰富的API,让您能够轻松构建自定义的RAG流程,从简单到高级的流程都能实现。在本节中,我们将介绍主要的领域类和API。

文档

Document类代表整个文档,例如单个 PDF 文件或网页。目前,Document仅能表示文本信息,但未来的更新将使其支持图片和表格。

实用方法
  • Document.text() 返回 Document的文本
  • Document.metadata()返回该Document的元数据(请参阅下文的“元数据”部分)
  • Document.toTextSegment() 将 Document转换为 TextSegment(参见下文“TextSegment”部分)
  • Document.from(String, Metadata) 从文本和Metadata创建一个Document
  • Document.from(String)从带有空 Metadata的文本创建一个 Document

元数据

每个 Document都包含 Metadata。它存储了关于该 Document的元信息,例如其名称、来源、最后更新日期、所有者,或其他任何相关细节。
元数据以键值对的形式存储,其中键为String类型,而值可以是以下类型之一:String、Integer、Long、Float、Double、UUID。

元数据之所以有用,有以下几个原因:

  • 在向大语言模型(LLM)提示中包含Document内容时,也可以包含元数据条目,为LLM提供额外的参考信息。例如,提供Document的名称和来源有助于提升LLM对内容的理解。

  • 在搜索提示中要包含的相关内容时,可以通过元数据条目进行筛选。例如,您可以将语义搜索范围缩小到仅属于特定所有者的文档。

  • 当Document的源文件更新时(例如文档的某个特定页面),可以通过其元数据条目(如"id"、"source"等)轻松定位对应的Document,并在EmbeddingStore中同步更新以保持一致性。

实用方法
  • Metadata.from(Map) 从 Map创建 Metadata
  • Metadata.put(String key, String value) / put(String, int) / 等等,向 Metadata添加一个条目
  • Metadata.putAll(Map) 向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.merge(Metadata) 将当前的 Metadata与另一个 Metadata合并

文档加载器

你可以通过 String创建一个 Document,但更简单的方法是使用库中包含的文档加载器之一:

  • FileSystemDocumentLoader 来自 langchain4j 模块
  • ClassPathDocumentLoader 来自 langchain4j模块
  • UrlDocumentLoader 来自 langchain4j模块
  • AmazonS3DocumentLoader 来自 langchain4j-document-loader-amazon-s3 模块
  • AzureBlobStorageDocumentLoader 来自 langchain4j-document-loader-azure-storage-blob 模块
  • GitHubDocumentLoader 来自 langchain4j-document-loader-github 模块
  • GoogleCloudStorageDocumentLoader 来自 langchain4j-document-loader-google-cloud-storage 模块
  • SeleniumDocumentLoader 来自 langchain4j-document-loader-selenium 模块
  • PlaywrightDocumentLoader 来自 langchain4j-document-loader-playwright 模块
  • TencentCosDocumentLoader 来自 langchain4j-document-loader-tencent-cos 模块

文档解析器

文档可以表示各种格式的文件,例如PDF、DOC、TXT等。
为了解析这些格式中的每一种,库中包含了一个DocumentParser接口及其多个实现:

  • TextDocumentParser 来自 langchain4j 模块, 可以解析纯文本格式的文件(例如TXT、HTML、MD等)
  • ApachePdfBoxDocumentParser 来自 langchain4j-document-parser-apache-pdfbox 模块, 可以解析PDF文件
  • ApachePoiDocumentParser 来自 langchain4j-document-parser-apache-poi 模块, 可以解析MS Office文件格式(如DOC、DOCX、PPT、PPTX、XLS、XLSX等)
  • ApacheTikaDocumentParser 来自 langchain4j-document-parser-apache-tika 模块,可以自动检测并解析几乎所有现有文件格式
  • MarkdownDocumentParser 来自 langchain4j-document-parser-markdown 模块,可以解析Markdown格式的文件
  • YamlDocumentParser 来自 langchain4j-document-parser-yaml 模块,可以解析YAML格式的文件

可以解析YAML格式的文件

// 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中的任意一个,则会使用对应的解析器)。如果通过 SPI 未找到任何 DocumentParser,系统将回退使用 TextDocumentParser作为备选方案。

文档转换器

DocumentTransformer实现可以执行各种文档转换操作,例如:

  • 清理:这包括去除文档文本中不必要的噪音,可以节省标记数量并减少干扰。
  • 过滤:彻底排除特定文档不参与搜索。
  • 丰富:可以向文档添加额外信息,可能提升搜索结果质量。
  • 摘要:可对文档进行摘要处理,并将其简短摘要存储在元数据中,后续可加入每个文本片段(下文将详述)以优化搜索效果。
  • 其他
    在此阶段还可以添加、修改或删除元数据条目。目前,开箱即用的唯一实现是 langchain4j-document-transformer-jsoup模块中的 HtmlToTextDocumentTransformer,它可以从原始 HTML 中提取所需的文本内容和元数据条目。

图变换

GraphTransformer是一个接口,通过提取节点和关系等语义图元素,将非结构化的 Document对象转换为结构化的 GraphDocument。它非常适合将原始文本转化为结构化的语义图。

GraphTransformer将原始文档转换为 GraphDocument,其中包括:

  • 一组节点(GraphNode),表示文本中的实体或概念。
  • 一组关系(GraphEdge),表示这些实体之间的连接方式。
  • 原始Document作为source。
    默认实现是LLMGraphTransformer,它利用语言模型(如OpenAI)通过提示工程从自然语言中提取图结构信息。
主要优势
  • **实体与关系抽取:识别关键概念及其语义关联。
  • **图表示:输出结果可直接集成到知识图谱或图数据库中。
  • **模型驱动的解析:利用大语言模型从非结构化文本中推断结构。
Maven依赖
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-community-llm-graph-transformer</artifactId>
  <version>${latest version here}</version>
</dependency>
示例用法
import dev.langchain4j.data.document.Document;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.community.data.document.graph.GraphDocument;
import dev.langchain4j.community.data.document.graph.GraphNode;
import dev.langchain4j.community.data.document.graph.GraphEdge;
import dev.langchain4j.community.data.document.transformer.graph.GraphTransformer;
import dev.langchain4j.community.data.document.transformer.graph.llm.LLMGraphTransformer;

import java.time.Duration;
import java.util.Set;

public class GraphTransformerExample {
    public static void main(String[] args) {
        // Create a GraphTransformer backed by an LLM
        GraphTransformer transformer = new LLMGraphTransformer(
            OpenAiChatModel.builder()
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .timeout(Duration.ofSeconds(60))
                .build()
        );

        // Input document
        Document document = Document.from("Barack Obama was born in Hawaii and served as the 44th President of the United States.");

        // Transform the document
        GraphDocument graphDocument = transformer.transform(document);

        // Access nodes and relationships
        Set<GraphNode> nodes = graphDocument.nodes();
        Set<GraphEdge> relationships = graphDocument.relationships();

        nodes.forEach(System.out::println);
        relationships.forEach(System.out::println);
    }
}
输出示例
GraphNode(name=Barack Obama, type=Person)
GraphNode(name=Hawaii, type=Location)
GraphEdge(from=Barack Obama, predicate=was born in, to=Hawaii)

GraphEdge(from=Barack Obama, predicate=served as, to=President of the United States)

文本片段

一旦您的Document加载完成,接下来就需要将它们分割(分块)成更小的片段(块)。LangChain4j的领域模型中包含一个TextSegment类,用于表示Document的一个片段。顾名思义,TextSegment只能表示文本信息。

分还是不分?

您可能只想在提示中包含少数相关片段而非整个知识库,原因有以下几点:

  • 大型语言模型的上下文窗口有限,因此可能无法容纳整个知识库
  • 提示中提供的信息越多,大型语言模型处理并响应所需的时间就越长
  • 提示中提供的信息越多,费用就越高
  • 提示中的无关信息可能会分散大型语言模型的注意力,增加产生幻觉(错误输出)的概率
  • 提示中提供的信息越多,就越难解释大型语言模型是基于哪些信息作出回应的
    我们可以通过将知识库拆分为更小、更易消化的部分来解决这些问题。这些部分应该有多大?这是个好问题。一如既往,这取决于具体情况。
    目前有两种广泛使用的方法:
  1. 每个文档(例如PDF文件、网页等)都是不可分割的独立单元。
    在RAG流程的检索阶段,系统会提取N个最相关的文档并将其注入提示词中。这种情况下,您很可能需要使用支持长上下文的LLM模型,因为文档可能非常冗长。当完整检索文档至关重要时(比如不能遗漏任何细节的场景),这种方法尤为适用。
  • 优点:不会丢失上下文。
  • 缺点:
    • 消耗了更多的令牌。
      -有时,文档可能包含多个部分/主题,但并非所有内容都与查询相关。
    • 向量搜索质量下降,是因为不同大小的完整文档被压缩成单一固定长度的向量。
  1. 文档被分割成更小的片段,例如章节、段落,有时甚至是句子。
    在RAG流程的检索阶段,系统会检索出N个最相关的片段并将其注入提示中。挑战在于确保每个片段都能为LLM提供足够的上下文/信息以便理解。
    缺失上下文可能导致LLM误解给定片段并产生幻觉(错误生成)。常见策略是将文档分割成有重叠的片段,但这并不能完全解决问题。这里可以采用几种高级技术,例如"句子窗口检索"、“自动合并检索"和"父文档检索”。我们不会在此详述,但这些方法本质上能帮助获取检索片段周围更多上下文,为LLM提供该片段前后的附加信息。
  • 优点:
    • 向量搜索质量更优。
    • 减少令牌消耗。
  • 缺点:某些上下文信息可能仍然会丢失。
实用方法
  • TextSegment.text() 返回 TextSegment的文本内容
  • TextSegment.metadata() 返回 TextSegment的 Metadata
  • TextSegment.from(String, Metadata) 从文本和Metadata创建一个TextSegment
  • TextSegment.from(String)从文本创建一个带有空Metadata的TextSegment

文档分割器

LangChain4j 提供了一个 DocumentSplitter接口,并内置了多种开箱即用的实现方案:

  • DocumentByParagraphSplitter
  • DocumentByLineSplitter
  • DocumentBySentenceSplitter
  • DocumentByWordSplitter
  • DocumentByCharacterSplitter
  • DocumentByRegexSplitter
  • 递归: DocumentSplitters.recursive(...)

它们的运作方式如下:
实例化一个DocumentSplitter,指定所需的TextSegment大小,并可选择设置字符或标记的重叠量。
2.调用DocumentSplitter的split(Document)splitAll(List<Document>)方法。
3.DocumentSplitter将给定的Document分割成更小的单元,具体形式因分割器而异。例如,DocumentByParagraphSplitter会将文档按段落分割(由两个或更多连续换行符定义),而DocumentBySentenceSplitter则使用OpenNLP库的句子检测器将文档拆分为句子,依此类推。
4.DocumentSplitter随后将这些较小单元(段落、句子、单词等)组合成TextSegment,尽可能在单个TextSegment中包含最多单元而不超过第一步设置的限制。若某些单元仍过大无法放入TextSegment,则会调用子分割器——这是另一个能对不匹配单元进行更细粒度分割的DocumentSplitter。所有Metadata条目会从Document复制到每个TextSegment,并为每个文本段添加唯一的"index"元数据条目:第一个TextSegment包含index=0,第二个为index=1,以此类推。

文本片段转换器

TextSegmentTransformer类似于 DocumentTransformer(如上所述),但它处理的是 TextSegment。与 DocumentTransformer一样,并不存在通用的解决方案,

因此我们建议根据您的独特数据实现自定义的 TextSegmentTransformer。一种对提升检索效果非常有效的技巧是:在每个 TextSegment中包含 Document的标题或简短摘要。

嵌入

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() 对嵌入向量进行归一化(标准操作)

嵌入模型

EmbeddingModel接口代表一种特殊类型的模型,它能将文本转换为 Embedding。目前支持的嵌入模型可以在这里.

实用方法
  • EmbeddingModel.embed(String) 嵌入给定文本
  • EmbeddingModel.embed(TextSegment) 嵌入给定的 TextSegment
  • EmbeddingModel.embedAll(List<TextSegment>) 嵌入所有给定的TextSegment
  • EmbeddingModel.dimension() 返回此模型生成的 Embedding的维度

嵌入存储

EmbeddingStore接口代表一个用于存储 Embedding(也称为向量数据库)的存储库。它允许存储和高效搜索相似(在嵌入空间中接近)的 Embedding。当前支持的嵌入存储库可以在
这里.

EmbeddingStore可以单独存储Embedding,也可以与对应的TextSegment一起存储:

  • 它只能通过ID存储Embedding。原始嵌入数据可以存储在其他地方,并通过ID进行关联。
  • 它可以同时存储Embedding和已被嵌入的原始数据(通常是TextSegment)。
实用方法
  • EmbeddingStore.add(Embedding)将给定的 Embedding添加到存储中并返回一个随机 ID
  • EmbeddingStore.add(String id, Embedding) 将给定的 Embedding与指定 ID 添加到存储中
  • EmbeddingStore.add(Embedding, TextSegment) 将给定的 Embedding与关联的 TextSegment添加到存储中,并返回一个随机 ID
  • EmbeddingStore.addAll(List<Embedding>)将给定的Embedding列表添加到存储中,并返回一个随机ID列表
  • EmbeddingStore.addAll(List<Embedding>, List<TextSegment>) 将给定的Embedding列表及其关联的TextSegment添加到存储中,并返回一个随机ID列表
  • EmbeddingStore.addAll(List<String> ids, List<Embedding>, List<TextSegment>) 将一组给定的带有相关ID和TextSegment的Embedding添加到存储中
  • EmbeddingStore.search(EmbeddingSearchRequest) 寻找最相似的嵌入向量
  • EmbeddingStore.remove(String id) 根据ID从存储中移除单个Embedding
  • EmbeddingStore.removeAll(Collection<String> ids) 移除存储中所有ID存在于给定集合中的Embedding。
  • EmbeddingStore.removeAll(Filter) 移除存储中所有与指定Filter匹配的Embedding
  • EmbeddingStore.removeAll() 从存储中移除所有 Embedding
嵌入搜索请求

EmbeddingSearchRequest表示在 EmbeddingStore中进行搜索的请求。

它具有以下属性:

  • Embedding queryEmbedding:用作参考的嵌入向量。
  • int maxResults:返回的最大结果数量。此为可选参数,默认值为 3。
  • double minScore:最小分数,范围从 0 到 1(含)。只有分数 >= minScore的嵌入向量才会被返回。此为可选参数,默认值为 0。
  • Filter filter:搜索期间应用于 Metadata的过滤器。只有 Metadata匹配 Filter的 TextSegment才会被返回。
过滤器

Filter允许在执行向量搜索时通过 Metadata条目进行筛选。目前支持以下 Filter类型/操作:

  • IsEqualTo
  • IsNotEqualTo
  • IsGreaterThan
  • IsGreaterThanOrEqualTo
  • IsLessThan
  • IsLessThanOrEqualTo
  • IsIn
  • IsNotIn
  • ContainsString
  • And
  • Not
  • Or

:::注意
并非所有嵌入存储都支持通过Metadata进行过滤,请查看“按元数据过滤”一栏
这里.
一些支持通过元数据筛选的商店并不支持所有可能的筛选类型/操作。例如,包含字符串目前仅由Milvus、PgVector和Qdrant支持。
:::

有关“Filter”的更多详情,请参阅 这里.

嵌入搜索结果

EmbeddingSearchResult表示在EmbeddingStore中搜索的结果。它包含EmbeddingMatch的列表。

嵌入匹配

EmbeddingMatch表示一个匹配的 Embedding,包含其相关性得分、ID 和原始嵌入数据(通常是 TextSegment)。

嵌入存储摄取器

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 OpenAiTokenCountEstimator("gpt-4o-mini")))

    // 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

一旦我们的文档被摄取(参见前文部分),我们就可以创建一个 EmbeddingStoreContentRetriever来实现基础的 RAG 功能。

使用AI服务时,可以按如下方式配置基础RAG:

ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
    .embeddingStore(embeddingStore)
    .embeddingModel(embeddingModel)
    .maxResults(5)
    .minScore(0.75)
    .build();

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .contentRetriever(contentRetriever)
    .build();

Naive RAG 样例

Advanced RAG

高级RAG可以通过LangChain4j的以下核心组件实现:- QueryTransformer

  • QueryRouter
  • ContentRetriever
  • ContentAggregator
  • ContentInjector

下图展示了这些组件如何协同工作:
在这里插入图片描述

流程如下:
1.用户生成一条UserMessage,该消息被转换为Query
2.QueryTransformer将Query转换成一个或多个Query
3.每个Query由QueryRouter路由到一个或多个ContentRetriever
4.每个ContentRetriever为每个Query检索相关的Content
5.ContentAggregator将所有检索到的Content合并成一个最终的排序列表
6.这个Content列表被注入到原始的UserMessage
7.最后,包含原始查询和注入的相关内容的UserMessage被发送给LLM
请参阅各组件对应的Javadoc以获取更多详细信息。

检索增强器

RetrievalAugmentor是进入 RAG(检索增强生成)流程的入口点。它负责通过从各种来源检索相关的 Content来增强 ChatMessage。在创建 AI 服务时,可以指定一个 RetrievalAugmentor实例:

Assistant assistant = AiServices.builder(Assistant.class)
    ...
    .retrievalAugmentor(retrievalAugmentor)
    .build();

每次调用AI服务时,都会触发指定的RetrievalAugmentor来增强当前的UserMessage。您可以使用默认的RetrievalAugmentor实现(如下所述),也可以自定义实现。

默认检索增强器

LangChain4j提供了一个开箱即用的RetrievalAugmentor接口实现:DefaultRetrievalAugmentor,它应该适用于大多数RAG(检索增强生成)用例。其设计灵感来源于 文章本文.
建议查阅这些资料以更好地理解这一概念。

查询

Query表示 RAG 流程中的用户查询。它包含查询的文本和查询元数据。

查询元数据

Query中的Metadata包含可能在RAG管道的各个组件中有用的信息,例如:

  • Metadata.userMessage() -应该增强的原始 UserMessage
  • Metadata.chatMemoryId() - @MemoryId注解方法参数的值。更多详情请参阅此处。该值可用于识别用户身份,并在检索过程中应用访问限制或过滤器。
  • Metadata.chatMemory() - @MemoryId注解方法参数的值。更多详情请参阅此处。该值可用于识别用户身份,并在检索过程中应用访问限制或过滤器。
  • Metadata.invocationParameters() - 包含在调用AI服务时可以指定的InvocationParameters:
interface Assistant {
    String chat(@UserMessage String userMessage, InvocationParameters parameters);
}

InvocationParameters parameters = InvocationParameters.from(Map.of("userId", "12345"));
String response = assistant.chat("Hello", parameters);

参数存储在一个可变的、线程安全的 Map中。在 AI 服务的单次调用过程中,数据可以通过 InvocationParameters在 AI 服务组件之间传递(例如,从一个 RAG 组件到另一个 RAG 组件,或从 RAG 组件到工具)。

查询转换器

QueryTransformer将给定的Query转换成一个或多个Query。其目标是通过修改或扩展原始Query来提高检索质量。一些已知的提高检索质量的方法包括:

  • 查询压缩
  • 查询扩展
  • 查询重写
  • 回退提示
  • 假设性文档嵌入(HyDE)

更多详情请见 文章.

默认查询转换器

DefaultQueryTransformer是 DefaultRetrievalAugmentor中使用的默认实现。它不会对 Query进行任何修改,只是原样传递。

压缩查询转换器

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时,大语言模型会读取整个对话内容,并将“Where did he live?”转换为“Where did John Doe live?”。

扩展查询转换器

ExpandingQueryTransformer利用大语言模型(LLM)将给定的Query扩展为多个Query。这种方法非常实用,因为大语言模型能够以多种方式重新表述和重构Query,从而有助于检索到更相关的内容。

内容

“内容”代表与用户“查询”相关的内容。目前仅限于文本内容(即“文本片段”),但未来可能支持其他形式(如图像、音频、视频等)。

内容检索器

ContentRetriever通过给定的 Query从底层数据源检索 Content。

底层数据源几乎可以是任何类型:

  • 嵌入存储
  • 全文搜索引擎
  • 向量与全文搜索的混合体
  • 网络搜索引擎
  • 知识图谱
  • SQL数据库

ContentRetriever返回的 Content列表按相关性从高到低排序。

嵌入存储内容检索器

EmbeddingStoreContentRetriever通过使用 EmbeddingModel嵌入 Query,从 EmbeddingStore中检索相关的 Content。

例如:

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 = query.metadata().invocationParameters().get("userId");
        return metadataKey("userId").isEqualTo(userId);
    })
    .build();

interface Assistant {
    String chat(@UserMessage String userMessage, InvocationParameters parameters);
}

InvocationParameters parameters = InvocationParameters.from(Map.of("userId", "12345"));
String response = assistant.chat("Hello", parameters);
网页搜索内容检索器

WebSearchContentRetriever通过 WebSearchEngine从网络检索相关的 Content。所有支持的 WebSearchEngine集成都可以
这里.

例如:

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();
SQL数据库内容检索器

SqlDatabaseContentRetriever是 ContentRetriever的一个实验性实现,位于 langchain4j-experimental-sql模块中。
它利用 DataSource和大型语言模型(LLM)来生成并执行针对给定自然语言 Query的 SQL 查询。更多信息请参阅 SqlDatabaseContentRetriever的 Javadoc 文档。

Azure AI Search 内容检索器

AzureAiSearchContentRetriever是一个与
Azure AI Search.
它支持全文检索、向量搜索和混合搜索,以及重新排序功能。该组件可在 langchain4j-azure-ai-search模块中找到。更多信息请参阅 AzureAiSearchContentRetriever的 Javadoc 文档。

Neo4j 内容检索器

Neo4jContentRetriever是与 Neo4j 图数据库.
它将自然语言查询转换为Neo4j的Cypher查询语句,并通过在Neo4j中执行这些查询来检索相关信息。该功能可在langchain4j-community-neo4j-retriever模块中找到。

Query 路由器

QueryRouter负责将Query路由到适当的ContentRetriever(s)。

默认查询路由器

DefaultQueryRouter是 DefaultRetrievalAugmentor中使用的默认实现。它将每个 Query路由到所有配置的 ContentRetriever。

语言模型查询路由器

LanguageModelQueryRouter使用大语言模型(LLM)来决定将给定的 Query路由到何处。

内容聚合器

ContentAggregator负责从以下来源聚合多个经过排序的 Content列表:

  • 多个 Query
  • 多个 ContentRetriever
  • 两者兼有
默认内容聚合器

DefaultContentAggregator是ContentAggregator的默认实现,它执行两阶段互惠排名融合(RRF)算法。详情请参阅DefaultContentAggregator Javadoc

内容聚合器的重新排名

ReRankingContentAggregator使用 ScoringModel(如 Cohere)进行重新排序。支持的评分(重新排序)模型的完整列表可在
这里.

内容注入器

ContentInjector负责将 ContentAggregator返回的 Content注入到 UserMessage中。

默认内容注入器

DefaultContentInjector是 ContentInjector的默认实现,它只是简单地将 Content附加到 UserMessage的末尾,并加上前缀 Answer using the following information:。
你可以通过以下三种方式自定义如何将 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()

In this case, TextSegment.text() will be prepended with the "content: " prefix,
and each value from Metadata will be prepended with a key.
The final UserMessage will look like this:

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

并行化

当只有一个Query和一个ContentRetriever时,DefaultRetrievalAugmentor会在同一个线程中执行查询路由和内容检索。否则,将使用Executor来并行处理。

默认情况下,会使用一个修改过的Executors.newCachedThreadPool()(keepAliveTime为1秒而非60秒),但你可以在创建DefaultRetrievalAugmentor时提供一个自定义的Executor实例:

DefaultRetrievalAugmentor.builder()
        ...
        .executor(executor)
        .build;

访问来源

如果你想在使用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<Content>>

interface Assistant {

    TokenStream chat(String userMessage);
}

assistant.chat("How to do Easy RAG with LangChain4j?")
    .onRetrieved((List<Content> sources) -> ...)
    .onPartialResponse(...)
    .onCompleteResponse(...)
    .onError(...)
    .start();

控制聊天记忆中的存储内容

在使用AI服务的RetrievalAugmentor时,您可以控制是将增强后的用户消息(包含注入的检索Content)还是原始用户消息存储在聊天记录中。此行为通过AiServices构建器上的storeRetrievedContentInChatMemory选项进行配置。

配置

  • true(默认值)
    将增强的UserMessage(原始查询加上检索到的内容)存储在聊天记录中。
    同样的增强消息也会发送给大语言模型(LLM)。
  • false
    仅将原始的UserMessage(不包含检索到的内容)存储在聊天记录中。
    在推理过程中,增强后的消息仍会发送给大语言模型(LLM)。
    当您希望保持聊天记录简洁且与用户实际输入一致,同时仍为大语言模型提供检索到的上下文以生成答案时,仅存储原始用户消息会很有帮助。

样例

interface Assistant {

    String chat(String userMessage);
}

ChatModel chatModel = OpenAiChatModel.builder()
    .apiKey(System.getenv("OPENAI_API_KEY"))
    .modelName(GPT_4_O_MINI)
    .build();

MessageWindowChatMemory chatMemory =
    MessageWindowChatMemory.withMaxMessages(10);

RetrievalAugmentor retrievalAugmentor =
    DefaultRetrievalAugmentor.builder()
        .contentRetriever(
            EmbeddingStoreContentRetriever.from(embeddingStore, embeddingModel))
        .build();

Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(chatModel)
    .chatMemory(chatMemory)
    .retrievalAugmentor(retrievalAugmentor)
    // Store only the original user message in chat memory
    .storeRetrievedContentInChatMemory(false)
    .build();
langchain开发群
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐