LangChain4j之检索生成增强(RAG)
如何让大模型回答专业领域知识
LLM的知识仅限于它所训练的数据,如果你想让LLM了解特定领域的知识或专有数据,你可以:
- 使用RAG;
- 使用你的数据微调LLM;
- 结合RAG和微调LLM。
大模型微调
在现有大模型的基础上,使用小规模的特定任务数据进行再次训练,调整模型参数,让模型更精确地处理特定领域或任务的数据。更新需重新训练,计算资源和时间成本高。
- 优点:一次会话只需一次模型调用,速度快,在特定任务上性能更高,准确性也更高。
- 缺点:知识更新不及时,模型训成本高、训练周期长。
- 应用场景:适合知识库稳定、对生成内容准确性和风格要求高的场景,如对上下文理解和语言生成质量要求高的文学创作、专业文档生成等。
RAG介绍
(1)RAG其实是三个单词的缩写,分别是Retrieval-Augmented-Generation,即检索增强生成。
(2)在将原始问题以及提示词信息发送给大语言模型之前,先通过外部知识库检索相关信息,然后将检索结果和原始问题一起发送给大模型,大模型依据外部知识库再结合自身的训练数据,组织自然语言回答问题。通过这种方式,大语言模型可以获取到特定领域的相关信息,并能够利用这些信息进行回复。
- 优点:数据存储在外部知识库,可以实时更新,不依赖对模型自身的训练,成本更低。
- 缺点:需要两次查询:先查询知识库,然后再查询大模型,性能不如微调大模型。
- 应用场景:适用于知识库规模大且频繁更新的场景,如企业客服、实时新闻查询、法律和医疗领域的最新知识问答等。
RAG常用方法
RAG有三种常用方法,分别是全文搜索、向量搜索和混合搜索。
(1)全文搜索,也被称为“关键词搜索”。通过将问题和提示词中的关键词,与知识库文档数据库进行匹配,来搜索文档,然后根据这些关键词在每个文档中的出现频率和相关性对搜索结果进行排序。
(2)向量搜索,也被称为 “语义搜索”。文本通过“嵌入模型”被转换为“数字向量”,然后它根据查询向量与文档向量之间的余弦相似度或其他相似性/距离度量,来查找和排序文档,从而捕捉更深层次的语义含义。
(3)混合搜索。结合多种搜索方法(如全文搜索 + 向量搜索)通常可以提高搜索的效果。
向量搜索(Vector Search)
向量(Vector)
可以将向量理解为从空间中的一个点到另一个点的移动。举个例子,在下图中,我们可以看到一些二维空间中的向量。a是一个从 (100, 50) 到 (-50, -50) 的向量,b是一个从 (0, 0) 到 (100, -50) 的向量。

很多时候,我们处理的向量是从原点 (0, 0) 开始的,比如b。这样我们可以省略向量起点部分,直接说b是向量 (100, -50)。那么问题来了,如何将向量的概念扩展到非数值实体上呢(如文本)?
维度(Dimensions)
(1)正如我们所见,每个数值向量都有x和y坐标(或者在多维系统中是 x、y、z,…),其中x、y、z… 是这个向量空间的轴,称为维度。
(2)对于我们想要表示为向量的一些非数值实体,我们首先需要决定这些维度,并为每个实体在每个维度上分配一个值。
举个例子,在一个交通工具数据集中,我们可以定义四个维度:“轮子数量”、“是否有发动机”、“是否可以在地上开动”和“最大乘客数”,然后我们可以将一些车辆表示为:

这样汽车Car向量将是 (4, yes, yes, 5),或者用数值表示为 (4, 1, 1, 5)(将 yes 设为 1,no 设为 0)。
(3)向量的每个维度代表数据的不同特性,维度越多对事务的描述越精确,我们可以使用“是否有翅膀”、“是否使用柴油”、“最高速度”、“平均重量”、“价格”等等更多的维度信息。
相似度(Similarity)
如果用户搜索“轿车Car”,你希望能够返回所有与“汽车automobile”和“车辆vehicle”等信息相关的结果,那么可以使用向量搜索。
如何确定哪些是最相似的?我们知道每个向量都有一个长度和方向。在下图中,p 和 a 指向相同的方向,但长度不同。p 和 b 正好指向相反的方向,但有相同的长度。然后还有c,长度比p短一点,方向不完全相同,但很接近。那么问题来了,哪一个最接近 p 呢?

如果“相似”仅仅意味着指向相似的方向,那么a是最接近p的。接下来是 c。b是最不相似的,因为它正好指向与p相反的方向。如果“相似”仅仅意味着相似的长度,那么 b 是最接近 p 的(因为它有相同的长度),接下来是 c,然后是 a。
由于向量通常用于描述语义意义,仅仅看长度通常无法满足需求。大多数相似度测量要么仅依赖于方向,要么同时考虑方向和大小。
相似度测量(Measures of similarity)
相似度测量即相似度计算,四种常见的向量相似度计算方法如下:
- 欧几里得距离 Euclidean distance;
- 曼哈顿距离 Manhattan distance;
- 点积 Dot product;
- 余弦相似度 Cosine similarity。
RAG的过程
RAG过程分为两个不同的阶段,分别是索引阶段和检索阶段。
索引阶段
在索引阶段,对知识库文档进行预处理,可实现检索阶段的高效搜索。以下是索引阶段的简化图:

索引阶段的流程如下:
(1)加载知识库文档;
(2)将文档中的文本分段;
(3)利用向量大模型将分段后的文本转换成向量;
(4) 将向量存入向量数据库。
什么要进行文本分段?因为大语言模型(LLM)的上下文窗口有限,所以整个知识库可能无法全部容纳其中:
- 你在提问中提供的信息越多,大语言模型处理并做出回应所需的时间就越长;
- 你在提问中提供的信息越多,花费也就越多;
- 提问中的无关信息可能会干扰大语言模型,增加产生幻觉(生成错误信息)的几率
针对上述问题,我们可以通过将知识库分割成更小、更易于理解的片段来进行解决。
检索阶段
以下是检索阶段的简化图:

检索阶段的流程如下:
(1)通过向量模型将用户查询转换成向量;
(2)在向量数据库中根据用户查询进行相似度匹配;
(3)将用户查询和向量数据库中匹配到的相关内容一起交给LLM处理.
文档加载器(Document Loader)
常见的文档加载器
点击 这里,阅读langchain4j对于文档加载器的说明文档:

(1)langchain4j 模块的类路径文档加载器(ClassPathDocumentLoader);
(2)langchain4j 模块的网址文档加载器(UrlDocumentLoader);
(3)langchain4j-document-loader-amazon-s3 模块的亚马逊 S3 文档加载器(AmazonS3DocumentLoader);
(4)langchain4j-document-loader-azure-storage-blob 模块的 Azure Blob 存储文档加载器(AzureBlobStorageDocumentLoader);
(5)langchain4j-document-loader-github 模块的 GitHub 文档加载器(GitHubDocumentLoader);
(6)langchain4j-document-loader-google-cloud-storage 模块的谷歌云存储文档加载器(GoogleCloudStorageDocumentLoader);
(7)langchain4j-document-loader-selenium 模块的 Selenium 文档加载器(SeleniumDocumentLoader);
(8)langchain4j-document-loader-tencent-cos 模块的腾讯云对象存储文档加载器(TencentCosDocumentLoader)。
测试文档加载
这里我们以langchain4j模块的类路径文档加载器,即ClassPathDocumentLoader为例进行学习。
在controller包内定义一个名为FileController的类,里面的代码如下所示:
1 | @RestController |
然后创建E:/Files/langchain4j/files目录,在里面创建四个文件,分别是:

之后启动项目,开始测试。由于这里使用的是TextDocumentParser,即文本文档解析器,因此只能解析text格式的文件,所以后面三个接口返回的都是2。
访问如下接口:
1 | http://localhost:8080/file/readOne |
页面显示结果如下:

访问后面三个接口:
1 | http://localhost:8080/file/reaAll |
页面均返回如下,符合预期:

文档解析器(Document Parser)
常见的文档解析器
点击 这里,阅读langchain4j对于文档解析器的说明文档:

文档可以是各种格式的文件,如PDF、DOC、TXT 等,为了解析这些不同格式的文件,langchain4j提供了一个 “文档解析器”(DocumentParser)接口,并且我们的库中包含了该接口的几种实现方式:
(1)来自 langchain4j 模块的文本文档解析器(TextDocumentParser):
- 用途:解析纯文本文件,如
.txt、.md、.html等; - 特点:轻量级,适用于结构简单的文本内容。
(2)来自 langchain4j-document-parser-apache-pdfbox 模块的 Apache PDFBox 文档解析器(ApachePdfBoxDocumentParser):
- 用途:解析 PDF 文件;
- 特点:能够提取 PDF 中的文本和元数据。
(3)来自 langchain4j-document-parser-apache-poi 模块的 Apache POI 文档解析器(ApachePoiDocumentParser):
- 用途:解析 Microsoft Office 文件,如
.doc、.docx、.xls、.xlsx、.ppt、.pptx等。 - 特点:支持提取 Office 文档中的文本内容。
(4)来自 langchain4j-document-parser-apache-tika 模块的Apache Tika文档解析器(ApacheTikaDocumentParser):
- 用途:通用解析器,支持多种文件格式。
- 特点:能够自动检测文件类型并解析,适用于处理多种格式的文档。
添加PDF解析依赖
现在我们希望能解析PDF文档,那么默认的TextDocumentParser类就无法做到了,此时可以使用langchain4j提供的langchain4j-document-parser-apache-pdfbox模块来实现。
点击 这里,获取上述模块的依赖信息:

在pom文件中新增如下依赖,修改为适合你自己的版本:
1 | <!-- langchain4j文档解析器 --> |
测试文档解析
这里我们以 langchain4j-document-parser-apache-pdfbox 模块的 Apache PDFBox 文档解析器(ApachePdfBoxDocumentParser),为例进行学习。
回到FileController的类,在里面定义一个名为parsePdf的方法,里面的代码如下所示:
1 | @RestController |
之后启动项目,开始测试。访问如下接口:
1 | http://localhost:8080/file/parsePdf |
页面显示结果如下:

说明pdf文档已经被成功解析了。
文档分割器(Document Splitter)
常见的文档分割器
langchain4j提供了一个 “文档分隔器”(DocumentSplitter)接口,并且提供了几种开箱即用的实现方式:
(1)DocumentByParagraphSplitter:
- 功能:将文档按段落进行分割,段落通常由两个或更多连续的换行符定义。
- 特点:适用于结构清晰、段落分明的文档,如新闻文章、博客等。
(2)DocumentBySentenceSplitter:
- 功能:基于句子进行分割,通常依赖于句子检测器(如 OpenNLP)来识别句子边界。
- 特点:适用于需要精细语义控制的场景,如问答系统、摘要生成等。
- 注意:需要引入相应的句子检测库作为依赖。
(3)RecursiveCharacterTextSplitte:
- 功能:递归地按字符进行分割,优先在自然的分隔符(如段落、句子、空格)处进行分割,以保持语义完整性。
- 特点:推荐的默认分割器,适用于大多数通用文本。
(4)CharacterTextSplitter:
- 功能:按固定的字符数进行分割,适用于结构简单、语义不太复杂的文本。
- 特点:实现简单,但可能会打断语义完整性。
(5)TokenTextSplitter:
- 功能:基于标记(Token)进行分割,适用于需要控制模型输入长度的场景。
- 特点:有助于防止超过语言模型的上下文窗口限制。
(6)MarkdownHeaderTextSplitter:
- 功能:基于 Markdown 文档的标题结构进行分割,保留标题元数据。
- 特点:适用于结构化的 Markdown 文档,便于上下文感知的处理。
除此之外,还有其他如按行文档分割器(DocumentByLineSplitter)、按单词文档分割器(DocumentByWordSplitter)、按字符文档分割器(DocumentByCharacterSplitter)、按正则表达式文档分割器(DocumentByRegexSplitter)。默认情况下每个文本片段最多不能超过300个token。
测试文档分隔
这里我们以DocumentByCharacterSplitter为例,即按字符文档分割进行学习。
回到FileController的类,在里面定义一个名为testSplitter的方法,里面的代码如下所示:
1 | @RestController |
这里的DocumentByCharacterSplitter对象,有两个参数:
(1)maxSegmentSizeInChars:最大分割长度(chunk size),即每个分割后的文档片段最多包含 20 个字符;
(2)maxOverlapSizeInChars:重叠长度(chunk overlap),即相邻两个片段之间重叠的字符数为 10,用于保持上下文连贯性。
然后修改E:/Files/langchain4j/files/langchain4j.txt文件中的内容为如下:
1 | 欢迎学习Langchain4j!Langchain4j是Langchain的Java版本! |
之后启动项目,开始测试。访问如下接口:
1 | http://localhost:8080/file/testSplitter |
页面显示结果如下:

向量生成和向量存储
(1)Embedding (Vector) Stores,即“嵌入(向量)存储” 。
(2)在机器学习和自然语言处理领域,Embedding指的是将数据(如文本、图像等)转换为低维稠密向量表示的过程,这些向量能够保留数据的关键特征。
(3)Stores表示存储,即用于存储这些嵌入向量的系统或工具。它们可以高效地存储和检索向量数据,支持向量相似性搜索,在文本检索、推荐系统、图像识别等任务中发挥着重要作用。
点击 这里,查看Langchain4j支持的向量存储,如下所示:

这里我们先使用langchain4j自带的RAG简单实现,后面会学习embedding模型及向量数据库的选型。在pom文件中新增如下依赖:
1 | <!--简单的rag实现--> |
langchain4j-easy-rag是LangChain4j提供的一个模块,该模块封装了文档解析、分割、嵌入生成和向量存储等复杂流程,使开发者能够更快速地搭建RAG系统。
测试向量生成及存储
第一步,在controller包中定义一个名为EmbeddingController的类,在里面定义一个名为readAndStore的方法,里面的代码如下所示:
1 | @Slf4j |
解释一下上述代码的含义:
(1)FileSystemDocumentLoader.loadDocument(filePath),表示使用FileSystemDocumentLoader读取指定目录下的知识库文档,并使用默认的文档解析器对文档进行解析(TextDocumentParser);
(2)InMemoryEmbeddingStore<TextSegment>是LangChain4j提供的一个轻量级、基于内存的向量存储实现;
(3)<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">EmbeddingStoreIngestor.ingest(document, embeddingStore);</font> 方法执行了以下操作:
- 文档分割:默认使用递归分割器(
RecursiveCharacterTextSplitter),将文档分割为多个文本片段(TextSegment)。每个片段的最大长度为 300 个 token,且相邻片段之间有 30 个 token 的重叠,以保持语义连贯性。请注意,当段落长度总和小于设定的最大长度时,就没有重叠的必要。 - 嵌入生成:使用内置的轻量级嵌入模型(如
BgeSmallEnV15QuantizedEmbeddingModel:一个量化的英文嵌入模型,具有较小的向量维度,适合快速处理。)将每个文本片段转换为向量表示。 - 向量存储:将原始文本和生成的向量存储到内存中的向量存储(
InMemoryEmbeddingStore)中。
第二步,启动项目,开始测试。访问如下接口:
1 | http://localhost:8080/file/readAndStore |
页面显示结果如下:

测试文档分隔及存储
在EmbeddingController类中定义一个名为splitterAndStore的方法,里面的代码如下所示:
1 | @Slf4j |
解释一下上述代码的含义:
(1)FileSystemDocumentLoader.loadDocument(filePath),表示使用FileSystemDocumentLoader读取指定目录下的知识库文档,并使用默认的文档解析器对文档进行解析(TextDocumentParser);
(2)InMemoryEmbeddingStore<TextSegment>是LangChain4j提供的一个轻量级、基于内存的向量存储实现;
(3)new DocumentByLineSplitter(300, 30),表示按照行进行分隔,每个片段包含不超过 300个token,并且有 30个token的重叠部分保证连贯性。请注意,当段落长度总和小于设定的最大长度时,就没有重叠的必要。
(4)<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">EmbeddingStoreIngestor.ingest(document, embeddingStore);</font> 方法执行了以下操作:
- 文档分割:默认使用递归分割器(
RecursiveCharacterTextSplitter),将文档分割为多个文本片段(TextSegment)。每个片段的最大长度为 300 个 token,且相邻片段之间有 30 个 token 的重叠,以保持语义连贯性。 - 嵌入生成:使用内置的轻量级嵌入模型(如
BgeSmallEnV15QuantizedEmbeddingModel:一个量化的英文嵌入模型,具有较小的向量维度,适合快速处理。)将每个文本片段转换为向量表示。 - 向量存储:将原始文本和生成的向量存储到内存中的向量存储(
InMemoryEmbeddingStore)中。
第二步,启动项目,开始测试。访问如下接口:
1 | http://localhost:8080/file/splitterAndStore |
页面显示结果如下:

token和token计算
点击 这里,查看DeepSeek的Token用量计算:

点击 这里,查看阿里百炼的计费规则。接下来我们在EmbeddingController类中定义一个名为tokenCount的方法,里面的代码如下所示:
1 | @GetMapping("/tokenCount") |
然后启动项目,开始测试。访问如下接口:
1 | http://localhost:8080/file/tokenCount |
页面显示结果如下:

可以看到上述文本的token长度为20。
工作方式
(1)实例化一个“文档分割器”(DocumentSplitter),指定所需的“文本片段”(TextSegment)大小,并且可以选择指定characters或token的重叠部分。
(2)“文档分割器”(DocumentSplitter)将给定的文档(Document)分割成更小的单元,这些单元的性质因分割器而异。举个例子,“按段落分割文档器”(DocumentByParagraphSplitter)将文档分割成段落(由两个或更多连续的换行符定义),而 “按句子分割文档器”(DocumentBySentenceSplitter)使用OpenNLP库的句子检测器将文档分割成句子,依此类推。
(3)然后“文档分割器”(DocumentSplitter)将这些较小的单元(段落、句子、单词等)组合成“文本片段”(TextSegment),尝试在单个“文本片段”(TextSegment)中包含尽可能多的单元,同时不超过第一步中设置的限制。如果某些单元仍然太大,无法放入一个 “文本片段”(TextSegment)中,它会调用一个子分割器。这是另一个 “文档分割器”(DocumentSplitter),能够将不适合的单元分割成更细粒度的单元。会向每个文本片段添加一个唯一的元数据条目 “index”。第一个 “文本片段”(TextSegment)将包含 index=0,第二个是 index=1,依此类推。
期望的文本片段大小
- 模型上下文窗口:如果你使用的大语言模型(LLM)有特定的上下文窗口限制,这个值不能超过模型能够处理的最大 token 数。例如,某些模型可能最大只能处理 2048 个 token,那么设置的文本片段大小就需要远小于这个值,为后续的处理(如添加指令、其他输入等)留出空间。通常,在这种情况下,你可以设置为 1000 - 1500 左右,具体根据实际情况调整。模型上下文窗口,可以通过模型参数列表查看。
- 数据特点:如果你的文档内容较为复杂,每个段落包含的信息较多,那么可以适当提高这个值,比如设置为 500 - 800 个 token,以便在一个文本片段中包含相对完整的信息块。相反,如果文档段落较短且信息相对独立,设置为 200 - 400 个 token 可能就足够了。
- 检索需求:如果希望在检索时能够更精确地匹配到相关信息,较小的文本片段可能更合适,这样可以提高信息的粒度。例如设置为 200 - 300 个 token。但如果更注重获取完整的上下文信息,较大的文本片段(如 500 - 600 个 token)可能更有助于理解相关内容。
重叠部分大小
- 上下文连贯性:重叠部分的主要作用是提供上下文连贯性,避免因分割导致信息缺失。如果文档内容之间的逻辑联系紧密,建议设置较大的重叠部分,如 50 - 100 个 token,以确保相邻文本片段之间的过渡自然,模型在处理时能够更好地理解上下文。
- 数据冗余:然而,设置过大的重叠部分会增加数据的冗余度,可能导致处理时间增加和资源浪费。因此,需要在上下文连贯性和数据冗余之间进行平衡。一般来说,20 - 50 个 token 的重叠是比较常见的取值范围。
- 模型处理能力:如果使用的模型对输入的敏感性较高,较小的重叠部分(如 20 - 30 个 token)可能就足够了,因为过多的重叠可能会引入不必要的干扰信息。但如果模型对上下文依赖较大,适当增加重叠部分(如 40 - 60 个 token)可能会提高模型的性能。
通常来说,在处理一般性的文本资料,且使用的模型上下文窗口较大(如 4096 个 token)时,设置文本片段最大大小为600 - 800个token,重叠部分为30 - 50个token是一个不错的选择。但最终的设置还需要通过实验和实际效果评估来确定,以找到最适合具体应用场景的参数值。
