前情回顾
书接上回!在上一篇文章:《RAG 每日一技(一):你的第一步就走错了?聊聊最基础的文本分块满怀期待地搭建自己的AI知识库,结果却被模型“人工智障” 》 中,我们聊了RAG系统中最基础、也最容易“翻车”的一步——文本分块(Chunking)。我们还亲手实践了最简单的“固定大小分块法”,并目睹了它如何无情地将完整的句子“大卸八块”,破坏了文本的语义。
这种“人工智障”式的分块方法,显然无法满足我们构建高质量AI问答系统的要求。
那么,有没有一种更“优雅”、更“智能”的分块方法呢?
当然有。今天的主角,就是大多数场景下的首选答案:递归字符文本分块 (Recursive Character Text Splitting) 。
什么是递归字符分块?
听名字有点“高大上”,但它的核心思想其实非常符合直觉。
想象一下,我们自己是如何阅读和拆分一篇长文的?我们通常会:
- 先看段落,尝试按段落分开。
- 如果一个段落太长,我们会再去看里面的句子,尝试按句号、问号等标点来切分。
- 如果一个句子还特别长(比如一个超长的法律条文),我们可能会再按短语(比如逗号、分号)来切分。
- 实在不行,最后才会按单词或字符来“暴力”拆分。
递归字符分块器 (RecursiveCharacterTextSplitter) 的工作原理与此完全相同!
它维护一个分隔符列表 (separators) ,默认情况下是 ["\n\n", "\n", " ", ""]
。
\n\n
(双换行符):代表段落。\n
(换行符):代表行。- ``(空格):代表单词。
""
(空字符串):代表字符。
它的“递归”就体现在:
它会优先尝试用列表中的第一个分隔符(
\n\n
)进行分割。如果分割后的文本块仍然大于我们设定的chunk_size
,它就会“递归地”进入下一层,用第二个分隔符(\n
)对这个过大的块进行再分割,以此类推,直到切分出的文本块小于chunk_size
为止。
这种方法最大限度地保留了文本的语义结构,确保了段落和句子的完整性。
上手实践,立见分晓
我们还是用代码来说话。这次,我们用一段稍微复杂一点,包含段落和换行的文本来测试。
# 稍微复杂一点的示例文本,包含段落和换行
text = """RAG(Retrieval-Augmented Generation)是一种结合了检索和生成技术的自然语言处理模型。
它的核心思想是,在生成答案之前,先从一个大规模的知识库中检索出与问题相关的文档片段。
然后将这些片段作为上下文信息,引导生成模型(Generator)产生更准确、更丰富的回答。
这个框架显著提升了大型语言模型在处理知识密集型任务时的表现,是当前构建高级问答系统的热门技术。
"""
# 导入递归字符分块器
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 初始化分块器
# 这次我们把 chunk_size 设置为80,overlap为10
# 注意看,分隔符列表是默认的
text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", " ", ""], # 这是默认的分隔符
chunk_size = 80,
chunk_overlap = 10,
length_function = len,
)
# 进行分块
chunks = text_splitter.split_text(text)
# 我们来看看分块结果
for i, chunk in enumerate(chunks):
print(f"--- Chunk {i+1} ---")
print(chunk)
print(f"(长度: {len(chunk)})\n")
输出结果:
--- Chunk 1 ---
RAG(Retrieval-Augmented Generation)是一种结合了检索和生成技术的自然语言处理模型。
它的核心思想是,在生成答案之前,先从一个大规模的知识库中检索出与问题相关的文档片段。
(长度: 79)
--- Chunk 2 ---
然后将这些片段作为上下文信息,引导生成模型(Generator)产生更准确、更丰富的回答。
(长度: 46)
--- Chunk 3 ---
这个框架显著提升了大型语言模型在处理知识密集型任务时的表现,是当前构建高级问答系统的热门技术。
(长度: 57)
效果分析
看到这个结果,是不是感觉舒服多了?
- 尊重段落结构:分块器首先尝试用
\n\n
(段落分隔符)进行分割。我们的原文有两个大段落,第一个段落长度为79,小于chunk_size
(80),所以它被完整地保留为 Chunk 1。完美! - 句子保持完整:第二个段落比较长,分块器会用下一级分隔符
\n
来切分。可以看到,Chunk 2 和 Chunk 3 都是完整的句子,没有任何一个词被粗暴地拆开。 - 语义连贯性强:每一个 chunk 都是一个或多个语义完整的句子,这为后续的向量嵌入和检索步骤打下了坚实的基础。当检索系统召回这样的 chunk 时,它提供给大模型的是高质量、易于理解的上下文。
对比一下昨天固定大小分块那个“惨案现场”,递归字符分块的优势不言而喻。它是在用一种更“懂”文本的方式在工作。
总结与预告
今日小结:
- 递归字符分块 (Recursive Character Text Splitting) 是处理通用文本文档时强烈推荐的默认选项。
- 它的核心优势在于按语义优先级(段落->句子->单词->字符)进行分割,能最大程度地保留文本的原始结构和语义完整性。
现在,我们有了一个强大的通用分块工具。但是,如果我们要处理的不是普通的文章,而是一些有特殊结构的文件呢?
比如,一段Python代码,我们希望按照“类”或“函数”来分割;或者一个 Markdown 文件,我们希望根据标题层级(#
,##
)来分割。
如果还用通用的递归分块器,效果可能就不会那么理想了。
明天预告:RAG 每日一技(三):不止文本,代码和Markdown如何优雅地分块?
对特定格式文档的处理感兴趣吗?点个关注,跟上《RAG 每日一技》,我们明天继续深挖!