LangChain 三大核心切分策略与高频考点

Loading...

Text splitter integrations - Docs by LangChain

一句话总结:文本切分器(Text Splitter)是 RAG 系统中用于解决模型上下文窗口受限的核心组件,它通过基于文本自然结构、物理长度或特定文档格式语义树(Markdown/JSON/HTML/代码)的三大核心策略,将超长语料安全截断为易于向量化和高信噪比检索的小文本块。


三大核心切分策略与 API 解析

官方文档明确指出了切分文档的三种核心策略,它们分别对应了不同的业务场景与数据容错级别:

策略一:基于文本结构切分 (Text structure-based)

  • 核心理念:利用人类自然语言固有的层级结构(段落 -> 句子 -> 单词)来指导切分,尽最大努力保持局部语义的连贯性。
  • 代表 APIRecursiveCharacterTextSplitter(递归字符切分器)
    • 它是 LangChain 官方最推荐的默认策略。底层默认按 ["\n\n", “\n”, " “, “”] 顺序尝试。
    • 如果一个段落(\n\n 分隔)小于目标尺寸,就整体保留;一旦超标,才会递归降级到按句子甚至单词切分,从而避免将完整的句子生硬拦腰截断。

策略二:基于长度切分 (Length-based)

  • 核心理念:简单粗暴的硬性截断,确保每个产出的文本块绝对不会超出设定的物理长度限制。它不关心自然语言的段落完整性,只关注底层数据的吞吐合规性。
  • 子分类与代表 API
    • 基于字符长度:使用 CharacterTextSplitter,通过设定特定的分隔符(默认是双换行)和严格的字符数量进行截断。
    • 基于 Token 长度:由于大模型(LLM)的底层计费和输入限制均基于 Token,通常配合 tiktoken 编码器使用(调用 CharacterTextSplitter.from_tiktoken_encoder)。它能精准算出 Token 消耗并以此为边界进行切割。

策略三:基于文档结构切分 (Document structure-based)

  • 核心理念:针对具备天然语法树或层级结构的半结构化文件(如 HTML、Markdown、JSON、代码),单纯按换行或长度切分会破坏其原有的逻辑组装。此策略能够感知文件结构,并在切分时保留父子层级关系,提取为元数据(Metadata)。
  • 代表 API
  1. MarkdownHeaderTextSplitter(Markdown 标题切分器):通过识别 #, ## 等标题,切分 Markdown,并将标题提取为上下文字段。
  2. RecursiveJsonSplitter(递归 JSON 切分器):智能识别 JSON 的 Object 或 Array 边界,保证切分出的块依然具备合法的 JSON 子结构特征。
  3. Code Splitter(语言感知切分器):通过例如 RecursiveCharacterTextSplitter.from_language(language=Language.PYTHON) 初始化。底层内置了对特定编程语言(如 Python、Go、JS 等)的抽象语法树(AST)规则。优先保证函数等代码逻辑块的完整性,避免代码被生硬切断。
  4. HTML Splitter(HTML 切分器)
    • HTMLHeaderTextSplitter(HTML 标题文本切分器):基于 HTML 的标题标签(<h1><h6>)来切分文本。它的核心逻辑和 Markdown 里的标题切分一样,主要用来提取并保留文章的“层级目录”作为元数据(Metadata)。
    • HTMLSectionSplitter(HTML 区块切分器):基于 HTML 的布局结构(如 <section><div>)或视觉特征(如字体大小)来切分文本。它更适合处理那些大段大段排版、没有明显 h1/h2 标签,但是通过 div 块进行视觉隔离的现代网页。
    • HTMLSemanticPreservingSplitter(HTML 语义保持切分器):“Semantic Preserving” 意为“保持语义完整”。它的核心是为了防止暴力切分把原本一体的结构(比如一个完整的 <table> 数据表、一段 <ul> 列表、甚至一段 <code> 代码块)从中间拦腰斩断,导致大模型读取时上下文错乱。

注意:

在构建生产级别的 RAG(检索增强生成)系统时,单一的切分策略往往无法兼顾“语义完整性”和“长度安全性”。业界最标准、最经典的最佳实践,就是将 “基于文档结构” 作为第一阶段(粗排提取),将 “基于文本结构与长度” 作为第二阶段(精细兜底)。


周边与核心参数梳理

在执行上述三大策略时,LangChain 提供了两个极其核心的参数来控制切分质量(这不仅是 API 参数,更是 RAG 调优的灵魂):

  • chunk_size切分块的物理容量上限。在基于长度的策略中,它可能是字符数,也可能是 Token 数;它直接决定了最终入库时每个向量(Embedding)所能承载的信息密度。
  • chunk_overlap相邻文本块之间的重叠容量。它采用了类似数据流处理中的“滑动窗口”思想。在物理切断文本时,强制保留一部分上一块的尾部数据,从而防止上下文语义在切割边界处发生断裂。

工程化代码落地示例

在面试或实际编写数据管道时,建议展示出对 chunk_size(分块大小)和 chunk_overlap(重叠区域)的控制。重叠区域是为了防止切分动作刚好把一个核心概念从中间斩断,导致上下文丢失。

示例 1:一个处理本地高隐私知识库的标准切分范例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Author         : nanzet
# Description    : 演示如何在 RAG 系统中使用两阶段文本切分策略,结合 `MarkdownHeaderTextSplitter` 和 `RecursiveCharacterTextSplitter`,实现既保留文档结构又控制块大小的高效文本预处理流程。

from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)

# 模拟一篇本地知识库中,带有层级结构的 Markdown 长文本
markdown_doc = """
# RAG 系统重构指南

## Local-First 本地化架构设计
为了保障核心技术文档的绝对隐私,我们需要构建本地化的知识库基础设施。
(这里假设有 2000 字的详细架构图文字描述...)

## 向量检索的痛点
在传统的文本匹配中,我们经常遇到以下问题:
第一,语义鸿沟...
第二,多语言对齐...
"""

# ==========================================
# 阶段 1:文档结构切分 (MarkdownHeaderTextSplitter)
# 目标:保持章节完整,并将标题层级提取为高价值的 Metadata
# ==========================================
headers_to_split_on = [
    ("#", "H1_Title"),
    ("##", "H2_Title"),
]
md_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)

# 切分后,你会得到按标题划分的块,并且每个块都携带了诸如 {"H2_Title": "1. Local-First 本地化架构设计"} 这样的元数据
md_header_splits = md_splitter.split_text(markdown_doc)


# ==========================================
# 阶段 2:长度兜底切分 (RecursiveCharacterTextSplitter)
# 目标:防止阶段 1 切出的块过长导致 OOM 或 Token 溢出,进行安全的二级截断
# ==========================================
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  # 设定 LLM 能够舒服吞下的分块大小
    chunk_overlap=50,  # 截断处的语义缓冲区域
)

# 核心工程思维:将阶段 1 的输出对象 (包含 metadata 的 Document 列表)
# 直接透传给阶段 2 的 split_documents 方法
final_splits = text_splitter.split_documents(md_header_splits)

print(f"最终经过两级管道切分出的 Chunk 数量: {len(final_splits)}")
# final_splits 中的每个 chunk 现在既有完美的大小控制,又完整继承了它的祖先标题 Metadata!
print("示例 Chunk Metadata:", final_splits[0].metadata)
print(
    "示例 Chunk 内容:", final_splits[0].page_content[:200], "..."
)  # 只展示前 200 字以示例

输出结果:

1
2
3
4
最终经过两级管道切分出的 Chunk 数量: 2
示例 Chunk Metadata: {'H1_Title': 'RAG 系统重构指南', 'H2_Title': '1. Local-First 本地化架构设计'}
示例 Chunk 内容: 为了保障核心技术文档的绝对隐私我们需要构建本地化的知识库基础设施
(这里假设有 2000 字的详细架构图文字描述...) ...

示例 2:日常开发中最常用的三种切分器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Author         : nanzet
# Description    : RAG数据流:三种核心文本切分策略的最佳实践(Recursive, Token-based, Markdown-based)
# requirements   : pip install langchain-text-splitters tiktoken

import tiktoken
from langchain_text_splitters import (
    MarkdownHeaderTextSplitter,
    RecursiveCharacterTextSplitter,
)


def demonstrate_recursive_splitter() -> None:
    """
    演示 RecursiveCharacterTextSplitter(递归字符文本切分器)的使用。

    该切分器是 LangChain 官方推荐的默认选择。它通过递归地尝试不同的分隔符
    (段落 -> 句子 -> 单词),在满足 chunk_size 限制的前提下,尽最大努力保持自然语言的语义完整性。
    """
    print("=== 1. 默认首选:RecursiveCharacterTextSplitter (递归语义切分) ===")
    document = "这是第一段话,非常重要。\n\n这是第二段话,包含了长句子。下周天气不错,我打算和几个朋友开车去湖边露营。\n\n下午我得把特斯拉开去楼下充下电,不然晚上回家就没电了。"

    # 这里的大小指的是“字符数”
    splitter = RecursiveCharacterTextSplitter(chunk_size=30, chunk_overlap=10)
    chunks = splitter.split_text(document)

    print(f"切分结果数量: {len(chunks)}")
    for i, chunk in enumerate(chunks):
        print(f"Chunk {i + 1} 内容: {chunk}")
        print(f"Chunk {i + 1} 长度: {len(chunk)} 字符\n")


def demonstrate_token_based_splitter() -> None:
    """
    演示基于 Token 的生产级切分策略。

    【最佳实践】:使用 RecursiveCharacterTextSplitter 配合 tiktoken_encoder。
    既能严格保证产出的 Chunk Token 数量不超过阈值,又能在切分时尽量保持句子的语义完整,避免出现生硬的截断。
    """
    print("=== 2. 精准控制:基于 Token 的降级切分 (Recursive + tiktoken) ===")
    document = "这是第一段话,非常重要。\n\n这是第二段话,包含了长句子。下周天气不错,我打算和几个朋友开车去湖边露营。\n\n下午我得把特斯拉开去楼下充下电,不然晚上回家就没电了。"

    encoding_name = "cl100k_base"
    encoder = tiktoken.get_encoding(encoding_name)

    # 【核心调整】:弃用死板的 CharacterTextSplitter,换用带递归降级能力的 Recursive
    # 这里的 chunk_size=20 和 chunk_overlap=5,指的都是 Token 数量,而非字符!
    splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
        encoding_name=encoding_name, chunk_size=20, chunk_overlap=5
    )

    chunks = splitter.split_text(document)
    print(f"切分结果数量: {len(chunks)}\n")

    for i, chunk in enumerate(chunks):
        tokens = encoder.encode(chunk)
        print(f"Chunk {i + 1} 内容: {chunk}")
        print(f"Chunk {i + 1} 长度: {len(chunk)} 字符 | Token 数量: {len(tokens)}\n")


def demonstrate_markdown_splitter() -> None:
    """
    演示基于 Markdown 文档结构的感知切分策略。

    此策略不仅会将文本按结构块切分,还会自动提取 Markdown 的层级标题(Header),
    并将其作为 Metadata(元数据)注入到每个 Chunk 中,极大增强向量检索的准确度溯源能力。
    """
    print("=== 3. 结构化感知:MarkdownHeaderTextSplitter ===")
    md_document = """
# 部门规章制度
## 考勤管理
早上 9 点打卡。
迟到扣钱。
## 财务报销
每月 10 号前提交发票。
    """
    headers_to_split_on = [
        ("#", "一级标题"),
        ("##", "二级标题"),
    ]

    splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
    splits = splitter.split_text(md_document)

    print(f"切分结果数量: {len(splits)}")
    for i, split in enumerate(splits):
        print(f"Chunk {i + 1} 内容: {split.page_content}")
        print(f"Chunk {i + 1} Metadata: {split.metadata}\n")


if __name__ == "__main__":
    demonstrate_recursive_splitter()
    demonstrate_token_based_splitter()
    demonstrate_markdown_splitter()

输出结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
=== 1. 默认首选RecursiveCharacterTextSplitter (递归语义切分) ===
切分结果数量: 4
Chunk 1 内容: 这是第一段话非常重要
Chunk 1 长度: 12 字符

Chunk 2 内容: 这是第二段话包含了长句子下周天气不错我打算和几个朋友
Chunk 2 长度: 29 字符

Chunk 3 内容:我打算和几个朋友开车去湖边露营
Chunk 3 长度: 18 字符

Chunk 4 内容: 下午我得把特斯拉开去楼下充下电不然晚上回家就没电了
Chunk 4 长度: 27 字符

=== 2. 精准控制基于 Token 的降级切分 (Recursive + tiktoken) ===
切分结果数量: 6

Chunk 1 内容: 这是第一段话非常重要
Chunk 1 长度: 12 字符 | Token 数量: 12

Chunk 2 内容: 这是第二段话包含了长句子下周天
Chunk 2 长度: 17 字符 | Token 数量: 18

Chunk 3 内容:下周天气不错我打算和几个朋友
Chunk 3 长度: 17 字符 | Token 数量: 20

Chunk 4 内容: 个朋友开车去湖边露营
Chunk 4 长度: 11 字符 | Token 数量: 16

Chunk 5 内容: 下午我得把特斯拉开去楼下充下电
Chunk 5 长度: 15 字符 | Token 数量: 19

Chunk 6 内容: 下充下电不然晚上回家就没电了
Chunk 6 长度: 16 字符 | Token 数量: 18

=== 3. 结构化感知MarkdownHeaderTextSplitter ===
切分结果数量: 2
Chunk 1 内容: 早上 9 点打卡
迟到扣钱
Chunk 1 Metadata: {'一级标题': '部门规章制度', '二级标题': '考勤管理'}

Chunk 2 内容: 每月 10 号前提交发票
Chunk 2 Metadata: {'一级标题': '部门规章制度', '二级标题': '财务报销'}

常见踩坑与高频面试点

在 RAG 系统的开发实践中,深入理解这三大策略的局限性并做出正确架构选型,是高级研发岗位的必考内容。

高频面试点 1:为何必须进行文本切分?如何设定合理的 Chunk Size 与 Overlap?

  • 考察点:对向量空间特征密度及上下文边界处理的理解。
  • 满分回答:切分的根本目的不仅是为了规避大模型上下文窗口的上限,更关键的是提升检索的信噪比(Precision/Recall)。长文本被压缩进单一稠密向量后,局部的关键语义特征会被稀释,导致向量相似度匹配精度大幅下降。设置 chunk_overlap(滑动窗口重叠,业内推荐占比通常为 15% 到 25%)是为了防止自然语义在切片物理边界处发生硬性断裂,确保模型接收到的上下文能够平滑衔接,避免断章取义。

常见踩坑 1:基于 Length-based 策略控制 Token 时的失效陷阱

  • 痛点场景:开发者为严格限制 Token 数,使用 CharacterTextSplitter.from_tiktoken_encoder 处理大段无换行的密集文本,结果控制台抛出 Created a chunk of size X, which is longer than the specified Y 的警告,并且重叠区(Overlap)完全失效。
  • 工程对策CharacterTextSplitter 采用单一分隔符(如 \n\n)的硬性逻辑,当遇到无分段的超长句子时,它不具备降级能力,只能强行输出超标块。在生产环境中,需要严格对齐 Token 时,最佳实践是改用 RecursiveCharacterTextSplitter.from_tiktoken_encoder。它既具备精确的 Token 计量能力,又保留了向下递归(按句子、单词)的灵活性,彻底杜绝该问题。

常见踩坑 2:多语言语料库的 Token 膨胀现象

  • 痛点场景:处理中文文档时,将 chunk_size 设为 500,预期切出约 500 个汉字,但在调用云端计费接口时发现消耗了超 1000 个 Token。
  • 工程对策:在底层 BPE(字节对编码)算法下,英文单词与 Token 大致是 1 比 1.3 的关系;但对于中文字符,因 UTF-8 编码机制,一个汉字往往会被切分为 2 到 3 个 Token。因此,在基于长度切分(Length-based)处理纯中文语料时,绝对不能将字符长度等同于 Token 数量,必须显式调用相应的 Tokenizer(如 tiktoken 或开源模型的专属编码器)进行强校验计算。

常见踩坑 3:Document structure-based 数据降级时的“范围幻觉”

  • 痛点场景:使用基础的文本结构策略(Recursive)处理包含多级标题的长 Markdown 文档。切分后,位于末尾的文本块彻底丢失了所属的章节大标题上下文,导致大模型读取该片段时出现严重的回答范围错误(幻觉)。
  • 工程对策:不能用通用的纯文本切分策略去破坏结构化文件。必须引入 MarkdownHeaderTextSplitter 等文档结构感知组件。它会在物理切分的同时,向上溯源并提取各级标题信息,将其注入到输出 Document 的 Metadata(元数据字典)中。这不仅防止了上下文遗失,更为下游向量数据库的元数据过滤(Metadata Filtering)提供了关键架构支撑。
使用 Hugo 构建
主题 StackJimmy 设计