在构建基于大语言模型(LLM)的应用程序时,如检索增强生成(RAG)系统,分块(chunking)是一项至关重要的技术。分块是将大块文本分解成较小段落的过程,这使得文本数据更易于管理和处理。通过分块,我们能够更高效地进行内容嵌入(embedding),并显著提升从向量数据库中召回内容的相关性和准确性。

在这里插入图片描述

在实际操作中,分块的好处是多方面的。首先,它能够提高模型处理的效率,因为较小的文本段落更容易进行嵌入和检索。

其次,分块后的文本能够更精确地匹配用户查询,从而提供更相关的搜索结果。这对于需要高精度信息检索和内容生成的应用程序尤为重要。

通过优化内容的分块和嵌入策略,我们可以最大化LLM在各种应用场景中的性能。分块技术不仅提高了内容召回的准确性,还提升了整体系统的响应速度和用户体验。

因此,在构建和优化基于LLM的应用程序时,理解和应用分块技术是不可或缺的步骤。

RAG 的分块(Chunking)直接决定检索质量与生成效果,主流方法可分为:固定窗口、滑动窗口、递归、基于结构、语义、LLM 辅助、层次 / 多粒度。下面逐一讲清原理、优缺点与适用场景。

一、固定大小分块(CharacterTextSplitter)

固定大小分块是一种最常见且最直接的分块方法。此方法中,我们需要决定每个块中包含的tokens数量,并确定这些块之间是否应该有重叠。通常,我们会在块之间保持一些重叠,以确保语义上下文在块之间不会丢失。这种方法的一个主要优势是,它在计算上更加经济且易于实现,因为在分块过程中无需依赖任何复杂的自然语言处理(NLP)库。

固定大小的分块方式在大多数情况下是最佳选择。其简单性和高效性使得这种方法在许多应用场景中得到了广泛应用。通过预先设定的块大小和适度的重叠,我们可以确保文本内容在被切分后仍然保持语义连贯性,从而提高后续处理和检索的准确性。

此外,固定大小分块的另一个优势在于其一致性。每个块具有相同的大小,使得后续处理步骤,如嵌入和检索,更加标准化和可预测。这种一致性不仅简化了实现过程,还提高了系统的整体稳定性和性能。

  • 原理:按固定 token / 字符数切分(如 512/1024 token),不考虑语义边界。

  • 特点:最简单、速度快、资源开销低。

  • 参数:chunk_size=512~1500 token,overlap=0。

  • 优缺点

    • ✅ 实现简单、速度快、易并行。
    • ❌ 易割裂句子 / 语义,边界信息丢失。
  • 适用:快速原型、日志 / OCR 低质量文本、非结构化原始数据。

示例代码
from langchain_text_splitters import CharacterTextSplitter

sample_text = """固定大小分块是一种最常见且最直接的分块方法。此方法中,我们需要决定每个块中包含的tokens数量,并确定这些块之间是否应该有重叠。通常,我们会在块之间保持一些重叠,以确保语义上下文在块之间不会丢失。这种方法的一个主要优势是,它在计算上更加经济且易于实现,因为在分块过程中无需依赖任何复杂的自然语言处理(NLP)库。

固定大小的分块方式在大多数情况下是最佳选择。其简单性和高效性使得这种方法在许多应用场景中得到了广泛应用。通过预先设定的块大小和适度的重叠,我们可以确保文本内容在被切分后仍然保持语义连贯性,从而提高后续处理和检索的准确性。

此外,固定大小分块的另一个优势在于其一致性。每个块具有相同的大小,使得后续处理步骤,如嵌入和检索,更加标准化和可预测。这种一致性不仅简化了实现过程,还提高了系统的整体稳定性和性能。"""

text_splitter = CharacterTextSplitter(
    separator="\n",#按空格分割
    chunk_size=256,#分块大小
    chunk_overlap=25,#重叠比例
    length_function=len
)

docs = text_splitter.create_documents([sample_text])

for i,doc in enumerate(docs):
    print(f"-----chunk {i+1}-----")
    print(len(doc.page_content))
    print(doc.page_content)

运行结果

-----chunk 1-----
157
固定大小分块是一种最常见且最直接的分块方法。此方法中,我们需要决定每个块中包含的tokens数量,并确定这些块之间是否应该有重叠。通常,我们会在块之间保持一些重叠,以确保语义上下文在块之间不会丢失。这种方法的一个主要优势是,它在计算上更加经济且易于实现,因为在分块过程中无需依赖任何复杂的自然语言处理(NLP)库。
-----chunk 2-----
201
固定大小的分块方式在大多数情况下是最佳选择。其简单性和高效性使得这种方法在许多应用场景中得到了广泛应用。通过预先设定的块大小和适度的重叠,我们可以确保文本内容在被切分后仍然保持语义连贯性,从而提高后续处理和检索的准确性。
此外,固定大小分块的另一个优势在于其一致性。每个块具有相同的大小,使得后续处理步骤,如嵌入和检索,更加标准化和可预测。这种一致性不仅简化了实现过程,还提高了系统的整体稳定性和性能。

进程已结束,退出代码为 0

二、递归分块(RecursiveCharacterTextSplitter)

递归分块是一种高级文本分块方法,通过使用一组分隔符以分层和迭代的方式将输入文本分解成更小的块。该方法的核心思想是,如果在初始分割时未能生成所需大小或结构的块,则会使用不同的分隔符或标准对生成的块递归调用,直到获得所需的块大小或结构。虽然最终生成的块大小可能不完全相同,但它们会逼近所需的大小,从而保持一定的均匀性。

递归分块方法的主要优势在于其灵活性和适应性。它能够根据文本的实际内容和结构进行调整,逐步细化块的大小,直至达到最佳的分割效果。这种方法特别适合处理结构复杂或长度不一的文本,使得分块后的文本既能保持语义完整性,又能满足后续处理的需求。

  • 原理:先按大粒度切(如章节 / 段落),过长则递归细分,直到满足长度限制;优先在语义边界(句子 / 标点)切。

  • 流程:文档 → 章节 → 段落 → 句子 → 子句。

  • 优缺点

    • ✅ 兼顾结构与长度,长文档友好,块大小更均匀。
    • ❌ 实现复杂,递归有开销,极端情况仍可能断句。
  • 适用:长篇技术文档、书籍、企业级长报告。

示例代码
from langchain_text_splitters import RecursiveCharacterTextSplitter

sample_text = """在嵌入内容(即embedding)时,我们需要根据内容的长度来预测和调整不同的行为。内容的长度可以是短如句子,或者长如段落甚至整个文档。不同长度的内容在嵌入过程中会有不同的表现和效果。

当嵌入一个句子时,生成的向量会集中在句子的特定含义上。因此,当与其他句子的嵌入进行比较时,这些向量会在句子的层次上进行比较。这种方法有助于精确捕捉单个句子的意义,但可能会忽略在更大段落或整个文档中存在的更广泛的上下文信息。

当嵌入一个完整的段落或文档时,嵌入过程不仅要考虑整体上下文,还要考虑文本中各个句子和短语之间的关系。这种方法可以产生更全面的向量表示,捕捉文本的广泛含义和主题。然而,较大的输入文本也可能引入噪声或淡化单个句子或短语的重要性,从而在查询索引时更难找到精确匹配。

查询的长度也会影响嵌入之间的关系。较短的查询,如单个句子或短语,将专注于细节,可能更适合与句子级别的嵌入进行匹配。而跨越多个句子或段落的较长查询则可能更适合段落或文档级别的嵌入,因为它们会寻找更广泛的上下文或主题。"""

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=256,
    chunk_overlap=25,
    #默认分隔符为["\n\n","\n"," ",""]
)

docs = text_splitter.create_documents([sample_text])

for i,doc in enumerate(docs):
    print(f"-----chunk {i+1}-----")
    print(doc.page_content)

运行结果

-----chunk 1-----
在嵌入内容(即embedding)时,我们需要根据内容的长度来预测和调整不同的行为。内容的长度可以是短如句子,或者长如段落甚至整个文档。不同长度的内容在嵌入过程中会有不同的表现和效果。

当嵌入一个句子时,生成的向量会集中在句子的特定含义上。因此,当与其他句子的嵌入进行比较时,这些向量会在句子的层次上进行比较。这种方法有助于精确捕捉单个句子的意义,但可能会忽略在更大段落或整个文档中存在的更广泛的上下文信息。
-----chunk 2-----
当嵌入一个完整的段落或文档时,嵌入过程不仅要考虑整体上下文,还要考虑文本中各个句子和短语之间的关系。这种方法可以产生更全面的向量表示,捕捉文本的广泛含义和主题。然而,较大的输入文本也可能引入噪声或淡化单个句子或短语的重要性,从而在查询索引时更难找到精确匹配。

查询的长度也会影响嵌入之间的关系。较短的查询,如单个句子或短语,将专注于细节,可能更适合与句子级别的嵌入进行匹配。而跨越多个句子或段落的较长查询则可能更适合段落或文档级别的嵌入,因为它们会寻找更广泛的上下文或主题。

进程已结束,退出代码为 0

三、基于结构的分块(MarkdownHeaderTextSplitter)

根据Markdown文件的标题层级进行精确分割,同时保留文本的上下文和结构信息。这种分割方式特别适合处理报告、教程等结构化文档,有助于提升文本向量化(embedding)的效果。

允许用户指定需要拆分的标题等级。例如,用户可以选择只拆分一级标题、二级标题或三级标题等。在拆分过程中,该工具会保留每个标题及其对应的内容块,从而生成一个结构化的文档集合。

  • 原理:利用文档原生结构(标题层级、段落、列表、换行、HTML/Markdown 标签)切分。

  • 常见规则

    • 按 H1/H2/H3 标题切分章节;
    • 按空行 / 段落分隔符切分段落;
    • 列表 / 表格独立成块。
  • 优缺点

    • ✅ 逻辑清晰、语义完整、检索精准、易解释。
    • ❌ 依赖格式规范,无结构文本效果差。
  • 适用:Markdown/HTML、技术手册、Wiki、结构化报告。

示例代码
from langchain_text_splitters import MarkdownHeaderTextSplitter

# 定义要按照哪些标题级别分块(# 一级标题、## 二级标题...)
headers_to_split_on = [
    ("#", "一级标题"),
    ("##", "二级标题"),
    ("###", "三级标题"),
]

# 创建分块器
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

# 测试用 Markdown 文本
md_text = """
# 画蛇添足
## 典故来源:
古代有几个人比赛画蛇,谁先画完就能喝到一壶酒。其中一人很快画好了,见别人还没完成,就想炫耀,于是给蛇添了几只脚。这时另一个人也画完了,他笑着说:“蛇本来没有脚,你这不是多此一举吗?” 最后,那壶酒被后者拿走了。
## 含义:
比喻做了多余的事,反而弄巧成拙,把事情搞坏。
## 使用场景:
多用于批评那些在事情已经圆满完成后,还额外做不必要的举动,导致结果变差的情况。
## 例句:
这篇文章已经写得很简洁明了了,你再加上那段无关的议论,反而有点画蛇添足。

# 掩耳盗铃
## 典故来源:
有个小偷,想偷别人家大门上挂着的门铃。他知道一碰门铃就会响,会被人发现。于是他想出个 “好办法”,认为只要把自己耳朵捂住,就听不到铃声了,别人也同样听不到。结果他去偷铃时,刚一碰到铃,铃声大作,他被当场抓住。
## 含义:
偷铃铛怕别人听见而捂住自己的耳朵。比喻自己欺骗自己,明明掩盖不住的事情偏要想法子掩盖。
## 使用场景:
常用来形容那些自欺欺人,采取的行为并不能真正解决问题,只是自我麻痹的情况。
## 例句:
他以为把考试作弊的纸条藏起来就不会被发现,这简直是掩耳盗铃,老师早就注意到他的小动作了。
"""

# 执行分块
chunks = markdown_splitter.split_text(md_text)

# 输出结果
for i, chunk in enumerate(chunks, 1):
    print(f"===== 分块 {i} =====")
    print("内容:", chunk.page_content)
    print("标题:", chunk.metadata)
    print()

运行结果

==== 分块 1 =====
内容: 古代有几个人比赛画蛇,谁先画完就能喝到一壶酒。其中一人很快画好了,见别人还没完成,就想炫耀,于是给蛇添了几只脚。这时另一个人也画完了,他笑着说:“蛇本来没有脚,你这不是多此一举吗?” 最后,那壶酒被后者拿走了。
标题: {'一级标题': '画蛇添足', '二级标题': '典故来源:'}

===== 分块 2 =====
内容: 比喻做了多余的事,反而弄巧成拙,把事情搞坏。
标题: {'一级标题': '画蛇添足', '二级标题': '含义:'}

===== 分块 3 =====
内容: 多用于批评那些在事情已经圆满完成后,还额外做不必要的举动,导致结果变差的情况。
标题: {'一级标题': '画蛇添足', '二级标题': '使用场景:'}

===== 分块 4 =====
内容: 这篇文章已经写得很简洁明了了,你再加上那段无关的议论,反而有点画蛇添足。
标题: {'一级标题': '画蛇添足', '二级标题': '例句:'}

===== 分块 5 =====
内容: 有个小偷,想偷别人家大门上挂着的门铃。他知道一碰门铃就会响,会被人发现。于是他想出个 “好办法”,认为只要把自己耳朵捂住,就听不到铃声了,别人也同样听不到。结果他去偷铃时,刚一碰到铃,铃声大作,他被当场抓住。
标题: {'一级标题': '掩耳盗铃', '二级标题': '典故来源:'}

===== 分块 6 =====
内容: 偷铃铛怕别人听见而捂住自己的耳朵。比喻自己欺骗自己,明明掩盖不住的事情偏要想法子掩盖。
标题: {'一级标题': '掩耳盗铃', '二级标题': '含义:'}

===== 分块 7 =====
内容: 常用来形容那些自欺欺人,采取的行为并不能真正解决问题,只是自我麻痹的情况。
标题: {'一级标题': '掩耳盗铃', '二级标题': '使用场景:'}

===== 分块 8 =====
内容: 他以为把考试作弊的纸条藏起来就不会被发现,这简直是掩耳盗铃,老师早就注意到他的小动作了。
标题: {'一级标题': '掩耳盗铃', '二级标题': '例句:'}

进程已结束,退出代码为 0

四、自定义正则分块

自定义的分块是针对特定应用场景和文本结构的定制化分块方法。这种方法依据文本的内容和结构特征,采用特定的分块策略,以实现更高效、更精确的文本处理。自定义的分块可以结合领域知识和具体需求,灵活调整分块方式,以优化文本的后续处理和分析。

这种分块方法的核心在于根据文本的特性选择合适的分隔符和分块策略。例如,在处理法律文档时,可能需要按条款或章节分块,以确保每个分块都能独立地表达特定的法律条款。而在处理技术文档时,则可能根据函数或代码段进行分块,以便于代码的理解和分析。

自定义的分块方法通常涉及以下几个步骤:首先,识别文本中的关键结构或语义单元;然后,定义适合的分隔符或规则;最后,将文本按照这些规则进行分块。这样的定制化分块不仅可以提高文本的处理效率,还能确保在分析和检索时的高准确性。

示例代码
import re

def specialized_chunking(text:str, delimiter:str):
    """
    自定义的分块函数,根据特定的分隔符将文本分割成块
    :param text: 输入文本
    :param delimiter: 分隔符,用于定义分块的边界
    :return: 分割后的文本块列表
    """
    # 使用正则表达式根据特定分隔符分割文本
    chunks = re.split(delimiter, text)
    # 去除空块
    chunks = [chunk.strip() for chunk in chunks if chunk.strip()]
    return chunks

# 示例文本:技术文档中的代码块
text = """# This is a code block
def example_function():
    print("Hello, world!")

# Another code block
def another_function():
    return True"""

# 定义特定的分隔符,例如注释标记
delimiter = r'#'

# 进行分块
chunks = specialized_chunking(text, delimiter)
for i, chunk in enumerate(chunks):
    print(f"===== 分块 {i+1} =====")
    print(chunk)

运行结果

===== 分块 1 =====
This is a code block
def example_function():
    print("Hello, world!")
===== 分块 2 =====
Another code block
def another_function():
    return True

进程已结束,退出代码为 0

五、总结

分块需要综合考虑多种因素,以确保生成的块既能满足应用的具体需求,又能在计算资源和性能之间取得平衡。

首先,块大小应与应用程序的功能和目标密切相关。例如,对于需要细粒度语义分析的应用,如细节丰富的问答系统或深入的文本理解,较小的块大小可能更为合适。这种块大小能够提供更精确的上下文信息,避免丢失关键细节。然而,块过小可能会导致上下文断裂,使得语义理解受到影响。

其次,考虑到处理效率和存储要求,块大小也应与计算资源和性能需求相匹配。在资源有限的情况下,较小的块可以减少内存和处理时间的消耗,但也可能增加处理的复杂性。相对较大的块虽然在处理时可能更具效率,但需要更多的存储和计算资源,并且在文本分析时可能会引入更多的噪声。

应用场景的特性也会影响块大小的选择。例如,在处理法律文档或技术手册时,可能需要根据章节或条款进行分块,这样的块大小能够更好地保持文档结构的完整性。而在处理社交媒体内容或实时消息时,较小的块可能更适合,能够更灵活地适应内容的快速变化。

最终,确定最佳块大小的过程通常是一个迭代过程,需要根据实际使用情况进行调整。通过对不同块大小的实验和评估,可以找到最适合特定应用程序的块大小,从而优化文本处理的效果和效率。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐