LangChain-TextSplitter文本分割策略详解
文档分块(chunking)是 RAG 系统里最容易被忽视、但对效果影响最大的环节之一。一个常见的误解是:换一个更好的 Embedding 模型可以解决检索效果差的问题。实际上,分块策略做得不好,再好的 Embedding 也无法弥补——把一段完整的解释劈成两半,或者把几千字的文档塞进一个 chunk,都会导致检索质量大幅下降。
LangChain TextSplitter 文本分割策略详解
文档分块(chunking)是 RAG 系统里最容易被忽视、但对效果影响最大的环节之一。一个常见的误解是:换一个更好的 Embedding 模型可以解决检索效果差的问题。实际上,分块策略做得不好,再好的 Embedding 也无法弥补——把一段完整的解释劈成两半,或者把几千字的文档塞进一个 chunk,都会导致检索质量大幅下降。
本章把 LangChain 主要的 TextSplitter 逐一拆解,并给出调参经验。
1.1 为什么分块策略影响 RAG 效果
四种文本分割策略对比——按字符、递归字符、语义分割、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 为止。
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 只用一个分隔符切,不递归。适合格式规整、段落分隔明确的文档:
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 不超过模型的处理限制:
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)。利用这个结构做分块,比按字符切分效果好得多:
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
### 在 Ubuntu 上安装
使用 apt 安装:
sudo apt-get install redis-server
## 基本命令
### 字符串操作
SET 和 GET 是最基础的命令:
SET key value
GET key
### 过期时间设置
使用 EXPIRE 命令设置键的过期时间(秒):
EXPIRE key 3600
"""
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 组合使用——先按标题切,再对太长的段落用递归字符分割:
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 计算相邻句子的语义相似度,相似的句子合并,语义转折处切断:
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 分块策略适用场景决策树
1.8 chunk_size 和 chunk_overlap 调参经验
没有放之四海而皆准的参数,但有一些经验值可以作为起点:
# 各场景推荐参数(中文,按字符数)
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 对比四种分块策略的效果
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 小结
分块策略的选择优先级:
- 有 Markdown/HTML 标题结构 → MarkdownHeaderTextSplitter + RecursiveCharacterTextSplitter 组合
- 通用文档 → RecursiveCharacterTextSplitter,这是 99% 场景的合理选择
- 需要精确 token 控制 → TokenTextSplitter
- 语义质量优先且预算允许 → SemanticChunker
chunk_size 从 500 开始,用评估集测试,根据数据调整,而不是盲目套用别人的参数。
下一篇是 LangChain 系列的终章:Agent 完整实战,从工具定义到完整可部署的 Agent,包含天气查询、搜索、计算三个工具的真实案例。