纲要
- 文档切分在 RAG 系统中的核心作用与必要性
- 四种主流切分策略的剖析与选型
- 基于长度切分:固定 Token 数分割
- 基于文本架构切分:保留段落与句子边界
- 基于文档格式切分:解析
Markdown、JSON等结构化数据 - 基于语义切分:利用模型判定语义边界(实验性)
LangChain切分器组件速览CharacterTextSplitter:基础字符切分RecursiveCharacterTextSplitter:递归式通用切分器MarkdownHeaderTextSplitter:保留标题元数据的 Markdown 切分器SemanticChunker(实验性):基于嵌入向量的语义切分
- 完整可运行示例:使用
RecursiveCharacterTextSplitter进行本地文档切分 - 切分器选型建议与最佳实践
引言
在构建检索增强生成(RAG)应用时,文档加载与向量化存储之间,存在一个关键且影响深远的环节:文本切分。切分策略的优劣直接决定了后续向量检索的命中精度,以及大语言模型所能获取的上下文质量。若切分粒度过粗,文本片段可能超出模型的上下文窗口,导致关键信息被截断;若粒度过细,语义完整性遭到破坏,模型将难以理解上下文的连贯含义,进而产生不准确的回答。
LangChain 作为构建 LLM 应用的主流框架,提供了一套层次化、可组合的文本切分工具集。从最直接的按字符长度切割,到基于文档结构的分割,再到引入模型判定边界的智能切分,开发者可以根据实际场景灵活选用。本文将深入剖析四种核心切分策略的实现原理与适用场景,并通过可直接运行的代码示例,展示如何在本地环境中高效实施文档切分。
为什么必须切分文档?
在 RAG 流水线中,对原始文档进行切分并非可有可无的步骤,而是由技术约束与业务需求共同决定的必要操作:
- 模型上下文窗口限制:无论是 GPT-4 还是各类开源大语言模型,其上下文窗口(Context Window)均有最大 Token 数限制。长文档(如报告、书籍)必须被拆分为多个片段,方可被模型有效处理。
- 检索精准度提升:将长文档分割为语义相对独立的段落或句子后,向量相似性搜索能够更精准地定位与用户查询最相关的信息片段,而非返回包含大量无关噪声的整页内容。
- 嵌入成本与效果控制:嵌入模型通常按 Token 计费或受限于最大输入长度。合理的片段大小不仅能够控制成本,还能确保生成的嵌入向量更聚焦于特定主题,提升检索效果。
四种切分策略详解
基于长度切分
这是最直观的切分方式,即指定一个固定的 Token 数或字符数作为切分阈值,按照该长度对文本进行分割,并允许相邻片段之间保留一定比例的重叠内容,以防止关键信息在分割点被意外截断。
CharacterTextSplitter 配合 tiktoken 分词器即可实现此类切分。其核心参数包括:
chunk_size:每个片段的最大长度(以 Token 或字符计)。chunk_overlap:相邻片段之间的重叠长度。encoding_name:指定使用的分词编码(如cl100k_base对应GPT-4所使用的编码)。
from langchain_text_splitters import CharacterTextSplitter
# 使用 tiktoken 编码器进行切分
splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=100,
chunk_overlap=20
)
注意:该方式实现简单,但完全忽略文本的自然边界(如句子、段落),容易在句中进行切割,破坏语义连贯性。仅适用于对语义要求极低的场景,或作为其他切分策略的兜底方案。
基于文本架构切分
RecursiveCharacterTextSplitter 是 LangChain 官方推荐的通用切分器,它采用递归尝试的策略来平衡长度限制与语义完整性。其工作流程如下:
- 首先按优先级最高的分隔符(如双换行符
\n\n)尝试分割。 - 若分割后的片段仍超过
chunk_size,则自动降级使用下一个优先级的分隔符(如单换行符\n、句号、逗号等)继续分割。 - 重复此过程,直至所有片段长度符合要求。
这种递归机制使得切分结果最大程度地保留了段落和句子的自然边界,是一种兼顾长度约束与语义保留的折中方案,适用于绝大多数通用文本。
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
separators=["\n\n", "\n", "。", ",", " ", ""], # 按优先级从高到低排列
keep_separator=False # 是否在结果中保留分隔符
)
基于文档格式切分
对于 Markdown、JSON 等具有明确结构特征的文档,利用文档本身的标记进行切分能够获得更好的语义完整性。
- Markdown 文档:
MarkdownHeaderTextSplitter根据标题层级(#、##、###等)对文档进行拆分,并将各级标题作为元数据附加到每个片段中,便于在检索时提供层次化的上下文信息。 - JSON 数据:
JsonSplitter能够按 JSON 对象或数组元素进行拆分,确保每个片段仍为合法的 JSON 结构。
from langchain_text_splitters import MarkdownHeaderTextSplitter
markdown_doc = """
# 第一章
这是第一章的内容...
## 第一节
第一节的详细描述...
"""
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
splits = splitter.split_text(markdown_doc)
基于语义切分(实验性)
SemanticChunker 是 LangChain 中处于实验阶段的切分器,它尝试从根本上解决语义完整性问题。其核心机制是:首先将文本拆分为单个句子,然后计算相邻句子嵌入向量之间的余弦相似度,当相似度低于预设阈值时,判定此处为语义边界,执行切分。
这种方式能够最大程度地保证每个片段在语义上独立且完整,非常适合对切分质量要求极高的场景。然而,由于需要调用外部嵌入模型进行计算,其处理速度较慢且会产生额外成本。目前该功能尚处于实验阶段,API 在未来版本中可能发生变化。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
# 需要配置 OpenAI API Key
embeddings = OpenAIEmbeddings()
splitter = SemanticChunker(
embeddings=embeddings,
breakpoint_threshold_type="percentile", # 可选: "percentile", "standard_deviation", "interquartile"
breakpoint_threshold_amount=95 # 百分位数阈值
)
完整可运行代码:使用 RecursiveCharacterTextSplitter 本地演示
以下示例使用 RecursiveCharacterTextSplitter 对一份模拟的产品手册进行切分,并详细输出每个切分后片段的长度与内容。该过程完全本地运行,无需调用任何外部 API。
环境准备
pip install langchain langchain-text-splitters
核心代码实现
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
def main():
# 1. 构造示例文档,模拟真实产品手册
raw_text = """
产品名称:智能温控水杯 X-200
功能描述:
1. 实时显示水温,精确到 ±0.5°C。
2. 支持手机 App 远程设置目标温度。
3. 内置 5000mAh 电池,可续航 48 小时。
4. 采用 316 不锈钢内胆,安全健康。
5. 具有过热保护功能,超过 99°C 自动断电。
使用说明:
- 首次使用前请用清水冲洗内胆。
- 长按电源键 3 秒开机,蓝牙自动配对。
- 可通过 App 调节温度范围:40°C~100°C。
注意事项:
- 请勿将杯体浸入水中清洗。
- 充电时请使用 5V/2A 适配器。
"""
doc = Document(
page_content=raw_text.strip(),
metadata={"source": "product_manual", "version": "v1.0"}
)
# 2. 创建切分器实例,配置关键参数
splitter = RecursiveCharacterTextSplitter(
chunk_size=100, # 每段最大字符数
chunk_overlap=20, # 段间重叠字符数,用于维持上下文连贯
separators=["\n\n", "\n", "。", ",", " ", ""],
keep_separator=False,
length_function=len, # 默认使用 len() 计算长度,也可替换为 Token 计数器
)
# 3. 执行切分并展示结果
split_docs = splitter.split_documents([doc])
print(f"原始文档长度:{len(raw_text)} 字符,成功切分为 {len(split_docs)} 个片段。\n")
for i, d in enumerate(split_docs, 1):
print(f"--- 片段 {i}(字符数:{len(d.page_content)})---")
print(d.page_content)
print(f"元数据:{d.metadata}\n")
if __name__ == "__main__":
main()
代码运行说明
- 确保 Python 环境版本不低于 3.9。
- 执行
pip install langchain langchain-text-splitters安装所需依赖。 - 将上述代码保存为
split_demo.py文件,并在终端中运行python split_demo.py。 - 观察输出结果,可见原始文本已按自然段落边界切分,每个片段的长度基本控制在 100 字符以内,且相邻片段之间保留了适当的重叠字符(如“实时显示水温”等上下文),有效维持了语义的连贯性。
切分器选型建议与实践指南
| 应用场景 | 推荐切分器 | 关键考量因素 |
|---|---|---|
| 通用文本、快速原型验证 | RecursiveCharacterTextSplitter | 适用性最广,平衡长度与语义,为多数场景的默认首选。 |
| 高度结构化文档(Markdown) | MarkdownHeaderTextSplitter | 保留标题层次结构,便于在检索时利用层级元数据进行筛选。 |
| 结构化数据(JSON) | JsonSplitter | 严格遵循数据结构,确保每个切分片段的合法性。 |
| 需极高质量语义边界 | SemanticChunker(实验性) | 利用嵌入模型判定语义边界,但需承担额外计算成本与 API 延迟。 |
此外,在配置 chunk_size 与 chunk_overlap 时,需综合考虑以下因素:
- 嵌入模型的最大输入长度:
chunk_size不应超过所选嵌入模型的max_seq_length。 - 下游 LLM 的上下文窗口:片段长度应远小于模型窗口限制,并为系统指令和用户问题预留空间。
- 数据集的平均段落长度:观察目标文档的段落结构,使切分后的片段尽可能与自然段落对齐。
API 速览
本节对涉及的核心切分器 API 进行梳理,以便开发者快速查阅。
1. CharacterTextSplitter
- 所属库:
langchain_text_splitters - 方法签名:
from_tiktoken_encoder(cls, encoding_name: str, chunk_size: int, chunk_overlap: int, **kwargs)split_documents(documents: List[Document]) -> List[Document]
- 核心参数:
encoding_name:tiktoken编码名称,如cl100k_base。chunk_size:最大长度(以 Token 计)。chunk_overlap:重叠长度。
- 返回值:
List[Document],每个Document对象包含切分后的文本及其元数据。
2. RecursiveCharacterTextSplitter
- 所属库:
langchain_text_splitters - 方法签名:
__init__(self, chunk_size: int, chunk_overlap: int, separators: List[str], keep_separator: bool = False, length_function: Callable = len, **kwargs)split_documents(documents: List[Document]) -> List[Document]
- 核心参数:
separators:按优先级从高到低排列的分隔符列表。length_function:计算片段长度的函数,默认为len,可替换为 Token 计数器。
- 返回值:
List[Document]。
3. MarkdownHeaderTextSplitter
- 所属库:
langchain_text_splitters - 方法签名:
__init__(self, headers_to_split_on: List[Tuple[str, str]], **kwargs)split_text(text: str) -> List[Document]
- 核心参数:
headers_to_split_on:列表,每个元素为(HTML标签, 元数据键名)的元组。
- 返回值:
List[Document],每个片段的metadata中包含对应的标题层级信息。
4. SemanticChunker
- 所属库:
langchain_experimental.text_splitter - 方法签名:
__init__(self, embeddings: Embeddings, breakpoint_threshold_type: str, breakpoint_threshold_amount: float, **kwargs)split_documents(documents: List[Document]) -> List[Document]
- 核心参数:
embeddings:嵌入模型实例,需实现embed_documents和embed_query方法。breakpoint_threshold_type:阈值类型,可选percentile、standard_deviation、interquartile。
- 返回值:
List[Document]。
完整 Demo 示例
以下提供一个端到端的 HTML 可视化演示页面,通过模拟文本输入实时展示 RecursiveCharacterTextSplitter 的切分效果。该 Demo 为独立前端应用,无需后端服务。
运行说明
- 将以下 HTML 代码保存为
splitter_demo.html文件。 - 使用现代浏览器(如 Chrome、Firefox)直接打开该文件。
- 在文本框中输入或修改待切分的文本,调整
chunk_size与chunk_overlap滑块。 - 实时观察切分结果,每个片段将以独立卡片形式呈现。
代码说明
- 核心逻辑:使用纯 JavaScript 模拟
RecursiveCharacterTextSplitter的递归分割算法,依据预定义的分隔符优先级对输入文本进行切分。 - 交互设计:提供滑块和实时反馈,帮助开发者直观理解不同参数对切分结果的影响。
- 技术点:展示了字符级递归分割的算法实现,以及前端事件驱动模型的基本应用。
技术点总结
- 递归分隔符优先级处理逻辑的实现。
- 滑块控件与文本渲染的动态绑定。
- 实时可视化反馈机制。
Demo 代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RecursiveCharacterTextSplitter 可视化演示</title>
<style>
* {
box-sizing: border-box;
font-family: system-ui, -apple-system, sans-serif;
}
body {
background: #f7f9fc;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
max-width: 960px;
width: 100%;
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
h1 {
font-size: 1.6rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: #0b1a33;
}
.sub {
color: #5e6f8d;
font-size: 0.95rem;
margin-bottom: 2rem;
border-left: 4px solid #3b82f6;
padding-left: 1rem;
}
.config {
display: flex;
flex-wrap: wrap;
gap: 2rem;
background: #f0f4fe;
padding: 1.2rem 1.5rem;
border-radius: 12px;
margin-bottom: 1.5rem;
}
.config-item {
flex: 1 1 160px;
}
.config-item label {
display: block;
font-weight: 500;
font-size: 0.85rem;
color: #1e293b;
margin-bottom: 4px;
}
.config-item input[type="range"] {
width: 100%;
accent-color: #3b82f6;
}
.config-item .value {
font-size: 0.9rem;
font-weight: 600;
color: #0b1a33;
float: right;
}
textarea {
width: 100%;
min-height: 140px;
padding: 0.8rem 1rem;
border: 1px solid #d1d9e8;
border-radius: 10px;
font-size: 0.95rem;
line-height: 1.6;
resize: vertical;
font-family: inherit;
margin-bottom: 1.5rem;
transition: border 0.2s;
}
textarea:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59,130,246,0.15);
}
.stats {
display: flex;
justify-content: space-between;
font-size: 0.9rem;
color: #334155;
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e9edf4;
}
.chunks-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.chunk-card {
background: #fafcff;
border: 1px solid #e2e9f3;
border-radius: 10px;
padding: 0.8rem 1rem;
transition: all 0.1s;
}
.chunk-card:hover {
border-color: #b0c8e8;
background: #ffffff;
}
.chunk-header {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
font-weight: 500;
color: #62748c;
border-bottom: 1px dashed #dce3ef;
padding-bottom: 4px;
margin-bottom: 6px;
}
.chunk-content {
font-size: 0.9rem;
line-height: 1.5;
word-break: break-word;
color: #0a1a2f;
}
.empty {
color: #94a3b8;
font-style: italic;
padding: 1rem 0;
}
.tag {
background: #dbeafe;
color: #1e4b8a;
padding: 0.15rem 0.6rem;
border-radius: 20px;
font-size: 0.75rem;
}
@media (max-width: 600px) {
body { padding: 1rem; }
.container { padding: 1rem; }
.config { flex-direction: column; gap: 0.6rem; }
}
</style>
</head>
<body>
<div class="container">
<h1>📄 递归字符切分器 · 实时演示</h1>
<div class="sub">基于优先级分隔符递归分割,保持语义边界 | 完全前端模拟</div>
<div class="config">
<div class="config-item">
<label>📏 chunk_size <span class="value" id="sizeVal">100</span></label>
<input type="range" id="chunkSize" min="20" max="300" value="100" step="5">
</div>
<div class="config-item">
<label>🔗 chunk_overlap <span class="value" id="overlapVal">20</span></label>
<input type="range" id="chunkOverlap" min="0" max="80" value="20" step="5">
</div>
<div class="config-item" style="flex: 0 0 auto;">
<label>分隔符优先级</label>
<span style="font-size:0.8rem; color:#334155;">\n\n → \n → 。 → , → 空格</span>
</div>
</div>
<textarea id="textInput" placeholder="在此输入或粘贴待切分的文本...">产品名称:智能温控水杯 X-200
功能描述:
1. 实时显示水温,精确到 ±0.5°C。
2. 支持手机 App 远程设置目标温度。
3. 内置 5000mAh 电池,可续航 48 小时。
4. 采用 316 不锈钢内胆,安全健康。
5. 具有过热保护功能,超过 99°C 自动断电。
使用说明:
- 首次使用前请用清水冲洗内胆。
- 长按电源键 3 秒开机,蓝牙自动配对。
- 可通过 App 调节温度范围:40°C~100°C。
注意事项:
- 请勿将杯体浸入水中清洗。
- 充电时请使用 5V/2A 适配器。</textarea>
<div class="stats">
<span>📝 原始字符数: <strong id="origLen">0</strong></span>
<span>🧩 切分片段: <strong id="chunkCount">0</strong></span>
<span>⚡ 总字符数 (含重叠): <strong id="totalLen">0</strong></span>
</div>
<div id="chunksContainer" class="chunks-grid"></div>
</div>
<script>
(function() {
const textInput = document.getElementById('textInput');
const chunkSizeSlider = document.getElementById('chunkSize');
const chunkOverlapSlider = document.getElementById('chunkOverlap');
const sizeVal = document.getElementById('sizeVal');
const overlapVal = document.getElementById('overlapVal');
const origLen = document.getElementById('origLen');
const chunkCount = document.getElementById('chunkCount');
const totalLen = document.getElementById('totalLen');
const container = document.getElementById('chunksContainer');
// 模拟 RecursiveCharacterTextSplitter 的核心逻辑
function recursiveSplit(text, chunkSize, chunkOverlap, separators) {
if (!text || text.length === 0) return [];
// 如果文本长度 <= chunkSize,直接返回
if (text.length <= chunkSize) {
return [text];
}
// 尝试按当前分隔符列表分割
for (let sep of separators) {
if (sep === '') continue; // 跳过空分隔符(兜底逻辑单独处理)
const parts = text.split(sep);
if (parts.length > 1) {
// 检查分割后的每个部分,如果都 <= chunk_size,则递归处理每个部分
let allFit = true;
for (let p of parts) {
if (p.length > chunkSize) {
allFit = false;
break;
}
}
if (allFit) {
// 递归处理每个部分,注意重叠逻辑:此处简化,仅对每个部分单独切分
let result = [];
for (let p of parts) {
if (p.length === 0) continue;
// 若 p 仍大于 chunk_size(理论上不会,但防御)
if (p.length > chunkSize) {
result = result.concat(recursiveSplit(p, chunkSize, chunkOverlap, separators));
} else {
result.push(p);
}
}
return result;
}
}
}
// 兜底:按字符切分(对应 separators 中的 "")
let chunks = [];
let start = 0;
while (start < text.length) {
let end = Math.min(start + chunkSize, text.length);
let chunk = text.substring(start, end);
chunks.push(chunk);
start = end - chunkOverlap;
if (start <= 0) start = end;
if (start >= text.length) break;
}
return chunks;
}
function updateDisplay() {
const raw = textInput.value;
const chunkSize = parseInt(chunkSizeSlider.value, 10);
const chunkOverlap = parseInt(chunkOverlapSlider.value, 10);
sizeVal.textContent = chunkSize;
overlapVal.textContent = chunkOverlap;
// 定义分隔符优先级
const separators = ['\n\n', '\n', '。', ',', ' ', ''];
// 执行切分
const chunks = recursiveSplit(raw, chunkSize, chunkOverlap, separators);
// 更新统计
origLen.textContent = raw.length;
chunkCount.textContent = chunks.length;
const totalChars = chunks.reduce((sum, c) => sum + c.length, 0);
totalLen.textContent = totalChars;
// 渲染
if (chunks.length === 0) {
container.innerHTML = '<div class="empty">暂无切分结果,请在上方输入文本。</div>';
return;
}
let html = '';
chunks.forEach((chunk, idx) => {
const isOverlap = (idx > 0 && chunk.length > 0);
html += `
<div class="chunk-card">
<div class="chunk-header">
<span>#${idx + 1}</span>
<span>${chunk.length} 字符 ${isOverlap ? '🔁' : ''}</span>
<span class="tag">${chunk.length <= chunkSize ? '✓' : '⚠️'}</span>
</div>
<div class="chunk-content">${escapeHtml(chunk)}</div>
</div>
`;
});
container.innerHTML = html;
}
// 简单防 XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 事件绑定
textInput.addEventListener('input', updateDisplay);
chunkSizeSlider.addEventListener('input', updateDisplay);
chunkOverlapSlider.addEventListener('input', updateDisplay);
// 初始渲染
updateDisplay();
})();
</script>
</body>
</html>
参考文档
官方文档
LangChain文本分割器官方指南:https://python.langchain.com/docs/modules/data_connection/document_transformers/RecursiveCharacterTextSplitterAPI 参考:https://api.python.langchain.com/en/latest/text_splitters/langchain_text_splitters.base.RecursiveCharacterTextSplitter.htmlSemanticChunker实验性文档:https://python.langchain.com/docs/modules/data_connection/document_transformers/semantic-chunker
参考链接
tiktoken编码器说明:https://github.com/openai/tiktokenLangChain社区关于切分策略的最佳实践讨论:https://github.com/langchain-ai/langchain/discussions
总结
文档切分作为 RAG 流水线中的关键预处理步骤,直接决定了检索质量与生成效果的上限。RecursiveCharacterTextSplitter 以其递归尝试的机制,在通用文本场景中提供了最佳的长度控制与语义平衡,成为绝大多数应用的首选。
对于高度结构化的 Markdown 或 JSON 文档,选用 MarkdownHeaderTextSplitter 与 JsonSplitter 能够更充分地利用结构信息。而实验性的 SemanticChunker 则代表了未来的发展方向——通过引入模型推理来精准划定语义边界,尽管目前存在性能开销,但对于追求极致质量的应用具有重要价值。
合理配置切分参数(chunk_size 与 chunk_overlap),并结合具体业务场景进行调优,是构建高性能 RAG 应用的基础工程实践。
30



被折叠的 条评论
为什么被折叠?



