1 Document Loaders 文档读取器
读取为文档,langchain4j官网上有详细讲解:
这里使用直接读取本地的:
Document document = ClassPathDocumentLoader.loadDocument("rag/terms-of-service.txt", new TextDocumentParser());
1.1.Document Parser 文档解析器
如果要开发一个知识库系统, 这些资料可能在各种文件中,比如word、txt、pdf、image、html等等,所以langchain4j也提供了不同的文档解析器:
- TextDocumentParser 来自 langchain4j模块的 TextDocumentParser,它可以解析纯文本格式(e.9.TXT、HTML、MD 等)的文件。
- ApachePdfBoxDocumentParser来自langchain4j-document-parser-apache-pdfbox,它可以解析 PDF 文件
- ApachePoiDocumentParser来langchain4j-document-parser-apache-poi,可以解析 MSOffice 文件格式(e.9.DOC、DOCX、PPT、PPTX、XLS、XLSX等)
- ApacheTikaDocumentParser 来自 langchain4j-document-parser-apache-tika 模块中,可以自动检测和解析几乎所有现有的文件格式
在这里我来解析一份这个txt文件 terms-of-service.txt,所以我们用 TextDocumentParser随便放这里吧
代码如下:
Path documentPath = Paths.get(VectorTest.class.getClassLoader().getResource("rag/terms-of-service.txt").toURI());
DocumentParser documentParser =new TextDocumentParser();
Document document = FileSystemDocumentLoader.loadDocument(documentPath, documentParser);
System.out.println(document.text());
2 DocumentSplitter 文档拆分器
由于文本读取过来后, 还需要分成一段一段的片段(分块chunk),分块是为了更好地拆分语义单元,这样在后面可以更精确地进行语义相似性检索,也可以避免LLM的Token限制。langchain4j也提供了不同的文档拆分器:
分词器类型 |
匹配能力 | 适用场景 |
DocumentByCharacterSplitter |
无符号分割 |
就是严格根据字数分隔(不推荐,会出现断句) |
DocumentByRegexSplitter |
正则表达式分隔 |
根据自定义正则分隔 |
DocumentByParagraphSplitter | 删除大段空白内容 |
处理连续换行符(如段落分隔)(\\s*(?>\\R)\\s*(?>\\R)\ls* |
DocumentByLineSplitter |
删除单个换行符周围的空白,替换一个换行 |
(|\s*|\R\\s*) 示例: 输入文本:"This is line one.\nltThis is linetwo." 使用s*\R\s*替换为单个换行符:"Thisisline one.\nThis is line two.” |
DocumentByWordSplitter |
删除连续的空白字符。 |
\\s+ 示例: 输入文本:"Hello World 使用\s+替换为单个空格:"Hello World' |
DocumentBySentenceSplitter | 按句子分割 |
Apache OpenNLP 库中的一个类,用于检测文本中的句子边界。它能够识别标点符号(如句号、问号、感叹号等)是否标记着句子的末尾,从而将一个较长的文本字符串分割成多个句子。 |
这里我们选DocumentByLineSplitter吧,因为内容不多,所以其实没有特别大的关系,后面如果大家有兴趣我详细讲解每一种的应用场景。
代码:
首先将读取到的文档进行分割
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
90, // 每段最长字数
10 // 自然语言最大重叠字数
);
List<TextSegment> segments = splitter.split(document);
结果如下:
[TextSegment { text = "本服务条款适用于您对图灵航空公司的体验。预订航班,即表示您同意这些条款。
1. 预订航班
- 通过我们的网站或移动应用程序预订。
- 预订时需要全额付款。
- 确保个人信息(姓名、" metadata = {file_name=terms-of-service.txt, index=0, url=/H:/project/langchain4jSpringbootpro/langchain4jSpringbootpro/target/test-classes/rag/terms-of-service.txt} }, TextSegment { text = "ID 等)的准确性,因为更正可能会产生 25 的费用。
2. 更改预订
- 允许在航班起飞前 24 小时更改。
- 通过在线更改或联系我们的支持人员。
- 改签费:经济舱 50,豪" metadata = {file_name=terms-of-service.txt, index=1, url=/H:/project/langchain4jSpringbootpro/langchain4jSpringbootpro/target/test-classes/rag/terms-of-service.txt} }, TextSegment { text = "华经济舱 30,商务舱免费。
3. 取消预订
- 最晚在航班起飞前 48 小时取消。
- 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
- 退款将在" metadata = {file_name=terms-of-service.txt, index=2, url=/H:/project/langchain4jSpringbootpro/langchain4jSpringbootpro/target/test-classes/rag/terms-of-service.txt} }, TextSegment { text = "7 个工作日内处理。" metadata = {file_name=terms-of-service.txt, index=3, url=/H:/project/langchain4jSpringbootpro/langchain4jSpringbootpro/target/test-classes/rag/terms-of-service.txt} }]
进程已结束,退出代码为 0
chunk size(块大小)指的就是我们分割的字符块的大小;chunk overlap(块间重叠大小)就是下图中加深的部分,上一个字符块和下一个字符块重叠的部分,即上一个字符块的未尾是下一个字符块的开始。
在使用按字符切分时,需要指定分割符,另外需要指定块的大小以及块之间重叠的大小(允许重叠是为了尽可能地避免按照字符进行分割造成的语义损失)。
比如:
-最晚在航班起飞前 48 小时取消。取消费用:经济舱75 美元,豪华经济舱 50 美元,商务舱 25 美元。退款将在 7 个工作日内处理。
按照chunksize可能会分隔成:
-最晚在航班起飞前 48 小时取消。取消费用:经济舱 7如果设置了重叠可能会:最晚在航班起飞前 48 小时取消。取消费用:经济舱 75 美元,豪华经济舱 58 美元,商务舱 25 美元。
-取消费用:经济舱 75 美元,豪华经济舱58 美元,商务舱 25 美元。退款将在 7 个工作日内处理。
整个流程如下:
首先按照指定的分割符进行切分,切分过之后,如果块的长度小于chunk size 的大小,则进行块之间的合并。在进行合并时,遵循下面的规则:
(1)如果相邻块加在一起的长度小于或等于chunk size,则进行合并;否则看你有没有子分割器,如果没有报错。
(2)在进行合并时,如果块的大小小于或等于chunk overlap,并且和前后两个相邻块合并后,两个合并后的块均不超过chunk size,则两个合并后的块允许有重叠在RAG系统中,文本分块的粒度需要平衡语义完整性与计算效率,并非越细越好。以下是关键考量点:
分隔经验
(1)过细分块的潜在问题
- 语义割裂: 破坏上下文连贯性,影响模型理解。
- 计算成本增加:分块过细会导致向量嵌入和检索次数增多,增加时间和算力开销。
- 信息冗余与干扰:碎片化的文本块可能引入无关内容,干扰检索结果的质量,降低生成答案的准确性
(2)分块过大的弊端
- 信息丢失风险:过大的文本块可能超出嵌入模型的输入限制,导致关键信息未被有效编码。
- 检索精度下降:大块内容可能包含多主题混合,与用户查询的相关性降低,影响模型反馈效果。
场景 | 分块策略 | 参数参考 |
微博/短文本 | 句子级分块,保留完整语义 | 每块100-200字符 |
学术论文 | 段落级分块,叠加10%重叠 | 每块300-500字符 |
法律合同 | 条款级分块,严格按条款分隔 | 每块200-400字符 |
长篇小说 | 章节级分块,过长段落递归拆分为段落 | 每块500-1000字符 |
(1)固定长度分块
字符数范围:通常建议每块控制在100-500字符(约20-100词),以平衡上下文完整性与检索效率。
重叠比例:相邻块间保留 10-20%的重叠内容(如块长500字符时重叠50-100字符),减少语义断层。
(2)语义分块
段落或章节:优先按自然段落、章节标题划分,保持逻辑单元完整。
动态调整:对于长段落,可递归分割为更小单元(如先按段落分块,过长时再按句子拆分)。
(3)专业领域调整
高信息密度文本(如科研论文、法律文件):采用更细粒度分块(100-200字符),保留专业术语细节。
通用文本(如新闻、社交媒体):适当放宽分块大小(300-500字符)
3 文本向量化
向量化存储之前在“文本向量化”介绍了,就是通过向量模型库进行向量化
代码:
依然通过Qwen向量模型进行向量化:将之前分割的chunk进行向量化
QwenEmbeddingModel embeddingModel= QwenEmbeddingModel.builder()
.apiKey(System.getenv("ALI_AI_KEY"))
.build();
// 向量化
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
System.out.println(embeddings);
4 存储向量
选择向量数据库进行存储即可
代码:
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings,segments);
5 向量数据索引
代码:
需要先将文本进行向量化,然后去向量数据库查询
// 生成向量
Response<Embedding> embed = embeddingModel.embed("退费费用");
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder()
.queryEmbedding(embed.content())
.maxResults(1)
.build();
// 查询
EmbeddingSearchResult<TextSegment> results = embeddingStore.search(build);
for (EmbeddingMatch<TextSegment> match : results.matches()) {
System.out.println(match.embedded().text() + ",分数为:" + match.score());
}
将以上代码片段整合如下:
@Test
public void test01() throws URISyntaxException {
Document document = ClassPathDocumentLoader.loadDocument("rag/terms-of-service.txt", new TextDocumentParser());
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
90, // 每段最长字数
10 // 自然语言最大重叠字数
);
List<TextSegment> segments = splitter.split(document);
System.out.println(segments);
QwenEmbeddingModel embeddingModel= QwenEmbeddingModel.builder()
.apiKey("你的apikey")
.build();
// 向量化
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings,segments);
// 生成向量
Response<Embedding> embed = embeddingModel.embed("退费费用");
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder()
.queryEmbedding(embed.content())
.maxResults(1)
.build();
// 查询
EmbeddingSearchResult<TextSegment> results = embeddingStore.search(build);
for (EmbeddingMatch<TextSegment> match : results.matches()) {
System.out.println(match.embedded().text());
System.out.println("分数为:" + match.score());
System.out.println("-------------------------------");
}
}
6 对话阶段(索引增强)
代码如下:
ChatLanguageModel model = QwenChatModel
.builder()
.apiKey("你的apikey")
.modelName("qwen-max")
.build();
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(1) // 最相似的5个结果
.minScore(0.7) // 只找相似度在0.6以上的内容
.build();
TestAI testAI = AiServices.builder(TestAI.class)
.chatLanguageModel(model)
.contentRetriever(contentRetriever)
.build();
System.out.println(testAI.chat("退费费用"));
注意:需要添加接口
public interface TestAI {
String chat(String message);
}
完整代码如下:
package org.example;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentParser;
import dev.langchain4j.data.document.loader.ClassPathDocumentLoader;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.parser.TextDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentByCharacterSplitter;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import org.junit.jupiter.api.Test;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
public class ELPTest {
@Test
public void test01() throws URISyntaxException {
Document document = ClassPathDocumentLoader.loadDocument("rag/terms-of-service.txt", new TextDocumentParser());
DocumentByCharacterSplitter splitter = new DocumentByCharacterSplitter(
90, // 每段最长字数
10 // 自然语言最大重叠字数
);
List<TextSegment> segments = splitter.split(document);
QwenEmbeddingModel embeddingModel= QwenEmbeddingModel.builder()
.apiKey("sk-4d1748fba8994a2e94cb0fbaf3d34f23")
.build();
// 向量化
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings,segments);
// 生成向量
Response<Embedding> embed = embeddingModel.embed("退费费用");
EmbeddingSearchRequest build = EmbeddingSearchRequest.builder()
.queryEmbedding(embed.content())
.maxResults(1)
.build();
/*---------------------检索增强阶段---------------------------*/
ChatLanguageModel model = QwenChatModel
.builder()
.apiKey("sk-4d1748fba8994a2e94cb0fbaf3d34f23")
.modelName("qwen-max")
.build();
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(embeddingModel)
.maxResults(1) // 最相似的5个结果
.minScore(0.7) // 只找相似度在0.6以上的内容
.build();
TestAI testAI = AiServices.builder(TestAI.class)
.chatLanguageModel(model)
.contentRetriever(contentRetriever)
.build();
System.out.println(testAI.chat("退费费用"));
}
public interface TestAI {
String chat(String message);
}
}
运行结果如下:
根据您提供的信息,关于退费费用的规定如下:
- 如果是取消预订的情况:
- 经济舱的取消费用为75美元。
- 豪华经济舱的取消费用为50美元。
- 商务舱的取消费用则较低,仅为25美元。
- 需要注意的是,最晚应在航班起飞前48小时进行取消操作。
- 关于退款的时间点,“退款将在...”这部分信息似乎没有完整给出。通常情况下,航空公司会在处理完您的退款请求后的一定时间内(比如几周内)将款项退还至原支付渠道,请联系相关客服或查看具体条款获取更准确的信息。
另外提到“华经济舱 30”,这个表述可能是指某个特定条件下(例如提前多少天退票等)经济舱的退票手续费为30单位货币(可能是人民币或其他),但基于给定的信息并不完全清楚这一规则的具体应用场景,建议参照航空公司的官方说明或直接咨询客服以获得最准确的答案。