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
创建一个 Document
Document.from(String)
从文本创建一个带空 Metadata
的 Document
每个 Document
都包含 Metadata
,用于存储文档的元信息,如名称、来源、最后更新时间、所有者或任何其他相关细节。
Metadata
以KV对形式存储,其中键是 String
类型,值可为 String
、Integer
、Long
、Float
、Double
中的任意一种。
Metadata.from(Map)
从 Map
创建 Metadata
Metadata.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)
嵌入给定的 TextSegment
EmbeddingModel.embedAll(List<TextSegment>)
嵌入所有给定的 TextSegment
EmbeddingModel.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)
搜索最相似的 Embedding
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
的更多细节可以在这里找到。
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()
- 需要增强的原始 UserMessage
Metadata.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 删除。