理解 LangChain 的 RecursiveCharacterTextSplitter
大型语言模型是功能强大的工具,拥有广泛的功能;然而,它们也面临着一个明显的限制,即所谓的“上下文窗口”。这个上下文窗口定义了这些模型能够高效处理文本的边界。例如,GPT-3.5-turbo的运行上下文长度为 4,096 个词条,大约相当于 3,500 个单词。
但是,当你向这些模型呈现超出其上下文窗口的文档时会发生什么?这时,一种名为“分块”的巧妙策略就派上用场了。分块是指将文档划分成更小、更易于管理的部分,使其能够轻松适应大型语言模型的上下文窗口。
Langchain为用户提供了多种分块技术供选择。然而,在这些选项中,RecursiveCharacterTextSplitter是最受欢迎且强烈推荐的方法。
快速概览
该函数RecursiveCharacterTextSplitter
接收一段较长的文本,并根据指定的块大小进行拆分。它使用一组字符来实现拆分。默认提供的字符为["\n\n", "\n", " ", ""]
。
它接收较大的文本,然后尝试按第一个字符 进行拆分\n\n
。如果第一个拆分结果\n\n
仍然很大,则移动到下一个字符\n
,并尝试按该字符进行拆分。如果仍然大于我们指定的块大小,则移动到集合中的下一个字符,直到得到小于我们指定块大小的拆分结果。
代码实现
我的工作内容
2021年2月
上大学之前,课外我主要做的两件事是写作和编程。我不写论文。我写的是当时初学写作的人应该写的东西,现在可能仍然如此:短篇小说。我的小说很糟糕。它们几乎没有情节,只有一些有着强烈情感的人物,我以为这会让它们变得深刻。
我尝试编写的第一个程序是在 IBM 1401 上写的,当时我们学区用它来做当时所谓的“数据处理”。那时我九年级,十三四岁。学区的 1401 恰好在我们初中的地下室,我和我的朋友 Rich Draves 获准使用它。地下室就像一个迷你版的邦德反派老巢,各种看起来像外星人的机器——CPU、磁盘驱动器、打印机、读卡器——都堆放在明亮的荧光灯下的高架地板上。
以上文字摘自Paul Graham撰写的一篇文章,标题为:我的工作内容。让我们利用RecursiveCharacterTextSplitter
将其分成小块,每块最多 100 个字符。
首先我们从 langchain 导入它:
from langchain.text_splitter import RecursiveCharacterTextSplitter
我们将想要创建块的文本加载到名为的变量中text
。
text = """What I Worked On
February 2021
Before college the two main things I worked on, outside of school, were writing and programming. I didn't write essays. I wrote what beginning writers were supposed to write then, and probably still are: short stories. My stories were awful. They had hardly any plot, just characters with strong feelings, which I imagined made them deep.
The first programs I tried writing were on the IBM 1401 that our school district used for what was then called "data processing." This was in 9th grade, so I was 13 or 14. The school district's 1401 happened to be in the basement of our junior high school, and my friend Rich Draves and I got permission to use it. It was like a mini Bond villain's lair down there, with all these alien-looking machines — CPU, disk drives, printer, card reader — sitting up on a raised floor under bright fluorescent lights.
"""
接下来,我们创建一个RecursiveCharacterTextSplitter
实例,将其配置为chunk_size
100,并将chunk_overlap
值设置为零。我们的方法是使用长度函数根据每个块的字符数来测量它。
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 100,
chunk_overlap = 0,
length_function = len,
)
提供RecursiveCharacterTextSplitter
了几种执行拆分的方法。在本例中,我们将使用split_text方法。此方法需要一个表示文本的字符串输入,并返回一个字符串数组,每个字符串代表拆分后的一个块。
texts = text_splitter.split_text(text)
print(len(texts)) # 11
print(texts[0]) # 'What I Worked On\n\nFebruary 2021'
执行拆分后,我们的文本被成功分成总共 11 个独立的块。
深入解释
正如其名称所示,它RecursiveCharacterTextSplitter
使用递归作为实现文本拆分的核心机制。现在,让我们详细了解一下之前的代码是如何实现这一功能的。
在我们的演示中,我们将使用与代码实现过程中相同的文本和参数。这涉及 Paul Graham 文章中的一段,我们将考虑 100 个字符的块大小。我们用于拆分的字符将是['\n\n', '\n', ' ', '' ]
。
让我们从初始文本开始。目前,它以人类可读的形式呈现,下一步是将其转换为计算机可以轻松理解的格式。
现在,新线路已转换为\n
,这正是我们进行拆分过程所需要的。
让我们选择文本。这可以比作split_text
在我们的 上调用方法text
。
如前所述,它RecursiveCharacterTextSplitter
会尝试使用一组预定义的字符来启动拆分。它的第一次尝试涉及\n\n
字符,该字符用作按段落拆分的手段。现在让我们在文本中识别出所有出现此字符的位置。
一旦我们找到了所有\n\n
字符的实例,下一步就是使用该字符作为指定的分隔符来执行拆分。
目前,我们有四个分片。下一步是评估每个分片,检查它们是否满足小于我们指定的块大小(设置为 100 个字符)的条件。
前两个拆分满足此条件,因此赢得了良好拆分的标签。由于两个片段都包含少于 100 个字符,因此我们可以将它们组合起来以创建初始块。
进行第二次拆分时,我们发现使用\n\n
字符无法进一步缩减。因此,我们继续处理下一个字符:\n
。我们的目标是使用 字符执行拆分,\n
并确定是否可以缩减拆分的大小。
此操作类似于split_text
在第二个拆分文本上调用,但包含字符\n
。这就是递归概念发挥作用的地方。
使用字符执行拆分后\n
,我们得到了两个拆分。第一个拆分由于只包含一个字符,因此算是一个好的拆分。然而,第二个拆分超出了我们指定的块大小。
因此,我们需要split_text
再次对这个特定的拆分调用该方法。不过,这次我们将使用字符列表中的下一个字符(恰好是该' '
字符)进行拆分。
最终,我们成功减小了分片大小。现在,我们继续迭代每个分片以执行合并。合并的指导原则是,合并后的分片大小不得超过我们指定的 100 个字符的块大小。
合并后,我们得到四个块,每个块都遵循我们的条件,即每个块不应超过 100 个字符。
现在,让我们重新审视原始文本分割并确定哪些分割仍有待处理。
我们仍然有一个分片大于我们的块大小。我们再次重复相同的步骤。
我们使用换行符作为分隔符来开始分割。
我们使用空格作为分隔符进行拆分。
接下来,我们进行合并,确保合并的段不超过定义的块大小。
经过整个流程,我们最终生成了 11 个独立的区块。这 11 个区块均成功遵守了 100 个字符的限制。
这一结果与我们通过编程实现的结果完全一致。
就这样,我们深入探究了 LangChain 的内部工作原理RecursiveCharacterTextSplitter
。感兴趣的朋友可以点击此处探索源代码。如果您觉得本文内容丰富,请点赞:💖 🦄 🤯 🙌 🔥