LangChain4j学习(四)RAG 检索增强 - 向量数据库
前提:使用LangChain4j + SpringBoot + DashScope通义千问
前言:本文主要用于博主个人学习,内容80%来自于官方文档(英文),一切以官方文档为准
系列文章
JavaAI:LangChain4j学习(一) 集成SpringBoot和阿里通义千问DashScope
JavaAI:LangChain4j学习(二)聊天,记忆存储,流式输出
JavaAI:LangChain4j学习(三)AI Service及与SpringBoot结合使用
一、概念
(文本图片来自官方文档)
RAG(Retrieval-Augmented Generation,检索增强生成)是一种在将数据发送到LLM大型语言模型之前,从数据中查找并注入相关信息的方法。这样,LLM将获得(希望获得的)相关信息,并能够利用这些信息进行回复,从而降低产生幻觉(即生成不准确或无关信息)的可能性。
可以使用各种信息检索方法来查找相关信息:
- 全文(关键字)搜索。此方法利用TF-IDF和BM25等技术,通过匹配查询中的关键字来搜索文档数据库(例如,用户正在询问的内容)。它根据每个文档中这些关键字的频率和相关性对结果进行排名。
- 向量搜索,也称为“语义搜索”。文本文档使用嵌入模型转换为数字向量。然后,根据余弦相似性或其他相似性/距离度量(如查询向量和文档向量之间的度量)来查找并排序文档,从而捕捉到更深层次的语义含义。
- 混合搜索。组合多种搜索方法(例如,全文搜索 + 向量搜索)通常会提高搜索效率。
目前,全文搜索和混合搜索仅受Azure AI搜索集成支持,因此本文主要关注向量搜索。
RAG阶段
RAG 过程分两个阶段:索引和检索,LangChain4j 为这两个阶段提供了工具
索引
在索引阶段,对文档进行预处理的方式能够在检索阶段实现高效的搜索。
这一过程可能会因所采用的信息检索方法而有所不同,以向量搜索为例,通常包括清理文档、利用额外数据和元数据对文档进行丰富处理、将文档拆分成更小的段落(即分块)、对这些段落进行嵌入处理,最后将其存储到嵌入存储(即向量数据库)中。
索引阶段通常是在离线状态下进行的,无需最终用户等待其完成。这可以通过定时任务(如cron作业)来实现,例如,该任务可以在周末每周对公司内部文档进行一次重新索引。负责索引的代码也可以作为一个独立的应用程序,仅用于处理索引任务。
然而,在某些情况下,最终用户可能希望上传其自定义文档,以便让LLM能够访问这些文档。在这种情况下,索引操作应该在线执行,并成为主应用程序的一部分。
索引阶段的简化图:
检索
检索阶段通常是在线进行的,会利用已索引的文档来回答用户提交的问题。
这一过程可能会因所采用的信息检索方法而有所不同。以向量搜索为例,这通常涉及将用户的查询(即问题)进行嵌入处理,然后在嵌入存储中执行相似性搜索。之后将相关句段(即原始文档的片段)注入到提示信息中,并发送给LLM进行处理。
检索阶段的简化图:
二、实践
LangChain4j 提供三种风格的 RAG
Easy RAG:使用 RAG 的最简单方法
自定义 RAG:向量搜索的 RAG 的基本实现
高级 RAG:模块化的 RAG 框架,允许执行 查询转换、多个源检索和重新排名 等
(一) Easy RAG 简易
Easy Rag 可以理解为 “ 快速启动 ” ,使用langchain提供的基础模型,执行简单的基础的功能
1. Document 文件/文档/文本
表示各种格式的文件/文档/文本,例如 PDF、DOC、TXT 、网页等。 未来的更新可能会支持图像和表格(目前不支持吧)。
常用方法
Document.text() 返回Document
Document.metadata() 返回 Metadata 部分
Document.toTextSegment() 将 Document 转为 TextSegment
Document.from(String, Metadata) 根据Text文本内容和Metadata元数据创建Document
Document.from(String) 根据Text文本内容和 空的Metadata 创建Document
2. Metadata 元数据
每个Document都存在Metadata元信息,例如Document的名称、来源、上次更新日期、所有者或其他细节。
存储为键值映射(k - v),其中 key 的类型为 值可以是以下类型之一:Metadata ,String , 其他基础数据类型
Metadata作用在于:
在 LLM 的提示中包含Document的内容时, 还可以包含元数据条目,为 LLM 提供需要考虑的其他信息。 例如,提供名称和来源有助于提高 LLM 对内容的理解。Document
搜索要包含在提示中的相关内容时, 可以按条目进行筛选。 例如,您可以将语义搜索范围缩小到仅 s 属于特定所有者。MetadataDocument
当 的源更新时(例如,文档的特定页面), 可以通过元数据条目(例如,“ID”、“Source”等)轻松找到相应的 并在 中更新它以使其保持同步。DocumentDocumentEmbeddingStore
3. DocumentLoader 文档加载器
根据路径加载文档,感觉跟File没什么区别
4. DocumentParser 文档解析器
用于解析Document
TextDocumentParser解析纯文本格式(e.g. TXT、HTML、MD 等)的文件
ApachePdfBoxDocumentParser解析 PDF 文件
ApachePoiDocumentParser解析 MS Office 文件格式 (DOC、DOCX、PPT、PPTX、XLS、XLSX 等)
ApacheTikaDocumentParser自动检测和解析几乎所有现有的文件格式
5. Embedding 嵌入
Embedding 在NLP语言处理中,可以将 “ 文本内容 ” 处理成为 向量 。
详情可以百度
6. Embedding Model 嵌入模型
接口,特殊类型的模型,将文本转换为Embedding
EmbeddingModel.embed(String) 嵌入给定的文本
EmbeddingModel.embed(TextSegment) 嵌入给定的TextSegment
EmbeddingModel.embedAll(List< TextSegment >) 嵌入所有给定的TextSegment
EmbeddingModel.dimension() 返回此模型生成的Embedding的维度
7. Embedding Store 嵌入数据库
EmbeddingStore可以单独存储 Embedding 或TextSegment
只能按 ID 存储Embedding ,原始嵌入数据可以存储在其他位置并使用 ID 进行关联。
它能够同时存储Embedding已嵌入的文本片段(TextSegment)及其原始数据。
* 个人记忆法
Embedding 记作动词 转换/嵌入,在NLP语言处理中,可以将 “ 文本内容 ” 处理成为 向量。
也记作名词 转换得到的产物 (向量) ,在这里 它一定被转换为向量。
Embedding Model 嵌入模型,记作 向量转换器,可以将 文本信息 转换为 Embedding向量
向量转换器将多个文本转为向量
Embedding Store 嵌入数据库,记作 向量数据库,储存向量的临时集合
流程:创建Embedding Model,将文本转为大量Embedding,将Embedding存入Embedding Store
8. Embedding Store Ingestor 嵌入数据库导入器
EmbeddingStoreIngestor负责将 Document 提取到 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));
9. 样例代码
- 导入依赖项:langchain4j-easy-rag
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.0.0-beta2</version>
</dependency>
- 加载文档(指定目录中所有文件或指定文件均可,也可以离线地将文本转换为嵌入内容):
文档加载器 FileSystemDocumentLoader
List<Document> documents = FileSystemDocumentLoader.loadDocuments("/home/langchain4j/documentation");
如果要加载 所有子目录 的文档,可以使用 loadDocumentsRecursively
List<Document> documents = FileSystemDocumentLoader.loadDocumentsRecursively("/home/langchain4j/documentation");
-
文档预处理并存储在向量数据库中,用于用户提出问题时快速找到相关信息
本质上可以使用任何一个embedding store,这里使用的是Easy RAG 的默认嵌入模型bge-small-en-v1.5向量数据库 InMemoryEmbeddingStore
攫取管道 EmbeddingStoreIngestor,用于将 Document 提取到 EmbeddingStore
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
EmbeddingStoreIngestor.ingest(documents, embeddingStore);
- 创建AI Service,用作 LLM 的 API
interface Assistant {
String chat(String userMessage);
}
ChatLanguageModel model = QwenChatModel.builder()
.apiKey("xxxxxxxxxxxxxxxxxxx")
.modelName("qwen-plus")
.build();
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(chatModel)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.contentRetriever(EmbeddingStoreContentRetriever.from(embeddingStore))
.build();
- 聊天
String answer = assistant.chat("How to do Easy RAG with LangChain4j?");
(二) Native Rag 自定义
增强灵活性,可控性,有更多的参数可以设置
10. Text Segment 文本段
Document加载后,就可以将它们拆分成TextSegment(更小的段 / 块)。 TextSegment只能表示文本信息。
11. TextSegmentTransformer 文本段转换器
.textSegmentTransformer(textSegment -> TextSegment.from(
textSegment.metadata("file_name") + "\n" + textSegment.text(),
textSegment.metadata()
))
TextSegmentTransformer 用于对每个文本段进行转换或增强
TextSegment.from 用于创建一个新的 TextSegment 对象
TextSegment 文本段,文档分割后的一部分
通常每个文本段包含 文本内容:实际的文本数据(例如一段文字),元数据:与该文本段相关的附加信息(例如文件名、用户 ID 等)
代码解析:
-
提取文件名:
textSegment.metadata("file_name")
从文本段的元数据中提取键为 " file_name " 的值
假设元数据中存储了该文本段所属文件的名称(例如 “example.txt”) -
提取文本内容:
textSegment.text()
获取该文本段的实际文本内容 -
拼接文件名和文本内容:
textSegment.metadata("file_name") + "\n" + textSegment.text()
将文件名和文本内容拼接起来,中间用换行符 \n 分隔。
例如:
文件名:“example.txt”
文本内容:“This is a sample text segment.”
拼接结果:“example.txt\nThis is a sample text segment.” -
保留原始元数据:
textSegment.metadata()
获取该文本段的所有原始元数据(被修改的部分,这里没有修改) -
创建新的文本段:
TextSegment.from( textSegment.metadata("file_name") + "\n" + textSegment.text(), textSegment.metadata() )
使用 TextSegment.from() 方法,创建一个新的 TextSegment 对象。
新的文本段包含:修改后的文本内容(文件名 + 换行符 + 原始文本内容),原始的元数据(未被修改)。
12. 一份简单的整体案例:
//1.读取文档
DocumentParser documentParser = new TextDocumentParser();
Document document = FileSystemDocumentLoader.loadDocument(RagUtil.toPath("documents/miles-of-smiles-terms-of-use.txt"), documentParser);
//2.分割文档,得到文本数据
DocumentSplitter splitter = DocumentSplitters.recursive(300, 0);
List<TextSegment> segments = splitter.split(document);
//3.创建嵌入模型(必要),向量模型一定要匹配语言模型,否则长度数量无法接驳
EmbeddingModel embeddingModel = new QwenEmbeddingModel(null,RagUtil.API_KEY,QWModelType.QWEN_PLUS.getModelName());
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
//4.创建向量数据库(langchain提供多种向量数据库,根据使用情况定)
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings, segments);
//5.创建内容检索器,主要功能:内容提取、语义匹配、多模态支持、高效检索
//相似性阈值:计算查询向量与检索到的内容片段向量之间的相似性得分(如余弦相似性或欧氏距离),保留相似性得分高于设定阈值的结果。
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(2) // 返回相似性得分最高的前两个内容片段
.minScore(0.5) //相似性阈值,最少相似度0.5
.build();
//6.记录历史对话(保留最近的 10 条消息)
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
//7.根据AI Service创建对话模型
RagAssistant assistant = AiServices.builder(RagAssistant.class)
.chatLanguageModel(CHAT_MODEL)
.contentRetriever(contentRetriever)
.chatMemory(chatMemory)
.build();
//8.聊天
RagUtil.startConversationWith(assistant);
(三) Advanced Rag 高级
看原文档是最好的,但这里也记录一下
13. Retrieval Augmentor 检索增强器
RetrievalAugmentor负责利用 ChatMessage 从各种来源检索到的相关信息来增强 Content
可以理解为一个“能够装载并设置其他增强内容 的 容器”
个人理解:Retrieval Augmentor 卷饼包万物!称之为配置类也不过分,大部分功能 均可以设置在 Retrieval Augmentor 内,一个build()之前能有N行设置。
示例:
在创建AI Service时,RetrievalAugmentor可以指定一个实例(默认 或者 自定义 均可)
QueryRouter queryRouter = new QueryRouter() ....; //后代码省略
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.queryRouter(queryRouter)
.build();
AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.retrievalAugmentor(retrievalAugmentor)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
14. Query Transformer 查询转换器
QueryTransformer将 given 转换为一个Query或多个 Query,通过修改或扩展原始Query来提高检索质量
改进检索的方法包括:查询压缩、查询扩展、查询重写、后退提示、假设文档嵌入 (HyDE)、等
14. 1. CompressingQueryTransformer 压缩查询
能将用户当前的查询及之前的对话内容压缩成一个独立的、完整的查询,用于提升检索过程的质量。
注意:用于压缩的 LLM 和 用于对话的 LLM ,可以不相同
QueryTransformer queryTransformer = new CompressingQueryTransformer(chatLanguageModel);
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(2)
.minScore(0.6)
.build();
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.queryTransformer(queryTransformer)
.contentRetriever(contentRetriever)
.build();
AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.retrievalAugmentor(retrievalAugmentor)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
15. Content Retriever 内容检索器
EmbeddingStoreContentRetriever示例
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore) // 设置嵌入存储(embedding store)
.embeddingModel(embeddingModel) // 设置嵌入模型(embedding model)
.maxResults(2) // 设置最大结果数量为2
// maxResults 也可以根据查询动态指定
.dynamicMaxResults(query -> 2) // 动态设置最大结果数量(此处设为固定返回2)
.minScore(0.75) // 设置最低相似度分数为0.75
// minScore 也可以根据查询动态指定
.dynamicMinScore(query -> 0.75) // 动态设置最低相似度分数(此处为固定返回0.75)
.filter(metadataKey("userId").isEqualTo("12345")) // 添加过滤条件,筛选 metadataKey 为 "userId" 且值为 "12345" 的结果
// filter 也可以根据查询动态指定
.dynamicFilter(query -> { // 动态设置过滤条件
String userId = getUserId(query.metadata().chatMemoryId()); // 从查询的元数据中获取用户ID
return metadataKey("userId").isEqualTo(userId); // 返回过滤条件:metadataKey 为 "userId" 且值等于获取的用户ID
})
.build(); // 构建 ContentRetriever 实例
15. 1. SQL Database Content Retriever 数据库内容检索器
SqlDatabaseContentRetriever可以在langchain4j-experimental-sql Module 中找到(必须引入依赖)
使用 DataSourc 数据源 和 语言模型 来生成SQL 查询语句(Query) 并执行。
DataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:mysql://localhost:3306/xxx?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8");
dataSource.setUser("xxx");
dataSource.setPassword("xxxx");
ContentRetriever contentRetriever = SqlDatabaseContentRetriever.builder()
.dataSource(dataSource)
.chatLanguageModel(chatLanguageModel)
.build();
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.contentRetriever(contentRetriever)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
16. Query Router 路由查询
负责将查询(Query)链接/指向/路由/检索 到合适的 ContentRetriever(内容检索器)
解释:embed 为自定义方法,功能为根据路径和 embeddingModel 构造 EmbeddingStore 。
//向量转换器 (嵌入)
EmbeddingModel embeddingModel = new QwenEmbeddingModel(null,RagUtil.API_KEY,QWModelType.QWEN_PLUS.getModelName());
// 读取文档并生成向量数据库(嵌入数据库) 1号
EmbeddingStore<TextSegment> studentEmbeddingStore = embed(toPath("documents/Question-student.txt"), embeddingModel);
ContentRetriever studentCR = EmbeddingStoreContentRetriever.builder()
.embeddingStore(studentEmbeddingStore)
.embeddingModel(embeddingModel)
.maxResults(2)
.minScore(0.6)
.build();
// 生成向量数据库(嵌入数据库) 2号
EmbeddingStore<TextSegment> teacherEmbeddingStore = embed(toPath("documents/Question-teacher.txt"), embeddingModel);
ContentRetriever teacherCR = EmbeddingStoreContentRetriever.builder()
.embeddingStore(teacherEmbeddingStore)
.embeddingModel(embeddingModel)
.maxResults(2)
.minScore(0.6)
.build();
ChatLanguageModel chatLanguageModel = QwenChatModel.builder()
.apiKey("xxxxxxxxxxxxxxxxxxx")
.modelName("qwen-plus")
.build();
// 创建 QueryRouter
Map<ContentRetriever, String> retrieverToDescription = new HashMap<>();
retrieverToDescription.put(studentCR , "学生问答信息");
retrieverToDescription.put(teacherCR , "教师问答信息");
QueryRouter queryRouter = new LanguageModelQueryRouter(chatLanguageModel, retrieverToDescription);
// 将 QueryRouter 添加到增强器
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.queryRouter(queryRouter)
.build();
AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.retrievalAugmentor(retrievalAugmentor)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
16. 1. Default Query Router 默认路由查询
DefaultRetrievalAugmentor 中默认使用DefaultQueryRouter,将每个Query 路由到所有已配置的ContentRetriever。
16. 2. Language Model Query Router 模型查询路由器
LanguageModelQueryRouter使用 LLM 来决定将给定的Query 。
上方例子中使用的是Language Model Query Router
17. Content Aggregator 内容聚合器 (NO)
ContentAggregator负责从Content聚合多个排名列表,可以是多个Query、多个ContentRetriever、或者都有
理解为整合就行,不同的Query问题发散去不同的文档得到答案,Content Aggregator将这些答案再次整合到一块
没太明白用法…后续学习了可能回来二更。
17. 1. Default Content Aggregator 默认内容聚合器
没明白…等自己回头二更
17. 2. Re-Ranking Content Aggregator 重新排名内容聚合器
没明白…等自己回头二更
18. Content Injector 内容注入器
ContentInjector负责将ContentAggregator整合内容 注入到 UserMessage中
想注入别的也行,总之就是负责 Content 和 UserMessage 的桥梁
18. 1. 默认内容注入器
ContentInjector默认实现DefaultContentInjector,将 Content 添加到 带有前缀(“Answer using the following information:”)的 UserMessage 的末尾。也就是不经过处理,直接将 某内容 加到已经成型的回答的末尾并输出。
这里的Answer using the following information: 仅代表 “以以下信息输出”,具体是中文或者是 “ 我是xx小智接下来为您解答 ” 都可以
三种自定义 Content 注入 UserMessage 方式:
1. 重写默认的 PromptTemplate
注意: PromptTemplate 必须包含 {{userMessage}} 和 {{contents}}
RetrievalAugmentor retrievalAugmentor = DefaultRetrievalAugmentor.builder()
.contentInjector(DefaultContentInjector.builder()
.promptTemplate(PromptTemplate.from("{{userMessage}}\n{{contents}}"))
.build())
.build();
2. 继承DefaultContentInjector并重写一个format方法
3. Implement a custom ContentInjector 实现自定义ContentInjector (自己写一个)
在这种情况下,TextSegment.text() 将预置 “content:” 前缀, 并且 UserMessage 中的每个文本元模型 Metadata 都将带有一个键。 最终结果如下所示:
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
19. Parallelization 并行 (NO)
没明白,期待二更…
(四) 高级RAG 全流程 总结
(图文来自官方文档)
核心组件:
RetrievalAugmentor 卷饼包万物
QueryTransformer查询转换器、QueryRouter路由查询、ContentRetriever内容检索器、ContentAggregator内容聚合器、ContentInjector内容注入器等功能均可以设置在RetrievalAugmentor内
流程:
- 用户生成 UserMessage ,转换为 Query
- 通过 QueryTransformer 转换为一个或多个Query
- 每个 Query 由 QueryRouter 路由指向到不同的 ContentRetriever 内容检索
- ContentRetrieve 执行每个 Query 得到答案 Content
- ContentAggregator 将检索到的所有 Content 整合到一个最终排名列表 中
- Content Injector 将 Content 最终结果列表注入到 UserMessage
- 包含 原始查询 、不同查询结果、默认注入信息等 整体内容的 UserMessage 发送到 LLM 语言模型中