文本切分策略
High Contrast
Dark Mode
Light Mode
Sepia
Forest
2 min read496 words

文本切分策略

文本切分(Chunking)是 RAG 系统中最关键的步骤之一。切分策略直接影响检索精度和回答质量。

为什么需要切分?

graph TB A[为什么需要切分] --> B[Embedding 模型有 Token 限制] A --> C[长文本语义稀释] A --> D[LLM 上下文窗口有限] A --> E[检索精度要求] B --> B1[大多数模型限制 512-8192 tokens] C --> C1[过长的文本向量
无法准确表达局部语义] D --> D1[传入过多文本
增加成本和延迟] E --> E1[更小的块
更精确的匹配] style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px

切分策略对比

策略 原理 优点 缺点 推荐场景
固定大小 按字符数切分 简单、快速 可能截断句子 非结构化文本
句子切分 按句号切分 保持句子完整 块大小不均 短文档
段落切分 按段落切分 语义完整 段落大小差异大 博客、文章
递归切分 多级分隔符递归 灵活、语义好 实现较复杂 通用场景推荐
语义切分 按语义相似度 语义最佳 速度慢、成本高 高精度需求
结构化切分 按文档结构 利用文档格式 格式依赖 Markdown/HTML

实现各种切分策略

1. 固定大小切分

"""
固定大小切分器
最简单的切分方式,按字符数切分
"""
class FixedSizeChunker:
"""固定大小切分"""
def __init__(self, chunk_size: int = 500, overlap: int = 50):
"""
Args:
chunk_size: 每个块的最大字符数
overlap: 相邻块之间的重叠字符数
"""
self.chunk_size = chunk_size
self.overlap = overlap
def split(self, text: str) -> list[str]:
chunks = []
start = 0
while start < len(text):
end = start + self.chunk_size
# 如果不是最后一块,尝试在句号处截断
if end < len(text):
# 在 chunk_size 范围内找最后一个句号
last_period = text.rfind("。", start, end)
if last_period == -1:
last_period = text.rfind(".", start, end)
if last_period > start:
end = last_period + 1
chunks.append(text[start:end].strip())
start = end - self.overlap
return [c for c in chunks if c]  # 去除空块
# 使用
chunker = FixedSizeChunker(chunk_size=500, overlap=50)
text = "这是一段很长的文本..." * 100
chunks = chunker.split(text)
print(f"原文长度: {len(text)} 字符")
print(f"切分为 {len(chunks)} 个块")
print(f"平均块大小: {sum(len(c) for c in chunks) / len(chunks):.0f} 字符")

2. 递归字符切分(推荐)

"""
递归字符切分器(LangChain 默认策略)
按照多级分隔符递归切分,优先保持语义完整性
"""
class RecursiveChunker:
"""递归切分器"""
def __init__(
self,
chunk_size: int = 500,
overlap: int = 50,
separators: list[str] = None
):
self.chunk_size = chunk_size
self.overlap = overlap
# 分隔符优先级:段落 > 换行 > 句子 > 逗号 > 空格
self.separators = separators or [
"\n\n",   # 段落
"\n",     # 换行
"。",     # 中文句号
".",      # 英文句号
"!",     # 中文感叹号
"?",     # 中文问号
";",     # 中文分号
",",     # 中文逗号
",",      # 英文逗号
" ",      # 空格
"",       # 单字符
]
def split(self, text: str) -> list[str]:
"""递归切分文本"""
return self._split_text(text, self.separators)
def _split_text(self, text: str, separators: list[str]) -> list[str]:
"""递归切分核心逻辑"""
final_chunks = []
# 找到最合适的分隔符
separator = separators[-1]
for sep in separators:
if sep in text:
separator = sep
break
# 按分隔符切分
if separator:
splits = text.split(separator)
else:
splits = list(text)
# 合并小块
current_chunk = ""
for split in splits:
piece = split if not separator else split + separator
if len(current_chunk) + len(piece) <= self.chunk_size:
current_chunk += piece
else:
if current_chunk:
final_chunks.append(current_chunk.strip())
# 如果单个片段超过限制,递归处理
if len(piece) > self.chunk_size:
remaining_seps = separators[separators.index(separator) + 1:]
if remaining_seps:
sub_chunks = self._split_text(piece, remaining_seps)
final_chunks.extend(sub_chunks)
current_chunk = ""
else:
current_chunk = piece
else:
current_chunk = piece
if current_chunk.strip():
final_chunks.append(current_chunk.strip())
# 添加重叠
return self._add_overlap(final_chunks)
def _add_overlap(self, chunks: list[str]) -> list[str]:
"""为相邻块添加重叠"""
if self.overlap == 0 or len(chunks) <= 1:
return chunks
result = [chunks[0]]
for i in range(1, len(chunks)):
# 取上一块的末尾作为重叠
prev_tail = chunks[i - 1][-self.overlap:]
result.append(prev_tail + chunks[i])
return result
# 使用
chunker = RecursiveChunker(chunk_size=300, overlap=30)
text = """
# Python 设计模式
## 单例模式
单例模式确保一个类只有一个实例。这在数据库连接池、配置管理器等场景中非常有用。
实现方式有很多种:使用 __new__ 方法、使用装饰器、使用元类等。
## 工厂模式
工厂模式将对象的创建和使用分离。当你需要创建复杂对象时,工厂模式可以简化代码。
工厂模式分为简单工厂、工厂方法和抽象工厂三种。
""".strip()
chunks = chunker.split(text)
for i, chunk in enumerate(chunks):
print(f"\n--- 块 {i + 1} ({len(chunk)} 字符) ---")
print(chunk[:100] + "..." if len(chunk) > 100 else chunk)

3. Markdown 结构化切分

"""
Markdown 结构化切分器
利用 Markdown 的标题结构进行智能切分
"""
import re
class MarkdownChunker:
"""基于 Markdown 结构的切分器"""
def __init__(self, chunk_size: int = 1000, overlap: int = 0):
self.chunk_size = chunk_size
self.overlap = overlap
def split(self, text: str) -> list[dict]:
"""
按 Markdown 标题切分
返回包含元数据的切分结果
"""
# 按标题分割
sections = self._split_by_headers(text)
chunks = []
for section in sections:
content = section["content"]
if len(content) <= self.chunk_size:
chunks.append(section)
else:
# 超长段落用递归切分器处理
sub_chunker = RecursiveChunker(
chunk_size=self.chunk_size,
overlap=self.overlap
)
sub_chunks = sub_chunker.split(content)
for i, sub in enumerate(sub_chunks):
chunks.append({
"content": sub,
"headers": section["headers"],
"part": i + 1
})
return chunks
def _split_by_headers(self, text: str) -> list[dict]:
"""按标题层级分割文档"""
lines = text.split("\n")
sections = []
current_headers = {}
current_content = []
for line in lines:
header_match = re.match(r"^(#{1,6})\s+(.+)$", line)
if header_match:
# 保存当前段落
if current_content:
content = "\n".join(current_content).strip()
if content:
sections.append({
"content": content,
"headers": dict(current_headers)
})
current_content = []
# 更新标题层级
level = len(header_match.group(1))
title = header_match.group(2)
current_headers[f"h{level}"] = title
# 清除更低层级的标题
for l in range(level + 1, 7):
current_headers.pop(f"h{l}", None)
current_content.append(line)
else:
current_content.append(line)
# 最后一段
if current_content:
content = "\n".join(current_content).strip()
if content:
sections.append({
"content": content,
"headers": dict(current_headers)
})
return sections
# 使用
chunker = MarkdownChunker(chunk_size=500)
md_text = """
# RAG 系统指南
## 第一章:基础概念
### 什么是 RAG
RAG 是检索增强生成的缩写。它结合了信息检索和文本生成两种技术。
### 为什么需要 RAG
LLM 有知识截止日期的限制,RAG 可以解决这个问题。
## 第二章:实战
### 环境搭建
首先安装必要的依赖包。
""".strip()
chunks = chunker.split(md_text)
for i, chunk in enumerate(chunks):
print(f"\n--- 块 {i + 1} ---")
print(f"标题路径: {chunk['headers']}")
print(f"内容: {chunk['content'][:80]}...")

Chunk 大小选择

graph TB A{Chunk 大小选择} --> B[小块 100-300 字符] A --> C[中块 300-800 字符] A --> D[大块 800-2000 字符] B --> B1[精确匹配] B --> B2[适合问答] B --> B3[可能缺少上下文] C --> C1[平衡选择] C --> C2[适合多数场景] C --> C3[推荐起始值] D --> D1[保持完整性] D --> D2[适合长文摘要] D --> D3[检索精度可能下降] style C fill:#c8e6c9,stroke:#388e3c,stroke-width:3px style C3 fill:#c8e6c9,stroke:#388e3c,stroke-width:2px

经验法则

应用场景 推荐 Chunk 大小 重叠大小 理由
精确问答 200-400 字符 20-50 问题通常指向具体段落
通用知识库 400-800 字符 50-100 平衡精度和上下文
文章摘要 800-1500 字符 100-200 需要更完整的段落
代码文档 按函数/类切分 0 代码有天然边界
法律文档 按条款切分 50 条款是天然的语义单元

本章小结

下一章:我们将学习如何构建向量索引并存储到向量数据库中。