(六)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (文档分割 Splitting)

本文主要是介绍(六)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (文档分割 Splitting),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在上一章中,我们刚刚讨论了如何将文档加载到标准格式中,现在我们要谈论如何将它们分割成较小的块。这听起来可能很简单,但其中有很多微妙之处会对后续工作产生重要影响。

文章目录

  • 1、为什么要做文档分割?
  • 2、文档分割方式
  • 3、基于字符分割:RecursiveCharacterTextSplitter 与 CharacterTextSplitter
    • 3.1、短句分割
    • 3.1、长句分割
  • 4、基于Token分割
  • 5、分割Markdown文档
    • 5.1、分割一个自定义 Markdown 文档
    • 5.2、分割数据库中的 Markdown 文档
  • Reference

1、为什么要做文档分割?

优点:

  • 1、模型大小和内存限制。
  • 2、计算效率。
  • 3、序列长度限制。
  • 4、更好的泛化:通过在多个文档块上进行训练,模型可以更好地学习和泛化到各种不同的文本样式和结构。
  • 5、数据增强:分割文档可以为训练数据提供更多的样本。例如,一个长文档可以被分割成多个部分,并分别作为单独的训练样本。

缺点:

  • 可能导致一些上下文信息的丢失,尤其是在分割点附近。因此,如何进行文档分割是一个需要权衡的问题。

因此,为了确保语义的准确性,我们应该尽量将文本分割为包含完整语义的段落或单元。

2、文档分割方式

Langchain 中文本分割器都根据 chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)进行分割:

  • chunk_size 指每个块包含的字符或 Token (如单词、句子等)的数量
  • chunk_overlap 指两个块之间共享的字符数量,用于保持上下文的连贯性,避免分割丢失上下文信息
    在这里插入图片描述

Langchain提供了很多文本切割的工具,区别在怎么确定块与块之间的边界、块由哪些字符/token组成、以及如何测量块大小。其中langchain默认使用RecursiveCharacterTextSplitter:

  • 1、CharacterTextSplitter():按字符来分割文本。
  • 2、MarkdownHeaderTextSplitter():基于指定的标题来分割markdown 文件。
  • 3、TokenTextSplitter():按token来分割文本。
  • 4、SentenceTransformersTokenTextSplitter() : 按token来分割文本
  • 5、RecursiveCharacterTextSplitter():按字符串分割文本,递归地尝试按不同的分隔符进行分割文本。
  • 6、Language() - 用于 CPP、Python、Ruby、Markdown 等。
  • 7、NLTKTextSplitter():使用 NLTK(自然语言工具包)按句子分割文本。
  • 8、SpacyTextSplitter() - 使用 Spacy按句子的切割文本。

3、基于字符分割:RecursiveCharacterTextSplitter 与 CharacterTextSplitter

如何进行文本分割,往往与我们的任务类型息息相关。当我们拆分代码时,这种相关性变得尤为突出。因此,我们引入了一个语言文本分割器,其中包含各种为 Python、Ruby、C 等不同编程语言设计的分隔符。在对这些文档进行分割时,必须充分考虑各种编程语言之间的差异。

我们将从基于字符的分割开始探索,借助 LangChain 提供的 RecursiveCharacterTextSplitter 和 CharacterTextSplitter 工具来实现此目标。

CharacterTextSplitter 是字符文本分割,分隔符的参数是单个的字符串;RecursiveCharacterTextSplitter 是递归字符文本分割,将按不同的字符递归地分割(按照这个优先级[“\n\n”, “\n”, " ", “”]),这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置。因此,RecursiveCharacterTextSplitter 比 CharacterTextSplitter 对文档切割得更加碎片化

RecursiveCharacterTextSplitter 需要关注的是如下4个参数:

  • separators - 分隔符字符串数组
  • chunk_size - 每个文档的字符数量限制
  • chunk_overlap - 两份文档重叠区域的长度
  • length_function - 长度计算函数

⭐从以下尝试可以看出,这就是递归字符文本分割器名字中“递归”的含义,总的来说,我们更建议在通用文本中使用递归字符文本分割器。

3.1、短句分割

# 导入文本分割器
from langchain.text_splitter import RecursiveCharacterTextSplitter, CharacterTextSplitterchunk_size = 20 #设置块大小
chunk_overlap = 10 #设置块重叠大小# 初始化递归字符文本分割器
r_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size,chunk_overlap=chunk_overlap
)
# 初始化字符文本分割器
c_splitter = CharacterTextSplitter(chunk_size=chunk_size,chunk_overlap=chunk_overlap
)text = "在AI的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好训练时间成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。"  #测试文本
# 递归字符分割器
r_splitter.split_text(text)
# 可以看到,分割结果中,第二块是从“大模型规模非常大,模”开始的,刚好是我们设定的块重叠大小['在AI的研究中,由于大模型规模非常大,模','大模型规模非常大,模型参数很多,在大模型','型参数很多,在大模型上跑完来验证参数好不','上跑完来验证参数好不好训练时间成本很高,','好训练时间成本很高,所以一般会在小模型上','所以一般会在小模型上做消融实验来验证哪些','做消融实验来验证哪些改进是有效的再去大模','改进是有效的再去大模型上做实验。']# 字符文本分割器
c_splitter.split_text(text)
# 可以看到字符分割器没有分割这个文本,因为字符文本分割器默认以换行符为分隔符,因此需要设置“,”为分隔符。['在AI的研究中,由于大模型规模非常大,模型参数很多,在大模型上跑完来验证参数好不好训练时间成本很高,所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']

设置空格分隔符。可以看到出现了提示"Created a chunk of size 23, which is longer than the specified 20",意思是“创建了一个长度为23的块,这比指定的20要长。”。


# 是因为CharacterTextSplitter优先使用我们自定义的分隔符进行分割,所以在长度上会有较小的差距
c_splitter = CharacterTextSplitter(chunk_size=chunk_size,chunk_overlap=chunk_overlap,separator=','
)
c_splitter.split_text(text)Created a chunk of size 23, which is longer than the specified 20
['在AI的研究中,由于大模型规模非常大','由于大模型规模非常大,模型参数很多','在大模型上跑完来验证参数好不好训练时间成本很高','所以一般会在小模型上做消融实验来验证哪些改进是有效的再去大模型上做实验。']

3.1、长句分割

some_text = """在编写文档时,作者将使用文档结构对内容进行分组。 \这可以向读者传达哪些想法是相关的。 例如,密切相关的想法\是在句子中。 类似的想法在段落中。 段落构成文档。 \n\n\段落通常用一个或两个回车符分隔。 \回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 \句子末尾有一个句号,但也有一个空格。\并且单词之间用空格分隔"""print(len(some_text)) # 177
# CharacterTextSplitter默认的字分割符是双换行符即\n\n
c_splitter = CharacterTextSplitter(chunk_size=80,chunk_overlap=0,separator=' '
)
c_splitter.split_text(some_text)
['在编写文档时,作者将使用文档结构对内容进行分组。 这可以向读者传达哪些想法是相关的。 例如,密切相关的想法 是在句子中。 类似的想法在段落中。 段落构成文档。','段落通常用一个或两个回车符分隔。 回车符是您在该字符串中看到的嵌入的“反斜杠 n”。 句子末尾有一个句号,但也有一个空格。 并且单词之间用空格分隔']

对于递归字符分割器,依次传入分隔符列表,分别是双换行符、单换行符、空格、空字符,
因此在分割文本时,首先会采用双换行符进行分割,同时依次使用其他分隔符进行分割(谁放列表前,谁优先级就大)。
意思就是先通过\n\n分割,然后在分割出来的每一段里继续用 [“\n”, " ", “”]分割,但每次分割要尽可能满足chunk_size和chunk_overlap

'''
# 默认字分割符是一个列表即["\n\n", "\n", " ", ""]
r_splitter = RecursiveCharacterTextSplitter(chunk_size=80,chunk_overlap=0,separators=["\n\n", "\n", " ", ""]
)
r_splitter.split_text(some_text)['在编写文档时,作者将使用文档结构对内容进行分组。     这可以向读者传达哪些想法是相关的。 例如,密切相关的想法    是在句子中。 类似的想法在段落中。','段落构成文档。','段落通常用一个或两个回车符分隔。     回车符是您在该字符串中看到的嵌入的“反斜杠 n”。     句子末尾有一个句号,但也有一个空格。','并且单词之间用空格分隔']

如果需要按照句子进行分隔,则还要用正则表达式添加一个句号分隔符


r_splitter = RecursiveCharacterTextSplitter(chunk_size=30,chunk_overlap=0,separators=["\n\n", "\n", "(?<=\。 )", " ", ""]
)
r_splitter.split_text(some_text)
['在编写文档时,作者将使用文档结构对内容进行分组。','这可以向读者传达哪些想法是相关的。 例如,密切相关的想法','是在句子中。 类似的想法在段落中。 段落构成文档。','段落通常用一个或两个回车符分隔。','回车符是您在该字符串中看到的嵌入的“反斜杠 n”。','句子末尾有一个句号,但也有一个空格。','并且单词之间用空格分隔']

4、基于Token分割

LLM 的上下文窗口长度限制一般是按照 Token 来计数的。因此,以 LLM 的视角,按照 Token 对文本进行分隔,通常可以得到更好的结果。 通过一个实例理解基于字符分割和基于 Token 分割的区别

# 使用token分割器进行分割,
# 将块大小设为1,块重叠大小设为0,相当于将任意字符串分割成了单个Token组成的列
from langchain.text_splitter import TokenTextSplitter
text_splitter = TokenTextSplitter(chunk_size=1, chunk_overlap=0)
text = "foo bar bazzyfoo"
text_splitter.split_text(text)
# 可以看出token长度和字符长度不一样,token通常为4个字符
# 注:目前 LangChain 基于 Token 的分割器还不支持中文
['foo', ' bar', ' b', 'az', 'zy', 'foo']

5、分割Markdown文档

5.1、分割一个自定义 Markdown 文档

分块的目的是把具有上下文的文本放在一起,我们可以通过使用指定分隔符来进行分隔,但有些类型的文档(例如 Markdown )本身就具有可用于分割的结构(如标题)。

Markdown 标题文本分割器会根据标题或子标题来分割一个 Markdown 文档,并将标题作为元数据添加到每个块中。

# 定义一个Markdown文档markdown_document = """# Title\n\n \
## 第一章\n\n \
李白乘舟将欲行\n\n 忽然岸上踏歌声\n\n \
### Section \n\n \
桃花潭水深千尺 \n\n 
## 第二章\n\n \
不及汪伦送我情"""print(markdown_document)
# Title## 第一章李白乘舟将欲行忽然岸上踏歌声### Section 桃花潭水深千尺 ## 第二章不及汪伦送我情
from langchain.text_splitter import MarkdownHeaderTextSplitter#markdown分割器# 定义想要分割的标题列表和名称
headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),("###", "Header 3"),
]markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on
)#message_typemd_header_splits = markdown_splitter.split_text(markdown_document)
print(len(md_header_splits),md_header_splits)print(md_header_splits[0])
print(md_header_splits[1])
print(md_header_splits[2])
3 
[Document(page_content='李白乘舟将欲行  \n忽然岸上踏歌声', metadata={'Header 1': 'Title', 'Header 2': '第一章'}), Document(page_content='桃花潭水深千尺', metadata={'Header 1': 'Title', 'Header 2': '第一章', 'Header 3': 'Section'}), Document(page_content='不及汪伦送我情', metadata={'Header 1': 'Title', 'Header 2': '第二章'})]
page_content='李白乘舟将欲行  \n忽然岸上踏歌声' metadata={'Header 1': 'Title', 'Header 2': '第一章'}
page_content='桃花潭水深千尺' metadata={'Header 1': 'Title', 'Header 2': '第一章', 'Header 3': 'Section'}
page_content='不及汪伦送我情' metadata={'Header 1': 'Title', 'Header 2': '第二章'}

5.2、分割数据库中的 Markdown 文档

在上一章中,我们尝试了 Notion 数据库的加载,Notion 文档就是一个 Markdown 文档。我们在此处加载 Notion 数据库中的文档并进行分割。

from langchain.document_loaders import NotionDirectoryLoader#Notion加载器
loader = NotionDirectoryLoader("./data/Notion_DB")
docs = loader.load()
txt = ' '.join([d.page_content for d in docs])# 如果Notion_DB下有多个md文件,那就拼一起headers_to_split_on = [("#", "Header 1"),("##", "Header 2"),
]#加载文档分割器
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on
)md_header_splits = markdown_splitter.split_text(txt)#分割文本内容print(len(md_header_splits), md_header_splits[0])#分割结果
8 
page_content="This is a living document with everything we've learned working with people while running a startup. And, of course, we continue to learn. ...."metadata={'Header 1': "Blendle's Employee Handbook (1)"}

Reference

  • [1] 吴恩达老师的教程
  • [2] DataWhale组织

这篇关于(六)、基于 LangChain 实现大模型应用程序开发 | 基于知识库的个性化问答 (文档分割 Splitting)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/395218

相关文章

5分钟获取deepseek api并搭建简易问答应用

《5分钟获取deepseekapi并搭建简易问答应用》本文主要介绍了5分钟获取deepseekapi并搭建简易问答应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需... 目录1、获取api2、获取base_url和chat_model3、配置模型参数方法一:终端中临时将加

Java中使用Java Mail实现邮件服务功能示例

《Java中使用JavaMail实现邮件服务功能示例》:本文主要介绍Java中使用JavaMail实现邮件服务功能的相关资料,文章还提供了一个发送邮件的示例代码,包括创建参数类、邮件类和执行结... 目录前言一、历史背景二编程、pom依赖三、API说明(一)Session (会话)(二)Message编程客

Java中List转Map的几种具体实现方式和特点

《Java中List转Map的几种具体实现方式和特点》:本文主要介绍几种常用的List转Map的方式,包括使用for循环遍历、Java8StreamAPI、ApacheCommonsCollect... 目录前言1、使用for循环遍历:2、Java8 Stream API:3、Apache Commons

JavaScript中的isTrusted属性及其应用场景详解

《JavaScript中的isTrusted属性及其应用场景详解》在现代Web开发中,JavaScript是构建交互式应用的核心语言,随着前端技术的不断发展,开发者需要处理越来越多的复杂场景,例如事件... 目录引言一、问题背景二、isTrusted 属性的来源与作用1. isTrusted 的定义2. 为

C#提取PDF表单数据的实现流程

《C#提取PDF表单数据的实现流程》PDF表单是一种常见的数据收集工具,广泛应用于调查问卷、业务合同等场景,凭借出色的跨平台兼容性和标准化特点,PDF表单在各行各业中得到了广泛应用,本文将探讨如何使用... 目录引言使用工具C# 提取多个PDF表单域的数据C# 提取特定PDF表单域的数据引言PDF表单是一

使用Python实现高效的端口扫描器

《使用Python实现高效的端口扫描器》在网络安全领域,端口扫描是一项基本而重要的技能,通过端口扫描,可以发现目标主机上开放的服务和端口,这对于安全评估、渗透测试等有着不可忽视的作用,本文将介绍如何使... 目录1. 端口扫描的基本原理2. 使用python实现端口扫描2.1 安装必要的库2.2 编写端口扫

PyCharm接入DeepSeek实现AI编程的操作流程

《PyCharm接入DeepSeek实现AI编程的操作流程》DeepSeek是一家专注于人工智能技术研发的公司,致力于开发高性能、低成本的AI模型,接下来,我们把DeepSeek接入到PyCharm中... 目录引言效果演示创建API key在PyCharm中下载Continue插件配置Continue引言

MySQL分表自动化创建的实现方案

《MySQL分表自动化创建的实现方案》在数据库应用场景中,随着数据量的不断增长,单表存储数据可能会面临性能瓶颈,例如查询、插入、更新等操作的效率会逐渐降低,分表是一种有效的优化策略,它将数据分散存储在... 目录一、项目目的二、实现过程(一)mysql 事件调度器结合存储过程方式1. 开启事件调度器2. 创

使用Python实现操作mongodb详解

《使用Python实现操作mongodb详解》这篇文章主要为大家详细介绍了使用Python实现操作mongodb的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、示例二、常用指令三、遇到的问题一、示例from pymongo import MongoClientf

SQL Server使用SELECT INTO实现表备份的代码示例

《SQLServer使用SELECTINTO实现表备份的代码示例》在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误,在SQLServer中,可以使用SELECTINT... 在数据库管理过程中,有时我们需要对表进行备份,以防数据丢失或修改错误。在 SQL Server 中,可以使用 SE