文档分块策略:Recursive、Semantic 与 Agentic 三大范式实战

RAG 的"上游"决定"下游"的天花板。系统对比固定大小、Recursive、Semantic、Agentic 四种分块策略,给出可量化的选型决策、Chonkie / LlamaIndex / LangChain 工具链对比和离线评估方法。

AgentList · 2026年7月1日
RAG文档分块ChunkingEmbeddingLangChain

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)

实现原理

  1. 把文档按句子拆开
  2. 计算相邻句子的 embedding 余弦相似度
  3. 找到相似度的"断崖点"(breakpoint)
  4. 在断崖点处切分

优势

  • 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)覆盖了分块工具链的核心节点。