
LLM 的知识仅限于其训练数据。如希望使 LLM 了解特定领域的知识或专有数据,可:
RAG 是一种在将提示词发送给 LLM 之前,从你的数据中找到并注入相关信息的方式。这样,LLM 希望能获得相关的信息并利用这些信息作出回应,从而减少幻觉概率。
可通过各种信息检索方法找到相关信息。这些方法包括但不限于:
本文主要关注向量搜索。全文搜索和混合搜索目前仅通过 Azure AI Search 集成支持,详情参见 AzureAiSearchContentRetriever。计划在不久的将来扩展 RAG 工具箱,以包含全文搜索和混合搜索。
RAG 过程分为两个不同阶段:索引和检索。LangChain4j 提供用于两个阶段的工具。
文档会进行预处理,以便在检索阶段实现高效搜索。
该过程可能因使用的信息检索方法而有所不同。对向量搜索,通常包括清理文档,利用附加数据和元数据对其进行增强,将其拆分为较小的片段(即“分块”),对这些片段进行嵌入,最后将它们存储在嵌入存储库(即向量数据库)。
通常在离线完成,即用户无需等待该过程的完成。可通过例如每周末运行一次的定时任务来重新索引公司内部文档。负责索引的代码也可以是一个仅处理索引任务的单独应用程序。
但某些场景,用户可能希望上传自定义文档以供 LLM 访问。此时,索引应在线进行,并成为主应用程序的一部分。

通常在线进行,当用户提交一个问题时,系统会使用已索引的文档来回答问题。
该过程可能会因所用的信息检索方法不同而有所变化。对于向量搜索,通常包括嵌入用户的查询(问题),并在嵌入存储库中执行相似度搜索。然后,将相关片段(原始文档的部分内容)注入提示词并发送给 LLM。


LangChain4j 提供了“简单 RAG”功能,使你尽可能轻松使用 RAG。无需学习嵌入技术、选择向量存储、寻找合适的嵌入模型、了解如何解析和拆分文档等操作。只需指向你的文档,LangChain4j 就会自动处理!
若需定制化RAG,请跳到第五节 RAG API。
当然,这种“简单 RAG”的质量会比定制化 RAG 设置的质量低一些。然而,这是学习 RAG 或制作概念验证的最简单方法。稍后,您可以轻松地从简单 RAG 过渡到更高级的 RAG,逐步调整和自定义各个方面。
langchain4j-easy-rag 依赖<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>0.34.0</version>
</dependency>List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j/documentation");这将加载指定目录下的所有文件。
Apache Tika 库被用于检测文档类型并解析它们。由于我们没有显式指定使用哪个 DocumentParser,因此 FileSystemDocumentLoader 将加载 ApacheTikaDocumentParser,该解析器由 langchain4j-easy-rag 依赖通过 SPI 提供。
若想加载所有子目录中的文档,可用 loadDocumentsRecursively :
List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively("/home/langchain4j/documentation");还可通过使用 glob 或正则表达式过滤文档:
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:*.pdf");
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j/documentation", pathMatcher);使用
loadDocumentsRecursively时,可能要在 glob 中使用双星号(而不是单星号):glob:**.pdf。
并将文档存储在专门的嵌入存储中也称向量数据库。这是为了在用户提出问题时快速找到相关信息片段。可用 15+ 种支持的嵌入存储,但为简化操作,使用内存存储:
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 作为简单 RAG 的默认嵌入模型。该模型在 MTEB 排行榜 上取得了不错的成绩,其量化版本仅占用 24 MB 空间。因此,我们可以轻松将其加载到内存中,并在同一进程中通过 ONNX Runtime 运行。
可在完全离线的情况下,在同一个 JVM 进程中将文本转换为嵌入。LangChain4j 提供 5 种流行的嵌入模型开箱即用。
TextSegment 和 Embedding 对被存储在 EmbeddingStore 中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 中检索相关内容。
String answer = assistant.chat("如何使用 LangChain4j 实现简单 RAG?");如希望访问增强消息的检索源,可将返回类型包装在 Result 类中:
interface Assistant {
Result<String> chat(String userMessage);
}
Result<String> result = assistant.chat("如何使用 LangChain4j 实现简单 RAG?");
String answer = result.content();
List<Content> sources = result.sources();流式传输时,可用 onRetrieved() 指定一个 Consumer<List<Content>>:
interface Assistant {
TokenStream chat(String userMessage);
}
assistant.chat("如何使用 LangChain4j 实现简单 RAG?")
.onRetrieved(sources -> ...)
.onNext(token -> ...)
.onError(error -> ...)
.start();LangChain4j 提供丰富的 API 让你可轻松构建从简单到高级的自定义 RAG 流水线。本节介绍主要的领域类和 API。
Document 类表示整个文档,例如单个 PDF 文件或网页。当前,Document 只能表示文本信息,但未来的更新将支持图像和表格。
package dev.langchain4j.data.document;
/**
* 表示通常对应于单个文件内容的非结构化文本。此文本可能来自各种来源,如文本文件、PDF、DOCX 或网页 (HTML)。
* 每个文档都可能具有关联的元数据,包括其来源、所有者、创建日期等
*/
public class Document {
/**
* Common metadata key for the name of the file from which the document was loaded.
*/
public static final String FILE_NAME = "file_name";
/**
* Common metadata key for the absolute path of the directory from which the document was loaded.
*/
public static final String ABSOLUTE_DIRECTORY_PATH = "absolute_directory_path";
/**
* Common metadata key for the URL from which the document was loaded.
*/
public static final String URL = "url";
private final String text;
private final Metadata metadata;Document.text() 返回 Document 的文本内容Document.metadata() 返回 Document 的元数据(见下文)Document.toTextSegment() 将 Document 转换为 TextSegment(见下文)Document.from(String, Metadata) 从文本和 Metadata 创建一个 DocumentDocument.from(String) 从文本创建一个带空 Metadata 的 Document每个 Document 都包含 Metadata,用于存储文档的元信息,如名称、来源、最后更新时间、所有者或任何其他相关细节。
Metadata 以KV对形式存储,其中键是 String 类型,值可为 String、Integer、Long、Float、Double 中的任意一种。
Metadata.from(Map) 从 Map 创建 MetadataMetadata.put(String key, String value) / put(String, int) / 等方法添加元数据条目Metadata.getString(String key) / getInteger(String key) / 等方法返回元数据条目的值,并转换为所需类型Metadata.containsKey(String key) 检查元数据中是否包含指定键的条目Metadata.remove(String key) 从元数据中删除指定键的条目Metadata.copy() 返回元数据的副本Metadata.toMap() 将元数据转换为 Map</details>可从 String 创建一个 Document,但更简单的是使用库中包含的文档加载器之一:
FileSystemDocumentLoader 来自 langchain4j 模块UrlDocumentLoader 来自 langchain4j 模块AmazonS3DocumentLoader 来自 langchain4j-document-loader-amazon-s3 模块AzureBlobStorageDocumentLoader 来自 langchain4j-document-loader-azure-storage-blob 模块GitHubDocumentLoader 来自 langchain4j-document-loader-github 模块TencentCosDocumentLoader 来自 langchain4j-document-loader-tencent-cos 模块TextSegmentTransformer 类似于 DocumentTransformer(如上所述),但它用于转换 TextSegment。
与 DocumentTransformer 类似,没有统一的解决方案,建议根据您的数据自定义实现 TextSegmentTransformer。
提高检索效果的有效方法是将 Document 的标题或简短摘要包含在每个 TextSegment 。
Embedding 类封装了一个数值向量,表示嵌入内容(通常是文本,如 TextSegment)的“语义意义”。
阅读更多关于向量嵌入的内容:
Embedding.dimension() 返回嵌入向量的维度(即长度)CosineSimilarity.between(Embedding, Embedding) 计算两个 Embedding 之间的余弦相似度Embedding.normalize() 对嵌入向量进行归一化(就地操作)EmbeddingModel 接口代表一种特殊类型的模型,将文本转换为 Embedding。
当前支持的嵌入模型可以在这里找到。
EmbeddingModel.embed(String) 嵌入给定的文本EmbeddingModel.embed(TextSegment) 嵌入给定的 TextSegmentEmbeddingModel.embedAll(List<TextSegment>) 嵌入所有给定的 TextSegmentEmbeddingModel.dimension() 返回该模型生成的 Embedding 的维度EmbeddingStore 接口表示嵌入存储,也称为向量数据库。它用于存储和高效搜索相似的(在嵌入空间中接近的)Embedding。
当前支持的嵌入存储可以在这里找到。
EmbeddingStore 可以单独存储 Embedding,也可以与相应的 TextSegment 一起存储:
Embedding,嵌入的数据可以存储在其他地方,并通过 ID 关联。Embedding 和被嵌入的原始数据(通常是 TextSegment)。EmbeddingStore.add(Embedding) 将给定的 Embedding 添加到存储中并返回随机 IDEmbeddingStore.add(String id, Embedding) 将给定的 Embedding 以指定 ID 添加到存储中EmbeddingStore.add(Embedding, TextSegment) 将给定的 Embedding 和关联的 TextSegment 添加到存储中,并返回随机 IDEmbeddingStore.addAll(List<Embedding>) 将一组 Embedding 添加到存储中,并返回一组随机 IDEmbeddingStore.addAll(List<Embedding>, List<TextSegment>) 将一组 Embedding 和关联的 TextSegment 添加到存储中,并返回一组随机 IDEmbeddingStore.search(EmbeddingSearchRequest) 搜索最相似的 EmbeddingEmbeddingStore.remove(String id) 按 ID 从存储中删除单个 EmbeddingEmbeddingStore.removeAll(Collection<String> ids) 按 ID 从存储中删除多个 EmbeddingEmbeddingStore.removeAll(Filter) 删除存储中与指定 Filter 匹配的所有 EmbeddingEmbeddingStore.removeAll() 删除存储中的所有 EmbeddingEmbeddingSearchRequest 表示在 EmbeddingStore 中的搜索请求。其属性如下:
Embedding queryEmbedding: 用作参考的嵌入。int maxResults: 返回的最大结果数。这是一个可选参数,默认为 3。double minScore: 最低分数,范围为 0 到 1(含)。仅返回得分 >= minScore 的嵌入。这是一个可选参数,默认为 0。Filter filter: 搜索时应用于 Metadata 的过滤器。仅返回 Metadata 符合 Filter 的 TextSegment。关于 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);
ingestor.ingest(List.of(document4, document5, document6));可选地,EmbeddingStoreIngestor 可以使用指定的 DocumentTransformer 来转换 Document。这在您希望在嵌入之前对文档进行清理、增强或格式化时非常有用。
可选地,EmbeddingStoreIngestor 可以使用指定的 DocumentSplitter 将 Document 拆分为 TextSegment。这在文档较大且您希望将其拆分为较小的 TextSegment 时非常有用,以提高相似度搜索的质量并减少发送给 LLM 的提示词的大小和成本。
可选地,EmbeddingStoreIngestor 可以使用指定的 TextSegmentTransformer 来转换 TextSegment。这在您希望在嵌入之前对 TextSegment 进行清理、增强或格式化时非常有用。
示例:
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// 为每个 Document 添加 userId 元数据条目,便于后续过滤
.documentTransformer(document -> {
document.metadata().put("userId", "12345");
return document;
})
// 将每个 Document 拆分为 1000 个 token 的 TextSegment,具有 200 个 token 的重叠
.documentSplitter(DocumentSplitters.recursive(1000, 200, new OpenAiTokenizer()))
// 为每个 TextSegment 添加 Document 的名称,以提高搜索质量
.textSegmentTransformer(textSegment -> TextSegment.from(
textSegment.metadata("file_name") + "\n" + textSegment.text(),
textSegment.metadata()
))
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.build();请阅读this:

进入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() - 需要增强的原始 UserMessageMetadata.chatMemoryId() - 带有 @MemoryId 的方法参数的值。可用于标识用户,并在检索时应用访问限制或过滤器Metadata.chatMemory() - 所有之前的 ChatMessage。有助理解提出 Query 时的上下文QueryTransformer 将给定的 Query 转换为一个或多个 Query。目的是通过修改或扩展原始查询来提升检索质量。
一些已知的改进检索的方法:
更多细节参见这里。
DefaultQueryTransformer 是 DefaultRetrievalAugmentor 中使用的默认实现,它不对 Query 进行任何修改,只是直接传递它。
CompressingQueryTransformer 使用LLM来压缩给定的 Query 和之前的对话,使之成为一个独立的 Query。这在用户可能提出参考之前问题的后续问题时非常有用。
如:
用户:告诉我关于 John Doe 的信息
AI:John Doe 是一个……
用户:他住在哪里?仅靠 “他住在哪里?” 这个查询无法检索到所需信息,因为没有明确说明 “他” 是谁,导致上下文不清晰。
使用 CompressingQueryTransformer 时,LLM 会读取整个对话,将 “他住在哪里?” 转换为 “John Doe 住在哪里?”。
ExpandingQueryTransformer 使用LLM将给定的 Query 扩展为多个 Query。这很有用,因为 LLM 可以用不同的方式重写和重新表述查询,从而帮助检索到更多相关内容。
代表与用户 Query 相关的内容。目前,它仅限于文本内容(即 TextSegment),将来可能支持其他模态(如图片、音频、视频等)。
ContentRetriever 使用给定的 Query 从底层数据源中检索 Content。底层数据源可以是几乎任何东西:
EmbeddingStoreContentRetriever 使用 EmbeddingModel 来嵌入查询,从 EmbeddingStore 检索相关的 Content。
示例:
EmbeddingStore embeddingStore = ...
EmbeddingModel embeddingModel = ...
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(3)
// maxResults 也可以根据查询动态指定
.dynamicMaxResults(query -> 3)
.minScore(0.75)
// minScore 也可以根据查询动态指定
.dynamicMinScore(query -> 0.75)
.filter(metadataKey("userId").isEqualTo("12345"))
// filter 也可以根据查询动态指定
.dynamicFilter(query -> {
String userId = getUserId(query.metadata().chatMemoryId());
return metadataKey("userId").isEqualTo(userId);
})
.build();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();完整示例这里。
SqlDatabaseContentRetriever 是 ContentRetriever 的实验性实现,位于 langchain4j-experimental-sql 模块中。
它使用 DataSource 和LLM为给定的自然语言 Query 生成并执行 SQL 查询。
有关更多信息,请参阅 SqlDatabaseContentRetriever 的 Javadoc。
示例。
AzureAiSearchContentRetriever 可以在 langchain4j-azure-ai-search 模块中找到。
Neo4jContentRetriever 可以在 langchain4j-neo4j 模块中找到。
QueryRouter 负责将 Query 路由到适当的 ContentRetriever。
DefaultQueryRouter 是 DefaultRetrievalAugmentor 中使用的默认实现。它将每个 Query 路由到所有配置的 ContentRetriever。
LanguageModelQueryRouter 使用大语言模型(LLM)来决定将给定的 Query 路由到哪里。
更多细节即将推出。
DefaultContentAggregator
更多细节即将推出。
ReRankingContentAggregator
/**
* 将给定Content注入指定UserMessage中。
* 目的是将Content格式化并整合到原始的UserMessage中,
* 使LLM能利用这些内容生成基于实际内容的响应。
*/
@Experimental
public interface ContentInjector {
/**
* 将给定Content注入指定ChatMessage
* 此方法包含一个默认实现,暂时支持当前自定义的 {@code ContentInjector} 实现。
* 该默认实现将很快被移除。
*
* @param contents 要注入的 {@link Content} 列表。
* @param chatMessage 要注入内容的 {@link ChatMessage},可以是 {@link UserMessage} 或 {@link SystemMessage}。
* @return 注入了 {@link Content} 的 {@link UserMessage}。
*/
default ChatMessage inject(List<Content> contents, ChatMessage chatMessage) {
if (!(chatMessage instanceof UserMessage)) {
throw runtime("请实现 'ChatMessage inject(List<Content>, ChatMessage)' 方法," +
"以便将内容注入到 " + chatMessage);
}
return inject(contents, (UserMessage) chatMessage);
}
/**
* 将给定的 {@link Content} 注入到指定的 {@link UserMessage} 中。
*
* @param contents 要注入的 {@link Content} 列表。
* @param userMessage 要注入内容的 {@link UserMessage}。
* @return 注入了 {@link Content} 的 {@link UserMessage}。
* @deprecated 请使用/实现 {@link #inject(List, ChatMessage)} 代替。
*/
@Deprecated
UserMessage inject(List<Content> contents, UserMessage userMessage);
}DefaultContentInjector
默认实现,旨在适用于大多数使用场景。注意,虽然会尽量避免对现有行为进行破坏性变更,但若发现当前行为不能充分满足大多数使用场景需求,未来可能更新。此类更改旨在为当前和未来的用户提供更多益处。
该实现会按迭代顺序将所有给定的 Content 附加到给定 UserMessage 的末尾。更多细节请参考 DEFAULT_PROMPT_TEMPLATE 和具体实现。
可配置参数(可选):
promptTemplate: 定义如何将原始 UserMessage 与 Content 组合为最终 UserMessage 的提示模板。metadataKeysToInclude: 应包含在每个 Content 中的 Metadata 键列表。public class DefaultContentInjector implements ContentInjector {
public static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = PromptTemplate.from(
"{{userMessage}}\n" +
"\n" +
"Answer using the following information:\n" +
"{{contents}}"
);
private final PromptTemplate promptTemplate;
private final List<String> metadataKeysToInclude;当只有一个 Query和一个 ContentRetriever 时,DefaultRetrievalAugmentor 在同一线程中执行查询路由和内容检索。否则,使用 Executor 进行并行化处理。默认情况下,使用修改后的(keepAliveTime 为 1 秒而不是 60秒)Executors.newCachedThreadPool(),但你也可以在创建 DefaultRetrievalAugmentor 时提供自定义的 Executor 实例:
DefaultRetrievalAugmentor.builder()
...
.executor(executor)
.build;原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。