课程0基础Agent开发课 / LangChain / LangChain-TextSplitter文本分割策略详解
— 17 min read

LangChain-TextSplitter文本分割策略详解

文档分块(chunking)是 RAG 系统里最容易被忽视、但对效果影响最大的环节之一。一个常见的误解是:换一个更好的 Embedding 模型可以解决检索效果差的问题。实际上,分块策略做得不好,再好的 Embedding 也无法弥补——把一段完整的解释劈成两半,或者把几千字的文档塞进一个 chunk,都会导致检索质量大幅下降。

LangChain TextSplitter 文本分割策略详解

文档分块(chunking)是 RAG 系统里最容易被忽视、但对效果影响最大的环节之一。一个常见的误解是:换一个更好的 Embedding 模型可以解决检索效果差的问题。实际上,分块策略做得不好,再好的 Embedding 也无法弥补——把一段完整的解释劈成两半,或者把几千字的文档塞进一个 chunk,都会导致检索质量大幅下降。

本章把 LangChain 主要的 TextSplitter 逐一拆解,并给出调参经验。

1.1 为什么分块策略影响 RAG 效果

Markdown分割 MarkdownHeader

• 按标题层级(# ## ###)
• 保留章节结构信息
• 元数据含标题路径

适用:文档知识库 / 结构化检索

语义分割 Semantic

• 基于句向量相似度
• 合并相似语义块
• 动态块大小
• 计算开销较大

适用:语义完整 / 适合问答RAG

递归字符分割 RecursiveCharacter

• 依次尝试多种分隔符
• \
\
→ \
→ 空格 → 字符
• 保留段落完整性

适用:最常用 / 平衡效果与速度

按字符分割 CharacterTextSplitter

• 按单一分隔符
• 如 \
\

• 固定长度
• 可能截断句子中间

适用:简单快速 / 适合结构化文本

四种文本分割策略对比——按字符、递归字符、语义分割、Markdown分割的特点与适用场景

两个极端情况说明问题:

chunk 太大(如 2000 token):一个 chunk 包含多个主题,embedding 是整段内容的平均语义,对单一问题的检索精度差。模型收到的 context 里噪音多,生成质量下降。

chunk 太小(如 50 token):一句话一个 chunk,上下文被切断,检索到的片段缺乏完整意思,模型缺少足够信息来回答。

分块的核心目标是:每个 chunk 语义完整,且大小在 Embedding 模型和 LLM context 的最优处理范围内

实践中,对于大多数中文技术文档,chunk_size=500-800 token,chunk_overlap=50-100 token 是一个不错的起点。

1.2 RecursiveCharacterTextSplitter:最常用的选择

RecursiveCharacterTextSplitter 是 LangChain 推荐的默认分块器,原理是:按分隔符优先级递归切分,先尝试用段落分隔符,切出来的 chunk 还是太大就用句子分隔符,还太大就用词……直到 chunk 小于 chunk_size 为止。

python
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 默认分隔符优先级(中文场景建议自定义)
# 英文默认:["\n\n", "\n", " ", ""]
# 中文场景推荐:
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,       # 每个 chunk 的最大字符数
    chunk_overlap=50,     # 相邻 chunk 的重叠字符数(保证上下文连续性)
    separators=[
        "\n\n",   # 段落分隔(最优先)
        "\n",     # 换行
        "。",     # 中文句号
        "!",     # 中文感叹号
        "?",     # 中文问号
        ";",     # 中文分号
        ",",     # 中文逗号(最后手段)
        "",       # 字符级切分(兜底)
    ],
    length_function=len,  # 用字符数计长度,也可以换成 token 计数函数
)

text = """
Redis 是一个开源的内存数据库,支持多种数据结构。它的主要特点包括:

一、速度极快
Redis 将数据存储在内存中,读写延迟通常在微秒级别。相比于磁盘数据库,速度提升可达10倍以上。

二、数据持久化
尽管是内存数据库,Redis 支持 RDB 快照和 AOF 日志两种持久化方式,重启后数据不会丢失。

三、丰富的数据类型
Redis 支持 String、List、Hash、Set、ZSet 五种基本类型,还有 Bitmap、HyperLogLog 等高级类型。
"""

chunks = splitter.create_documents([text])
print(f"切出 {len(chunks)} 个 chunk:")
for i, chunk in enumerate(chunks):
    print(f"\n--- Chunk {i+1} ({len(chunk.page_content)} 字符) ---")
    print(chunk.page_content)

chunk_overlap 的作用是保证语义连续性——如果某个重要信息恰好在两个 chunk 的边界,重叠部分保证两个 chunk 都包含足够的上下文来理解这个信息。

1.3 CharacterTextSplitter:按单一分隔符切分

CharacterTextSplitter 只用一个分隔符切,不递归。适合格式规整、段落分隔明确的文档:

python
from langchain_text_splitters import CharacterTextSplitter

splitter = CharacterTextSplitter(
    separator="\n\n",   # 只按双换行(段落)切分
    chunk_size=500,
    chunk_overlap=50,
    length_function=len,
)

# 如果某个段落本身超过 chunk_size,CharacterTextSplitter 不会再细分
# 这是和 RecursiveCharacterTextSplitter 的主要区别
chunks = splitter.create_documents([text])

适用场景:FAQ 文档(问题和答案之间用双换行分隔),每个 QA 对作为一个完整 chunk,不希望被进一步切碎。

1.4 TokenTextSplitter:按 Token 数精确控制

LLM 的 context 限制是按 token 计算的,不是按字符。用字符数作为 chunk_size 时,同样500字符的中英文,token 数量差距可能很大(中文一个字约1.5-2 token,英文一个词约1.3 token)。

TokenTextSplitter 直接按 token 数切,保证每个 chunk 不超过模型的处理限制:

python
from langchain_text_splitters import TokenTextSplitter

# 使用 tiktoken(OpenAI 开源的 token 计数工具)计算 token 数
splitter = TokenTextSplitter(
    encoding_name="cl100k_base",  # GPT-4 / text-embedding-3-small 使用的编码
    chunk_size=300,    # 最多 300 个 token 每个 chunk
    chunk_overlap=30,
)

# 也可以按模型名自动选择编码
splitter_by_model = TokenTextSplitter.from_tiktoken_encoder(
    model_name="gpt-4o",
    chunk_size=300,
    chunk_overlap=30,
)

chunks = splitter.create_documents([text])
print(f"切出 {len(chunks)} 个 chunk")

适用场景:需要精确控制 context 大小,或者文档内容中英文混杂时。代价是依赖 tiktoken 库(OpenAI 开源的 token 计数工具),且切分不考虑语义边界(可能在句子中间切断)。

1.5 MarkdownHeaderTextSplitter:保留 Markdown 结构

技术文档、Wiki 通常是 Markdown 格式,有清晰的标题层级(H1、H2、H3)。利用这个结构做分块,比按字符切分效果好得多:

python
from langchain_text_splitters import MarkdownHeaderTextSplitter

# 指定哪些标题级别作为分割点,并把标题内容放入 metadata
headers_to_split_on = [
    ("#", "h1_title"),    # 一级标题内容存入 metadata["h1_title"]
    ("##", "h2_title"),   # 二级标题内容存入 metadata["h2_title"]
    ("###", "h3_title"),
]

splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=headers_to_split_on,
    # strip_headers=True 表示 chunk 内容不包含标题行本身
    strip_headers=False,
)

markdown_text = """
# Redis 入门指南

## 安装与配置

### 在 macOS 上安装

使用 Homebrew 安装 Redis:

brew install redis
brew services start redis

code

### 在 Ubuntu 上安装

使用 apt 安装:

sudo apt-get install redis-server

code

## 基本命令

### 字符串操作

SET 和 GET 是最基础的命令:

SET key value
GET key

code

### 过期时间设置

使用 EXPIRE 命令设置键的过期时间(秒):

EXPIRE key 3600

code
"""

chunks = splitter.split_text(markdown_text)
for chunk in chunks:
    print(f"元数据:{chunk.metadata}")
    print(f"内容:{chunk.page_content[:100]}")
    print("---")
# 输出示例:
# 元数据:{'h1_title': 'Redis 入门指南', 'h2_title': '安装与配置', 'h3_title': '在 macOS 上安装'}
# 内容:使用 Homebrew 安装 Redis:...

标题信息存入 metadata 之后,检索时可以用来做过滤("只搜安装相关的内容"),或者在返回结果时显示来源章节。

通常把 MarkdownHeaderTextSplitter 和 RecursiveCharacterTextSplitter 组合使用——先按标题切,再对太长的段落用递归字符分割:

python
from langchain_text_splitters import MarkdownHeaderTextSplitter, RecursiveCharacterTextSplitter

# 第一步:按标题切分
header_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[("#", "h1"), ("##", "h2"), ("###", "h3")],
)
header_chunks = header_splitter.split_text(markdown_text)

# 第二步:对太长的 chunk 进一步切分
char_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)
final_chunks = char_splitter.split_documents(header_chunks)
# split_documents 会保留原有的 metadata(h1、h2、h3 标题信息)

1.6 SemanticChunker:语义分块

前面几种 Splitter 都是基于结构(字符数、token 数、分隔符)切分,不考虑语义。SemanticChunker 用 Embedding 计算相邻句子的语义相似度,相似的句子合并,语义转折处切断:

python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
import os

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-small",
    api_key=os.getenv("OPENAI_API_KEY"),
)

# breakpoint_threshold_type 决定如何判断"语义转折"
# "percentile":相似度低于第 X 百分位时切断(默认)
# "standard_deviation":相似度低于均值减去 X 个标准差时切断
# "interquartile":使用四分位数判断
splitter = SemanticChunker(
    embeddings=embeddings,
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95,  # 相似度低于第95百分位才切断,较保守
)

text = """
Python 是一种解释型编程语言,以简洁易读著称。它支持面向对象、函数式等多种编程范式。

Redis 是一个内存数据库,常用于缓存和消息队列。它的数据结构丰富,支持原子操作。

Python 中使用 redis-py 库可以连接 Redis。安装方式是 pip install redis。
连接后可以使用 set 和 get 方法操作字符串类型的键值对。
"""

# 注意:SemanticChunker 会调用 Embedding API,有额外成本
chunks = splitter.create_documents([text])
# 预期:第一个 chunk 关于 Python,第二个关于 Redis,第三个是两者的结合
for i, chunk in enumerate(chunks):
    print(f"Chunk {i+1}{chunk.page_content[:80]}...")

SemanticChunker 的代价是需要调用 Embedding API,分块本身有成本。对于大型文档库,这个成本不可忽视。适合文档内容语义跳跃频繁、结构分隔符不明确的场景。

1.7 分块策略适用场景决策树

Markdown/HTML

无结构

是,需卡 LLM 限制

是,质量优先

否,通用场景

是,FAQ/QA 格式

否,普通文章

需要分块的文档

文档有结构化标题?

MarkdownHeaderTextSplitter
先按标题切

需要精确控制 token 数?

切后 chunk 还太长?

+RecursiveCharacterTextSplitter
进一步细分

完成

TokenTextSplitter

文档语义跳跃频繁
无明显分隔符?

SemanticChunker
有额外 API 成本

段落结构规整?

CharacterTextSplitter
按段落

RecursiveCharacterTextSplitter
默认选择

1.8 chunk_size 和 chunk_overlap 调参经验

没有放之四海而皆准的参数,但有一些经验值可以作为起点:

python
# 各场景推荐参数(中文,按字符数)
CHUNK_CONFIGS = {
    # 密集技术文档(API 文档、代码注释)
    # chunk 小,精确检索
    "technical_dense": {"chunk_size": 300, "chunk_overlap": 30},

    # 普通技术文章(博客、教程)
    # 平衡精度和上下文完整性
    "technical_article": {"chunk_size": 500, "chunk_overlap": 50},

    # 长篇报告(年报、白皮书)
    # chunk 大,保留更多上下文
    "long_report": {"chunk_size": 800, "chunk_overlap": 100},

    # QA 对话记录(客服日志、FAQ)
    # 每个 QA 对保持完整
    "qa_pairs": {"chunk_size": 1000, "chunk_overlap": 0},
}

调参的正确方式:建立评估集(20-50 个问答对),对不同分块参数分别建立向量库,用命中率(Hit Rate)和 MRR(Mean Reciprocal Rank,平均倒数排名:衡量检索结果中正确答案出现位置的指标,排名越靠前分数越高)评估检索质量,选择效果最好的参数,而不是凭感觉调。

1.9 对比四种分块策略的效果

python
import os
from langchain_text_splitters import (
    RecursiveCharacterTextSplitter,
    CharacterTextSplitter,
    TokenTextSplitter,
    MarkdownHeaderTextSplitter,
)
from langchain_openai import OpenAIEmbeddings

sample_text = """
# 数据库索引原理

## B+ 树索引

B+ 树是最常用的数据库索引结构。它是 B 树的变体,所有数据都存储在叶子节点,叶子节点之间通过指针连接,形成有序链表,非常适合范围查询。

B+ 树的每个节点对应一个磁盘页,通常为 16KB。节点内存储多个键值,这种设计减少了磁盘 I/O 次数——一次读取可以获得多个键值,降低了树的高度。

## 哈希索引

哈希索引通过哈希函数直接定位数据,等值查询的时间复杂度为 O(1),比 B+ 树快得多。

但哈希索引不支持范围查询,也不支持前缀匹配。MySQL 的 InnoDB 引擎不支持手动创建哈希索引,但有自适应哈希索引(Adaptive Hash Index)功能。
"""

# 各分块器测试
splitters = {
    "Recursive": RecursiveCharacterTextSplitter(
        chunk_size=200, chunk_overlap=20,
        separators=["\n\n", "\n", "。", ",", ""],
    ),
    "Character": CharacterTextSplitter(
        separator="\n\n", chunk_size=200, chunk_overlap=20,
    ),
    "Token": TokenTextSplitter(
        encoding_name="cl100k_base", chunk_size=80, chunk_overlap=10,
    ),
    "MarkdownHeader": None,  # 特殊处理
}

print("=" * 60)
for name, splitter in splitters.items():
    if name == "MarkdownHeader":
        header_splitter = MarkdownHeaderTextSplitter(
            headers_to_split_on=[("#", "h1"), ("##", "h2")]
        )
        chunks = header_splitter.split_text(sample_text)
    else:
        chunks = splitter.create_documents([sample_text])

    print(f"\n[{name}] 切出 {len(chunks)} 个 chunk:")
    for i, chunk in enumerate(chunks):
        content_preview = chunk.page_content[:60].replace("\n", "↵")
        meta = chunk.metadata if chunk.metadata else {}
        print(f"  Chunk{i+1} ({len(chunk.page_content)}字符): {content_preview}... | meta={meta}")
    print("-" * 40)

1.10 小结

分块策略的选择优先级:

  1. 有 Markdown/HTML 标题结构 → MarkdownHeaderTextSplitter + RecursiveCharacterTextSplitter 组合
  2. 通用文档 → RecursiveCharacterTextSplitter,这是 99% 场景的合理选择
  3. 需要精确 token 控制 → TokenTextSplitter
  4. 语义质量优先且预算允许 → SemanticChunker

chunk_size 从 500 开始,用评估集测试,根据数据调整,而不是盲目套用别人的参数。

下一篇是 LangChain 系列的终章:Agent 完整实战,从工具定义到完整可部署的 Agent,包含天气查询、搜索、计算三个工具的真实案例。

本页目录