文本切分三大方法:固定窗口 / 语义切分 / 递归字符切分
适用场景:RAG 系统中的 Chunking 阶段。切分质量直接决定召回率与生成质量。
一、固定窗口切分(Fixed-Size Chunking)
1.1 原理
按预设的固定长度(如 500 token)对文本进行硬切,不考虑语义边界。最简单的切分方式。
1.2 实现方式
原文: "今天天气真好。我们去公园散步,看到了许多花朵盛开。"
窗口大小 = 10(字符)
切分结果:
Chunk1: "今天天气真好。我们去公"
Chunk2: "园散步,看到了许多花"
Chunk3: "朵盛开。"
1.3 优缺点
| 优点 | 缺点 |
|---|---|
| 实现极简,性能高 | 破坏语义完整性,句子可能被截断 |
| 切分结果可预测,便于调试 | 上下文割裂,影响 Embedding 质量 |
| 适合对延迟敏感的场景 | 长距离依赖信息丢失 |
1.4 适用场景
- 日志、二进制文本等无语义结构的数据
- 对延迟极端敏感、且对召回质量要求不高的场景
- 通常作为 baseline 对照方案
二、递归字符切分(Recursive Character Splitting) ⭐ 推荐
2.1 原理
LangChain 默认切分器。按一组优先级递减的分隔符递归切分:
- 先尝试用 "\n\n"(段落)切
- 切出的块仍太大 → 用 "\n"(行)切
- 还大 → 用 " "(空格)切
- 还大 → 用 ""(字符)硬切
2.2 核心代码逻辑
python
def recursive_split(text, separators, chunk_size):
for sep in separators:
if sep in text:
splits = text.split(sep)
# 合并 splits,直到累积长度 >= chunk_size
..
# 兜底:按字符硬切
return hard_cut(text, chunk_size)
默认分隔符列表(LangChain):
python
separators = ["\n\n", "\n", " ", ""]
2.3 切分示例
原文:
"## 标题1
这是第一段内容,讲的是 A 概念。
## 标题2
这是第二段内容,讲的是 B 概念。"
chunk_size = 30
切分过程:
1. 按 "\n\n" 切 → ["## 标题1\n这是第一段内容,讲的是 A 概念。", "## 标题2\n这是第二段内容,讲的是 B 概念。"]
2. 每段都 < 30,无需继续切
2.4 优缺点
| 优点 | 缺点 |
|---|---|
| 保留自然语义边界(段落/句子) | 仍是基于规则,无法感知深层语义 |
| 比固定窗口效果好,实现相对简单 | 切分粒度依赖分隔符选择 |
| LangChain 等框架开箱即用 | 对结构化文档(Markdown/LaTeX)支持一般 |
2.5 适用场景
- 通用文本的首选方案(新闻、博客、报告)
- Markdown / HTML 等半结构化文档
- 不具备 Embedding 计算条件、又想比固定窗口更好的场景
三、语义切分(Semantic Chunking)
3.1 原理
利用 Embedding 向量化句子,根据相邻句子的语义相似度动态决定切分点:
- 计算相邻句子(s₁, s₂)的余弦相似度
- 相似度低的位置 = 主题切换点 = 切分边界
- 相似度高的句子合并为同一 chunk
3.2 核心流程
输入文本
↓
按句子切分(s₁, s₂, s₃, .., sₙ)
↓
对每个句子 Embedding 得到 (v₁, v₂, .., vₙ)
↓
计算相邻相似度 sim(vᵢ, vᵢ₊₁)
↓
根据阈值或百分位切分
↓
合并语义相近的句子为 chunk
3.3 关键参数
- percentile_threshold:相似度低于此百分位的位置作为切分点(如 90 表示取相似度最低的 10% 位置切分)
- min_chunk_size:最小 chunk 大小,避免切出过短片段
3.4 代码示例(LangChain)
python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
text_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=90
)
chunks = text_splitter.split_text(text)
3.5 优缺点
| 优点 | 缺点 |
|---|---|
| 切分点 = 主题切换点,语义最连贯 | 计算成本高:每个句子都要 Embedding |
| 召回质量最优 | 切分速度慢,不适合大文档 |
| chunk 内部语义高度内聚 | 依赖 Embedding 模型质量 |
| chunk 大小不可控,可能过长或过短 |
3.6 适用场景
- 长文档(论文、法律文书、技术手册)
- 对召回精度要求极高的场景
- 预算允许且对延迟不敏感的场景
四、三者对比总结
| 维度 | 固定窗口 | 递归字符 | 语义切分 |
|---|---|---|---|
| 切分质量 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 切分速度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 实现复杂度 | 极低 | 中 | 高 |
| 语义保持 | 差 | 较好 | 优 |
| chunk 大小可控 | 完全可控 | 可控 | 不可控 |
| 计算成本 | 几乎为零 | 低 | 高(Embedding) |
| 框架支持 | 全部 | LangChain 等 | LangChain Experimental |
| 典型场景 | 基线/日志 | 通用首选 | 高精度检索 |
五、面试回答模板
问:RAG 中如何做文本切分?有哪几种方法?
答:常见三种切分方式,按推荐度排序:
- 递归字符切分(最常用):按 "\n\n → \n → 空格 → 字符" 的优先级递归切分,既能保持语义边界,又比纯固定窗口效果好,LangChain 默认方案。
- 语义切分(精度最高):用 Embedding 计算句子间相似度,在主题切换点切分,召回质量最好,但计算成本高,适合对精度要求高且预算允许的场景。
- 固定窗口切分(基线方案):按固定长度硬切,实现简单但会破坏语义,一般作为 baseline 或对延迟极端敏感时使用。
实际项目中,我会先用递归字符切分作为默认 ,再根据召回评估结果决定是否升级到语义切分。同时配合 chunk overlap(通常 10%-20%)缓解跨 chunk 上下文断裂问题。
六、延伸考点(配合 checklist 一起看)
- Chunk 大小选择:512 / 1024 token 是常见起点,需要根据 Embedding 模型的最长输入限制和下游 LLM 的上下文窗口共同决定。
- Overlap 的意义:避免关键信息正好落在切分边界,典型取 chunk 大小的 10%-20%。
- 不同文档类型的策略:
- PDF:按页 + 段落结构(MarkdownHeaderTextSplitter)
- Markdown:按标题层级切分
- 代码:按 AST(抽象语法树)切分,避免函数被截断