文档分块策略:Recursive、Semantic 与 Agentic 三大范式实战
RAG 的"上游"决定"下游"的天花板。系统对比固定大小、Recursive、Semantic、Agentic 四种分块策略,给出可量化的选型决策、Chonkie / LlamaIndex / LangChain 工具链对比和离线评估方法。
RAG 系统的"上游"决定了"下游"的天花板。检索质量再高,如果文档在切块阶段就破坏了语义边界,再先进的 embedding + Reranker 也救不回来。本文从工程实战出发,系统对比三种主流分块策略——固定大小、递归分割、语义分块——并介绍最新的 Agentic Chunking 范式,给出可量化的选型决策。
为什么分块是 RAG 的隐形瓶颈
RAG 的标准流程是:文档 → 切块 → embedding → 检索 → 答案生成。这个流程里"切块"看似最简单、最不起眼,但它直接决定了下游所有环节的输入质量。
最常见的失败模式:
- 关键信息被切断:表格在 chunk N 结束、表头在 chunk N-1,模型看不到完整表格
- 语义单元被破坏:一个完整的论述被强行从中间切开,两半都失去上下文
- 噪声污染:单个 chunk 包含 3 个不相关主题,embedding 把它映射到一个不聚焦的向量
- 过度切分:1000 tokens 的文档被切成 20 个 50-token 的小块,检索时只能命中半句话
更严重的是,这些错误是"沉默"的——你的评估指标可能显示 RAG 准确率只有 60%,但你以为是 embedding 或 LLM 的问题,花几周调 embedding 模型,结果发现根本原因是分块策略选错了。
策略 1:固定大小分块(Fixed-Size Chunking)
最朴素的方法:每 N tokens 切一刀,N 通常是 256、512、1024。
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separator="\n\n",
)
chunks = text_splitter.split_text(document)
优点:
- 实现最简单,几十行代码
- chunk 大小均匀,embedding 不会因为长度差异产生质量波动
- 性能可预测:单文档的 chunk 数量 = 总长度 / chunk_size
缺点:
- 强行按字符/字数切断,不考虑语义边界
- 表头、代码块、列表项经常被切到不同 chunk
- 长文档的"概念连续性"被破坏——第三段讲的内容和第二段讲的内容被切分到两个 chunk 后,各自都失去上下文
适合场景:
- 内部知识库是结构化、格式统一的技术文档
- 极简原型阶段,先跑通再优化
策略 2:递归分割(Recursive Chunking)
LangChain 默认推荐的分块策略。先按段落切,段落太长就按句子切,句子还长就按词切——逐级降级:
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""],
length_function=len,
)
chunks = text_splitter.split_text(document)
关键设计:
- 分隔符优先级:
\n\n>\n>。>>""——优先按最大的语义边界切 - 中英文混合分隔符:上面示例同时支持中文和英文
- chunk_size 单位:默认是字符数,不是 tokens。如果按 tokens 算,需要用
tiktoken等库自定义length_function
优势:
- 兼顾语义完整性和大小控制
- 段落、句子结构被尽量保留
- 性能与固定大小相当,开销极小
劣势:
- 对超长段落无能为力(整个段落就是一个 chunk)
- 对表格、代码块、列表项没有特殊处理
- chunk 之间的边界仍然是启发式规则
适合场景:
- 大多数通用文档(产品文档、博客文章、FAQ)
- 不想花太多精力调分块参数的项目
策略 3:语义分块(Semantic Chunking)
基于 embedding 相似度动态切块:当相邻句子的语义相似度低于阈值时,认为这里是一个"概念切换"点。
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker(
OpenAIEmbeddings(),
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=80, # 相似度低于 80% 分位数时切分
)
chunks = text_splitter.split_text(document)
实现原理:
- 把文档按句子拆开
- 计算相邻句子的 embedding 余弦相似度
- 找到相似度的"断崖点"(breakpoint)
- 在断崖点处切分
优势:
- chunk 边界对齐语义边界
- 概念连续的段落被合并到同一个 chunk
- 章节切换、主题转换被正确识别
劣势:
- 计算成本高(每个句子都要过 embedding)
- 阈值难以调优
- 短文档(<10 个句子)效果差
- 对 embedding 模型质量敏感
适合场景:
- 高质量、深度技术文档
- 召回率敏感的 RAG 系统
- 可以接受额外 embedding 成本
策略 4:Agentic Chunking(最新范式)
2024 年底开始流行的"由 LLM 决定如何切块"的方法。让一个 LLM 读取整个文档,输出"哪些段落应该被合并到一个 chunk":
from openai import OpenAI
client = OpenAI()
def agentic_chunk(document: str, max_chunk_size: int = 800) -> list[str]:
prompt = f"""You are a document chunking specialist. Your job is to split the
following document into semantically coherent chunks for a RAG system.
Rules:
1. Each chunk should cover ONE concept or topic
2. Do not split mid-sentence
3. Do not split tables, code blocks, or list items
4. If a paragraph is self-contained, keep it as one chunk
5. Maximum chunk size: approximately {max_chunk_size} characters
6. Output the chunks as a JSON array of strings
Document:
{document}
Output (JSON array of strings):"""
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
import json
result = json.loads(response.choices[0].message.content)
return result["chunks"]
优势:
- 真正"理解"文档结构——表格、代码块、列表项保持完整
- 可以结合上下文判断边界
- 适合复杂结构(带图表的研究论文、法律合同)
劣势:
- 成本高(每篇文档都要 LLM 调用)
- 速度慢(一次 LLM 调用 2-5s)
- 不可重现(LLM 输出有随机性)
- 难以并行化(需要整个文档的全局视角)
适合场景:
- 文档量小但价值高(法律合同、医疗指南)
- 一次性离线批处理,非实时系统
- 预算允许的场景
四种策略对比
| 维度 | 固定大小 | 递归分割 | 语义分块 | Agentic |
|---|---|---|---|---|
| 速度 | 最快 | 快 | 中 | 慢 |
| 成本 | 几乎为零 | 几乎为零 | 中(每句 embedding) | 高(每文档 LLM) |
| 语义边界 | 差 | 中 | 好 | 优 |
| 表格/代码处理 | 差 | 差 | 中 | 优 |
| 调参难度 | 低 | 低 | 中 | 高 |
| 可重现 | 完全 | 完全 | 完全 | 弱 |
Chonkie:分块库的事实标准
如果你不想自己实现分块,Chonkie 是当前最快的选择:
from chonkie import RecursiveChunker, SemanticChunker, TokenChunker
# Token-based 分块
chunker = TokenChunker(chunk_size=512, chunk_overlap=50)
chunks = chunker.chunk(text)
# Recursive
chunker = RecursiveChunker(chunk_size=512)
chunks = chunker.chunk(text)
# Semantic
from sentence_transformers import SentenceTransformer
embedder = SentenceTransformer("BAAI/bge-m3")
chunker = SemanticChunker(embedder=embedder, chunk_size=512)
chunks = chunker.chunk(text)
# 输出包含 metadata
for chunk in chunks:
print(f"Text: {chunk.text[:80]}")
print(f"Tokens: {chunk.token_count}")
print(f"Start: {chunk.start_index}, End: {chunk.end_index}")
print("---")
Chonkie 相对 LangChain 的优势:快 5-10 倍(Rust 核心)、更轻量、API 更现代。
LlamaIndex 的 SentenceSplitter
LlamaIndex 提供细粒度的分块控制:
from llama_index.core.node_parser import SentenceSplitter, SemanticSplitterNodeParser
# 按句子分块
parser = SentenceSplitter(chunk_size=512, chunk_overlap=50)
nodes = parser.get_nodes_from_documents(documents)
# 语义分块
from llama_index.embeddings.openai import OpenAIEmbedding
embed_model = OpenAIEmbedding()
parser = SemanticSplitterNodeParser(
embed_model=embed_model,
breakpoint_percentile_threshold=80,
)
nodes = parser.get_nodes_from_documents(documents)
选型决策
默认从 Recursive 开始。大多数项目不需要分块策略上的"完美",Recursive 在 80% 的场景下都够用。把精力放在 embedding 选型、检索策略上。
什么时候升级到 Semantic:
- Recursive 切出来的 chunk 经常切断概念
- 召回率(recall@10)明显低于预期
- 文档主题切换频繁(技术手册、政策文件)
什么时候升级到 Agentic:
- 文档是高度结构化(合同、论文)
- 召回质量直接决定业务结果(医疗、法律)
- 文档量不大(每天 < 1000 份),可以承担 LLM 成本
什么时候必须用 Fixed:
- 极简原型阶段
- 文档库内容高度同质(每个文档结构都一样)
离线评估
不要凭感觉选分块策略——用评估集量化:
from langchain_community.embeddings import HuggingFaceEmbeddings
from qdrant_client import QdrantClient
embedder = HuggingFaceEmbeddings(model_name="BAAI/bge-m3")
client = QdrantClient(":memory:")
def evaluate_chunking(strategy, eval_set):
metrics = {"recall_at_5": 0, "mrr": 0}
for item in eval_set:
chunks = strategy(item["document"])
# 入库
client.add("eval", chunks, item["document_id"])
# 检索
results = client.search(item["query"], k=5)
result_ids = [r.id for r in results]
if any(rid in item["relevant_doc_ids"] for rid in result_ids):
metrics["recall_at_5"] += 1
for i, rid in enumerate(result_ids):
if rid in item["relevant_doc_ids"]:
metrics["mrr"] += 1 / (i + 1)
break
return {k: v / len(eval_set) for k, v in metrics.items()}
# 对比不同策略
strategies = {
"fixed_500": lambda doc: fixed_chunk(doc, 500),
"recursive_500": lambda doc: recursive_chunk(doc, 500),
"semantic_500": lambda doc: semantic_chunk(doc, 500),
}
for name, fn in strategies.items():
metrics = evaluate_chunking(fn, eval_set)
print(f"{name}: Recall@5={metrics['recall_at_5']:.3f}, MRR={metrics['mrr']:.3f}")
典型结果:
- Fixed: Recall@5 = 0.55
- Recursive: Recall@5 = 0.68
- Semantic: Recall@5 = 0.78
- Agentic: Recall@5 = 0.85(成本高 10x)
每次分块策略升级前,都先看评估集的指标变化。
实施路径
第 1 周:先用 Recursive 切块(chunk_size=512, overlap=50),跑通基本 RAG 链路。第 2 周:构建 100 条 query 的评估集,量化基线 recall@10。第 3 周:尝试 Semantic 切块,对比指标。第 4 周:如果 Semantic 提升明显(>+10%),正式切换;否则优化 embedding 和检索。第 5 周:对核心文档(如政策、产品手册)做 Agentic 切块,建立"高质量分块"单独管线。第 6 周:建立分块质量的离线评估 CI。
总结
分块策略是 RAG 系统的"地基"——选错了再花哨的 embedding + Reranker 也救不回来。从 Recursive 开始,把评估集建好,根据数据驱动的指标决定是否升级到 Semantic 或 Agentic。不要凭感觉调 chunk_size——把每一次分块参数调整都当作一次 A/B 实验,用评估集数据说话。
参考工具:Chonkie(最快的 Rust-based 分块库)、LlamaIndex(细粒度分块节点解析器)、LangChain(Recursive / Semantic 分块器)、txtai(内置 chunking 的端到端 RAG)和 EmbedAnything(多模态 embedding 框架,支持自定义 chunking)覆盖了分块工具链的核心节点。
本文涉及的项目
Langchain-Chatchat
38.4k ⭐Langchain-Chatchat 是一个基于 Langchain 和多种大语言模型的本地知识库 RAG 与 Agent 应用平台,支持 ChatGLM、Qwen、Llama 等模型,提供对话、知识库管理、Agent 调用等功能。
Chonkie
4.5k ⭐轻量级文档分块库,专为快速、高效和稳健的 RAG 管道设计,支持多种分块策略和嵌入模型,显著提升检索增强生成效果。
txtai
12.7k ⭐集成语义搜索、LLM 编排和语言模型工作流的全能 AI 框架,支持 Agent、RAG 和向量数据库
EmbedAnything
1.3k ⭐EmbedAnything 是一个用 Rust 构建的高性能嵌入推理和索引框架,提供模块化、内存安全的 RAG 数据摄取和索引管道,支持本地和云端部署。