LangChain4j(6):LangChain4j实现知识库RAG演练

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单位货币(可能是人民币或其他),但基于给定的信息并不完全清楚这一规则的具体应用场景,建议参照航空公司的官方说明或直接咨询客服以获得最准确的答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不死鸟.亚历山大.狼崽子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值