一、理解文本分块文本分块Text Chunking是构建 RAG 流程的关键步骤。它的原理是将加载后的长篇文档切分成更小、更易于处理的单元。这些被切分出的文本块是后续向量检索和模型处理的基本单位。二、文本分块重要性2.1 满足模型上下文限制将文本分块的首要原因是为了适应 RAG 系统中两个核心组件的硬性限制嵌入模型 (Embedding Model): 负责将文本块转换为向量。这类模型有严格的输入长度上限。例如许多常用的嵌入模型如bge-base-zh-v1.5的上下文窗口为512个token。任何超出此限制的文本块在输入时都会被截断导致信息丢失生成的向量也无法完整代表原文的语义。因此文本块的大小必须小于等于嵌入模型的上下文窗口。大语言模型 (LLM): 负责根据检索到的上下文生成答案。LLM同样有上下文窗口限制尽管通常比嵌入模型大得多从几千到上百万token不等。检索到的所有文本块连同用户问题和提示词都必须能被放入这个窗口中。如果单个块过大可能会导致只能容纳少数几个相关的块限制了LLM回答问题时可参考的信息广度。因此分块是确保文本能够被两个模型完整、有效处理的基础。2.2 为何“块”不是越大越好假设嵌入模型最多能处理 8192 个 token是否应该把块切得尽可能大比如8000个token呢答案是否定的。块的大小并非越大越好过大的块会严重影响RAG系统的性能。2.2.1 嵌入过程中的信息损失大多数嵌入模型都基于 Transformer 编码器。其工作流程大致如下分词 (Tokenization): 将输入的文本块分解成一个个 token。向量化 (Vectorization): Transformer 为每个 token生成一个高维向量表示。池化 (Pooling): 通过某种方法如取[CLS]位的向量、对所有token向量求平均mean pooling等将所有 token 的向量压缩成一个单一的向量这个向量代表了整个文本块的语义。[CLS]是BERT等Transformer模型在输入文本开头添加的特殊标记它通过自注意力机制动态聚合整个序列的上下文信息其最终向量被训练用作代表全局语义的嵌入。在这个压缩过程中信息损失是不可避免的。一个768维的向量需要概括整个文本块的所有信息。文本块越长包含的语义点越多这个单一向量所承载的信息就越稀释导致其表示变得笼统关键细节被模糊化从而降低了检索的精度。2.2.2 生成过程的“大海捞针” (Lost in the Middle)即使将检索到的多个大块文本都塞进LLM的长上下文窗口中也会出现关键信息被“淹没”在大量无关内容里的问题。有研究表明 1当LLM处理非常长的、充满大量信息的上下文时它倾向于更好地记住开头和结尾的信息而忽略中间部分的内容。如果提供给LLM的上下文块又大又杂充满了与问题无关的噪音模型就很难从中提取出最关键的信息来形成答案从而导致回答质量下降或产生幻觉。2.2.3 主题稀释导致检索失败一个好的文本块应该聚焦于一个明确、单一的主题。如果一个块包含太多不相关的主题它的语义就会被稀释导致在检索时无法被精确匹配。举个栗子假设有一个关于《王者荣耀》英雄鲁班七号的攻略文档。糟糕的分块策略将“技能介绍”、“推荐出装”和“背景故事”这三个完全不同主题的内容全部放在一个巨大的文本块里。当玩家查询“鲁班七号怎么出装”时这个大块虽然包含了出装信息但由于被技能说明和英雄故事等无关主题严重稀释其整体的检索相关性得分可能会很低导致无法被召回。优秀的分块策略将“技能”、“出装”和“故事”分别切分为三个独立的、主题聚焦的块。当玩家再次查询时“推荐出装”这个块会因为与查询高度相关而获得极高的分数从而被精准地检索出来。通过合理分块可以有效提升检索的信噪比确保了后续生成环节能得到最优质、最相关的上下文。三、基础分块策略LangChain 提供了丰富且易于使用的文本分割器Text Splitters下面将介绍几种最核心的策略。3.1 固定大小分块这是最简单直接的分块方法。根据LangChain源码这种方法的工作原理分为两个主要阶段1按段落分割CharacterTextSplitter采用默认分隔符\n\n使用正则表达式将文本按段落进行分割通过_split_text_with_regex函数处理。2智能合并调用继承自父类的_merge_splits方法将分割后的段落依次合并。该方法会监控累积长度当超过chunk_size时形成新块并通过重叠机制chunk_overlap保持上下文连续性同时在必要时发出超长块的警告。需要注意CharacterTextSplitter实际实现的并非严格的固定大小分块。根据_merge_splits源码逻辑这种方法会优先保持段落完整性只有当添加新段落会导致总长度超过chunk_size时才会结束当前块处理超长段落如果单个段落超过chunk_size系统会发出警告但仍将其作为完整块保留应用重叠机制通过chunk_overlap参数在块之间保持内容重叠确保上下文连续性所以LangChain 的实现更准确地应该称为段落感知的自适应分块块大小会根据段落边界动态调整。from langchain.text_splitter import CharacterTextSplitter from langchain_community.document_loaders import TextLoader loader TextLoader(../../data/C2/txt/蜂医.txt) docs loader.load() text_splitter CharacterTextSplitter( chunk_size200, # 每个块的目标大小为100个字符 chunk_overlap10 # 每个块之间重叠10个字符以缓解语义割裂 ) chunks text_splitter.split_documents(docs) print(f文本被切分为 {len(chunks)} 个块。\n) print(--- 前5个块内容示例 ---) for i, chunk in enumerate(chunks[:5]): print( * 60) # chunk 是一个 Document 对象需要访问它的 .page_content 属性来获取文本 print(f块 {i1} (长度: {len(chunk.page_content)}): {chunk.page_content})这种方法的主要优势在于实现简单、处理速度快且计算开销小。劣势在于可能会在语义边界处切断文本影响内容的完整性和连贯性。实际的固定大小分块实现如LangChain的CharacterTextSplitter通常会结合分隔符来减少这种问题在段落边界处优先切分只有在必要时才会强制按大小切断。因此这种方法在日志分析、数据预处理等场景中仍有其应用价值。3.2 递归字符分块在前面的章节中已经尝试了使用RecursiveCharacterTextSplitter的默认配置来处理文档分块。现在让我们深入了解RecursiveCharacterTextSplitter的实现。这种分块器通过分隔符层级递归处理相对与固定大小分块改善了超长文本的处理效果。算法流程 1寻找有效分隔符: 从分隔符列表中从前到后遍历找到第一个在当前文本中存在的分隔符。如果都不存在使用最后一个分隔符通常是空字符串。2切分与分类处理: 使用选定的分隔符切分文本然后遍历所有片段如果片段不超过块大小: 暂存到_good_splits中准备合并如果片段超过块大小:首先将暂存的合格片段通过_merge_splits合并成块然后检查是否还有剩余分隔符有剩余分隔符: 递归调用_split_text继续分割无剩余分隔符: 直接保留为超长块3最终处理: 将剩余的暂存片段合并成最后的块实现细节批处理机制: 先收集所有合格片段_good_splits遇到超长片段时才触发合并操作。递归终止条件: 关键在于if not new_separators判断。当分隔符用尽时new_separators为空停止递归直接保留超长片段。确保算法不会无限递归。与固定大小分块的关键差异固定大小分块遇到超长段落时只能发出警告并保留。递归分块会继续使用更细粒度的分隔符句子→单词→字符直到满足大小要求。具体示例如下from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.document_loaders import TextLoader loader TextLoader(../../data/C2/txt/蜂医.txt) docs loader.load() text_splitter RecursiveCharacterTextSplitter( separators[\n\n, \n, 。, , , ], # 分隔符优先级 chunk_size200, chunk_overlap10, ) chunks text_splitter.split_text(docs)分隔符配置默认分隔符[\n\n, \n, , ]多语言支持对于无词边界语言中文、日文、泰文可添加separators[ \n\n, \n, , ., ,, \u200b, # 零宽空格(泰文、日文) \uff0c, \u3001, # 全角逗号、表意逗号 \uff0e, \u3002, # 全角句号、表意句号 ]编程语言特化支持RecursiveCharacterTextSplitter能够针对特定的编程语言如Python, Java等使用预设的、更符合代码结构的分隔符。它们通常包含语言的顶级语法结构如类、函数定义和次级结构如控制流语句以实现更符合代码逻辑的分割。# 针对代码文档的优化分隔符 splitter RecursiveCharacterTextSplitter.from_language( languageLanguage.PYTHON, # 支持Python、Java、C等 chunk_size500, chunk_overlap50 )递归字符分块的原理是采用一组有层次结构的分隔符如段落、句子、单词进行递归分割旨在有效平衡语义完整性与块大小控制。在RecursiveCharacterTextSplitter的实现中该分块器首先尝试使用最高优先级的分隔符如段落标记来切分文本。如果切分后的块仍然过大会继续对这个大块应用下一优先级分隔符如句号如此循环往复直到块满足大小限制。这种分层处理的机制能够在尽可能保持高级语义结构完整性的同时有效控制块大小。3.3 语义分块语义分块Semantic Chunking是一种更智能的方法这种方法不依赖于固定的字符数或预设的分隔符而是尝试根据文本的语义内涵来切分。其核心是在语义主题发生显著变化的地方进行切分。这使得每个分块都具有高度的内部语义一致性。LangChain 提供了langchain_experimental.text_splitter.SemanticChunker来实现这一功能。实现原理SemanticChunker的工作流程可以概括为以下几个步骤1句子分割 (Sentence Splitting)首先使用标准的句子分割规则例如基于句号、问号、感叹号将输入文本拆分成一个句子列表。2上下文感知嵌入 (Context-Aware Embedding)这是SemanticChunker的一个关键设计。该分块器不是对每个句子独立进行嵌入而是通过buffer_size参数默认为1来捕捉上下文信息。对于列表中的每一个句子这种方法会将其与前后各buffer_size个句子组合起来然后对这个临时的、更长的组合文本进行嵌入。这样每个句子最终得到的嵌入向量就融入了其上下文的语义。3计算语义距离 (Distance Calculation)计算每对相邻句子的嵌入向量之间的余弦距离。这个距离值量化了两个句子之间的语义差异——距离越大表示语义关联越弱跳跃越明显。4识别断点 (Breakpoint Identification)SemanticChunker会分析所有计算出的距离值并根据一个统计方法默认为percentile来确定一个动态阈值。例如它可能会将所有距离中第95百分位的值作为切分阈值。所有距离大于此阈值的点都被识别为语义上的“断点”。5合并成块 (Merging into Chunks)最后根据识别出的所有断点位置将原始的句子序列进行切分并将每个切分后的部分内的所有句子合并起来形成一个最终的、语义连贯的文本块。断点识别方法 (breakpoint_threshold_type)如何定义“显著的语义跳跃”是语义分块的关键。SemanticChunker提供了几种基于统计的方法来识别断点percentile(百分位法 -默认方法):逻辑: 计算所有相邻句子的语义差异值并将这些差异值进行排序。当一个差异值超过某个百分位阈值时就认为该差异值是一个断点。参数:breakpoint_threshold_amount(默认为95)表示使用第95个百分位作为阈值。这意味着只有最显著的5%的语义差异点会被选为切分点。standard_deviation(标准差法):逻辑: 计算所有差异值的平均值和标准差。当一个差异值超过“平均值 N * 标准差”时被视为异常高的跳跃即断点。参数:breakpoint_threshold_amount(默认为3)表示使用3倍标准差作为阈值。interquartile(四分位距法):逻辑: 使用统计学中的四分位距IQR来识别异常值。当一个差异值超过Q3 N * IQR时被视为断点。参数:breakpoint_threshold_amount(默认为1.5)表示使用1.5倍的IQR。gradient(梯度法):逻辑: 这是一种更复杂的方法。它首先计算差异值的变化率梯度然后对梯度应用百分位法。对于那些句子间语义联系紧密、差异值普遍较低的文本如法律、医疗文档特别有效因为这种方法能更好地捕捉到语义变化的“拐点”。参数:breakpoint_threshold_amount(默认为95)。3.4 基于文档结构的分块对于具有明确结构标记的文档格式如Markdown、HTML、LaTex可以利用这些标记来实现更智能、更符合逻辑的分割。以 Markdown 结构分块为例针对结构清晰的 Markdown 文档利用其标题层级进行分块是一种高效且保留了丰富语义的方法。LangChain 提供了MarkdownHeaderTextSplitter来处理。实现原理: 该分块器的主要逻辑是“先按标题分组再按需细分”。定义分割规则: 用户首先需要提供一个标题层级的映射关系例如[ (#, Header 1), (##, Header 2) ]告诉分块器#是一级标题##是二级标题。内容聚合: 分块器会遍历整个文档将每个标题下的所有内容直到下一个同级或更高级别的标题出现前聚合在一起。每个聚合后的内容块都会被赋予一个包含其完整标题路径的元数据。元数据注入的优势: 这是此方法的主要特点。例如对于一篇关于机器学习的文章某个段落可能位于“第三章模型评估”下的“3.2节评估指标”中。经过分割后这个段落形成的文本块其元数据就会是{Header 1: 第三章模型评估, Header 2: 3.2节评估指标}。这种元数据为每个块提供了精确的“地址”极大地增强了上下文的准确性让大模型能更好地理解信息片段的来源和背景。局限性与组合使用: 单纯按标题分割可能会导致一个问题某个章节下的内容可能非常长远超模型能处理的上下文窗口。为了解决这个问题MarkdownHeaderTextSplitter可以与其它分块器如RecursiveCharacterTextSplitter组合使用。具体流程是第一步使用MarkdownHeaderTextSplitter将文档按标题分割成若干个大的、带有元数据的逻辑块。第二步对这些逻辑块再应用RecursiveCharacterTextSplitter将其进一步切分为符合chunk_size要求的小块。由于这个过程是在第一步之后进行的所有最终生成的小块都会继承来自第一步的标题元数据。RAG应用优势: 这种两阶段的分块方法既保留了文档的宏观逻辑结构通过元数据又确保了每个块的大小适中是处理结构化文档进行RAG的理想方案。四、其他开源框架中的分块策略4.1 Unstructured基于文档元素的智能分块Unstructured是一个强大的文档处理工具同样提供了实用的分块功能。1分区 (Partitioning): 这是一个重要功能负责将原始文档如PDF、HTML解析成一系列结构化的“元素”Elements。每个元素都带有语义标签如Title(标题)、NarrativeText(叙述文本)、ListItem(列表项) 等。这个过程本身就完成了对文档的深度理解和结构化。2分块 (Chunking): 该功能建立在分区的结果之上。分块功能不是对纯文本进行操作而是将分区产生的“元素”列表作为输入进行智能组合。Unstructured 提供了两种主要的分块方法basic: 这是默认方法。这种方法会连续地组合文档元素如段落、列表项直到达到max_characters上限尽可能地填满每个块。如果单个元素超过上限则会对其进行文本分割。by_title: 该方法在basic方法的基础上增加了对“章节”的感知。该方法将Title元素视为一个新章节的开始并强制在此处开始一个新的块确保同一个块内不会包含来自不同章节的内容。这在处理报告、书籍等结构化文档时非常有用效果类似于 LangChain 的MarkdownHeaderTextSplitter但适用范围更广。Unstructured 允许将分块作为分区的一个参数在单次调用中完成也支持在分区之后作为一个独立的步骤来执行分块。这种“先理解、后分割”的策略使得 Unstructured 能在最大程度上保留文档的原始语义结构特别是在处理版式复杂的文档时优势尤为明显。4.2 LlamaIndex面向节点的解析与转换LlamaIndex 将数据处理流程抽象为对“节点Node”的操作。文档被加载后首先会被解析成一系列的“节点”分块只是节点转换Transformation中的一环。LlamaIndex 的分块体系有以下特点1丰富的节点解析器 (Node Parser): LlamaIndex 提供了大量针对特定数据格式和方法的节点解析器可以大致分为几类结构感知型: 如MarkdownNodeParser,JSONNodeParser,CodeSplitter等能理解并根据源文件的结构如Markdown标题、代码函数进行切分。语义感知型:SemanticSplitterNodeParser: 与 LangChain 的SemanticChunker类似这种解析器使用嵌入模型来检测句子之间的语义“断点”在语义连续性明显减弱的地方切开从而让每个 chunk 内部尽量连贯。SentenceWindowNodeParser: 这是一种巧妙的方法。该方法将文档切分成单个的句子但在每个句子节点Node的元数据中会存储其前后相邻的N个句子即“窗口”。这使得在检索时可以先用单个句子的嵌入进行精确匹配然后将包含上下文“窗口”的完整文本送给LLM极大地提升了上下文的质量。常规型: 如TokenTextSplitter,SentenceSplitter等提供基于Token数量或句子边界的常规切分方法。2灵活的转换流水线: 用户可以构建一个灵活的流水线例如先用MarkdownNodeParser按章节切分文档再对每个章节节点应用SentenceSplitter进行更细粒度的句子级切分。每个节点都携带丰富的元数据记录着其来源和上下文关系。3良好的互操作性: LlamaIndex 提供了LangchainNodeParser可以方便地将任何 LangChain 的TextSplitter封装成 LlamaIndex 的节点解析器无缝集成到其处理流程中。4.3 ChunkViz简易的可视化分块工具在本文开头部分展示的分块图就是通过 ChunkViz 生成的。可以将你的文档、分块配置作为输入用不同的颜色块展示每个 chunk 的边界和重叠部分方便快速理解分块逻辑。