(六)、基于 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

相关文章

python生成随机唯一id的几种实现方法

《python生成随机唯一id的几种实现方法》在Python中生成随机唯一ID有多种方法,根据不同的需求场景可以选择最适合的方案,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习... 目录方法 1:使用 UUID 模块(推荐)方法 2:使用 Secrets 模块(安全敏感场景)方法

Java docx4j高效处理Word文档的实战指南

《Javadocx4j高效处理Word文档的实战指南》对于需要在Java应用程序中生成、修改或处理Word文档的开发者来说,docx4j是一个强大而专业的选择,下面我们就来看看docx4j的具体使用... 目录引言一、环境准备与基础配置1.1 Maven依赖配置1.2 初始化测试类二、增强版文档操作示例2.

Redis中Stream详解及应用小结

《Redis中Stream详解及应用小结》RedisStreams是Redis5.0引入的新功能,提供了一种类似于传统消息队列的机制,但具有更高的灵活性和可扩展性,本文给大家介绍Redis中Strea... 目录1. Redis Stream 概述2. Redis Stream 的基本操作2.1. XADD

Spring StateMachine实现状态机使用示例详解

《SpringStateMachine实现状态机使用示例详解》本文介绍SpringStateMachine实现状态机的步骤,包括依赖导入、枚举定义、状态转移规则配置、上下文管理及服务调用示例,重点解... 目录什么是状态机使用示例什么是状态机状态机是计算机科学中的​​核心建模工具​​,用于描述对象在其生命

Spring Boot 结合 WxJava 实现文章上传微信公众号草稿箱与群发

《SpringBoot结合WxJava实现文章上传微信公众号草稿箱与群发》本文将详细介绍如何使用SpringBoot框架结合WxJava开发工具包,实现文章上传到微信公众号草稿箱以及群发功能,... 目录一、项目环境准备1.1 开发环境1.2 微信公众号准备二、Spring Boot 项目搭建2.1 创建

IntelliJ IDEA2025创建SpringBoot项目的实现步骤

《IntelliJIDEA2025创建SpringBoot项目的实现步骤》本文主要介绍了IntelliJIDEA2025创建SpringBoot项目的实现步骤,文中通过示例代码介绍的非常详细,对大家... 目录一、创建 Spring Boot 项目1. 新建项目2. 基础配置3. 选择依赖4. 生成项目5.

JSONArray在Java中的应用操作实例

《JSONArray在Java中的应用操作实例》JSONArray是org.json库用于处理JSON数组的类,可将Java对象(Map/List)转换为JSON格式,提供增删改查等操作,适用于前后端... 目录1. jsONArray定义与功能1.1 JSONArray概念阐释1.1.1 什么是JSONA

nginx -t、nginx -s stop 和 nginx -s reload 命令的详细解析(结合应用场景)

《nginx-t、nginx-sstop和nginx-sreload命令的详细解析(结合应用场景)》本文解析Nginx的-t、-sstop、-sreload命令,分别用于配置语法检... 以下是关于 nginx -t、nginx -s stop 和 nginx -s reload 命令的详细解析,结合实际应

Linux下删除乱码文件和目录的实现方式

《Linux下删除乱码文件和目录的实现方式》:本文主要介绍Linux下删除乱码文件和目录的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux下删除乱码文件和目录方法1方法2总结Linux下删除乱码文件和目录方法1使用ls -i命令找到文件或目录

SpringBoot+EasyExcel实现自定义复杂样式导入导出

《SpringBoot+EasyExcel实现自定义复杂样式导入导出》这篇文章主要为大家详细介绍了SpringBoot如何结果EasyExcel实现自定义复杂样式导入导出功能,文中的示例代码讲解详细,... 目录安装处理自定义导出复杂场景1、列不固定,动态列2、动态下拉3、自定义锁定行/列,添加密码4、合并