RAG 每日一技(二):告别“一刀切”,试试更智能的递归字符分块!

9 阅读5分钟

前情回顾

书接上回!在上一篇文章:《RAG 每日一技(一):你的第一步就走错了?聊聊最基础的文本分块满怀期待地搭建自己的AI知识库,结果却被模型“人工智障” 》 中,我们聊了RAG系统中最基础、也最容易“翻车”的一步——文本分块(Chunking)。我们还亲手实践了最简单的“固定大小分块法”,并目睹了它如何无情地将完整的句子“大卸八块”,破坏了文本的语义。

这种“人工智障”式的分块方法,显然无法满足我们构建高质量AI问答系统的要求。

那么,有没有一种更“优雅”、更“智能”的分块方法呢?

当然有。今天的主角,就是大多数场景下的首选答案:递归字符文本分块 (Recursive Character Text Splitting)

什么是递归字符分块?

听名字有点“高大上”,但它的核心思想其实非常符合直觉。

想象一下,我们自己是如何阅读和拆分一篇长文的?我们通常会:

  1. 先看段落,尝试按段落分开。
  2. 如果一个段落太长,我们会再去看里面的句子,尝试按句号、问号等标点来切分。
  3. 如果一个句子还特别长(比如一个超长的法律条文),我们可能会再按短语(比如逗号、分号)来切分。
  4. 实在不行,最后才会按单词字符来“暴力”拆分。

递归字符分块器 (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)

效果分析

看到这个结果,是不是感觉舒服多了?

  1. 尊重段落结构:分块器首先尝试用 \n\n(段落分隔符)进行分割。我们的原文有两个大段落,第一个段落长度为79,小于 chunk_size (80),所以它被完整地保留为 Chunk 1。完美!
  2. 句子保持完整:第二个段落比较长,分块器会用下一级分隔符 \n 来切分。可以看到,Chunk 2 和 Chunk 3 都是完整的句子,没有任何一个词被粗暴地拆开。
  3. 语义连贯性强:每一个 chunk 都是一个或多个语义完整的句子,这为后续的向量嵌入和检索步骤打下了坚实的基础。当检索系统召回这样的 chunk 时,它提供给大模型的是高质量、易于理解的上下文。

对比一下昨天固定大小分块那个“惨案现场”,递归字符分块的优势不言而喻。它是在用一种更“懂”文本的方式在工作。

总结与预告

今日小结:

  • 递归字符分块 (Recursive Character Text Splitting) 是处理通用文本文档时强烈推荐的默认选项。
  • 它的核心优势在于按语义优先级(段落->句子->单词->字符)进行分割,能最大程度地保留文本的原始结构和语义完整性。

现在,我们有了一个强大的通用分块工具。但是,如果我们要处理的不是普通的文章,而是一些有特殊结构的文件呢?

比如,一段Python代码,我们希望按照“类”或“函数”来分割;或者一个 Markdown 文件,我们希望根据标题层级(###)来分割。

如果还用通用的递归分块器,效果可能就不会那么理想了。

明天预告:RAG 每日一技(三):不止文本,代码和Markdown如何优雅地分块?

对特定格式文档的处理感兴趣吗?点个关注,跟上《RAG 每日一技》,我们明天继续深挖!