为什么分块策略如此重要?
在RAG系统中,分块(Chunking)是影响检索质量的第一道关卡。同样的文档、同样的Embedding模型、同样的LLM,仅仅因为分块策略不同,检索准确率可以相差30%以上。
核心矛盾在于:块太大,检索精度下降(噪声多);块太小,上下文不足(语义不完整)。 好的分块策略要在这两端之间找到最优解。
六种分块策略全面对比
1. 固定大小分块(Fixed-Size Chunking)
最简单的策略:按固定token数切分,带overlap。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=50,
separators=["\n\n", "\n", "。", "!", "?", ",", " "],
)
chunks = splitter.split_text(document)
| 参数 | 推荐值 | 说明 |
|---|---|---|
| chunk_size | 256-1024 | 取决于文档类型,技术文档建议512 |
| chunk_overlap | 10-20% | 防止语义在切分边界断裂 |
优点:实现简单、速度快、可预测 缺点:可能在句子中间切断,破坏语义完整性 适用:快速原型、均匀文本(如日志、评论)
2. 语义分块(Semantic Chunking)
不按固定大小切,而是按语义相似度断点切。当相邻句子的语义相似度低于阈值时,在此处分块。
from semantic_chunker import SemanticChunker
from langchain_openai import OpenAIEmbeddings
# 基于Embedding相似度的语义分块
chunker = SemanticChunker(
OpenAIEmbeddings(model="text-embedding-3-large"),
breakpoint_threshold_type="percentile", # 或 "standard_deviation"
breakpoint_threshold_amount=95, # 百分位阈值
)
chunks = chunker.split_text(document)
工作原理:
句子1 → 句子2 → 句子3 → 句子4 → 句子5
0.92 0.88 0.45↓ 0.91 0.87
↑
语义断点(相似度骤降)
在这里切分
# 自定义语义分块实现
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
def semantic_chunk(text, embeddings_model, threshold=0.5):
"""基于句子间语义相似度的自适应分块"""
sentences = split_into_sentences(text)
# 计算每对相邻句子的相似度
embeddings = [embeddings_model.embed(s) for s in sentences]
similarities = [
cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0]
for i in range(len(embeddings) - 1)
]
# 找到相似度低于阈值的位置作为断点
chunks = []
current_chunk = [sentences[0]]
for i, sim in enumerate(similarities):
if sim < threshold:
chunks.append(" ".join(current_chunk))
current_chunk = [sentences[i + 1]]
else:
current_chunk.append(sentences[i + 1])
chunks.append(" ".join(current_chunk))
return chunks
优点:语义完整、适应文档结构 缺点:计算开销大(每句话都要Embedding)、块大小不可控 适用:长文章、研究报告、新闻
3. 文档感知分块(Document-Aware Chunking)
尊重文档的结构边界(标题、段落、列表、表格)进行分块。
from langchain.text_splitter import MarkdownHeaderTextSplitter
# Markdown文档按标题层级分块
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
md_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=headers_to_split_on,
strip_headers=False, # 保留标题在块中
)
chunks = md_splitter.split_text(markdown_document)
# 对于HTML文档
from langchain.text_splitter import HTMLSectionSplitter
html_splitter = HTMLSectionSplitter(
sections_to_split_on=[
("h1", "Header 1"),
("h2", "Header 2"),
("div.class='section'", "Section"),
]
)
# 带元数据的文档感知分块
def document_aware_chunk(doc, max_chunk_size=800):
"""先按结构分块,超大块再二次切分"""
# 第一层:按标题分块
sections = split_by_headers(doc)
final_chunks = []
for section in sections:
if len(section["content"]) <= max_chunk_size:
final_chunks.append(section)
else:
# 第二层:超大块按段落再切
paragraphs = section["content"].split("\n\n")
current = ""
for para in paragraphs:
if len(current) + len(para) <= max_chunk_size:
current += "\n\n" + para
else:
if current:
final_chunks.append({
"content": current,
"metadata": section["metadata"]
})
current = para
if current:
final_chunks.append({
"content": current,
"metadata": section["metadata"]
})
return final_chunks
优点:保留文档结构、元信息丰富 缺点:依赖文档格式、需要格式检测 适用:技术文档、论文、HTML页面
4. 层级分块(Hierarchical Chunking)
同时生成大块和小块,小块用于精确检索,大块用于提供上下文。
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.extractors import (
SummaryExtractor,
QuestionsAnsweredExtractor,
)
# 层级分块:父子结构
parent_splitter = SentenceSplitter(chunk_size=2048) # 大块(父)
child_splitter = SentenceSplitter(chunk_size=256) # 小块(子)
# 先切成大块
parent_nodes = parent_splitter.get_nodes_from_documents(documents)
# 每个大块再切成小块
all_nodes = []
for parent_node in parent_nodes:
parent_id = parent_node.node_id
child_nodes = child_splitter.get_nodes_from_documents(
[parent_node],
)
# 为子节点设置父节点引用
for child in child_nodes:
child.relationships["parent"] = parent_id
all_nodes.extend(child_nodes)
# 检索时用小块,返回时带上父块
from llama_index.core.retrievers import AutoMergingRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
base_retriever = index.as_retriever(similarity_top_k=10)
retriever = AutoMergingRetriever(
base_retriever,
storage_context,
verbose=True,
)
架构示意:
文档
├── 父块A (2048 tokens)
│ ├── 子块A1 (256 tokens)
│ ├── 子块A2 (256 tokens)
│ └── 子块A3 (256 tokens)
├── 父块B (2048 tokens)
│ ├── 子块B1 (256 tokens)
│ └── 子块B2 (256 tokens)
检索时:用子块匹配→如果多个子块来自同一父块→返回父块
优点:兼顾检索精度和上下文完整性 缺点:存储开销大、实现复杂 适用:高质量RAG系统、长文档问答
5. 递归分块(Recursive Splitting)
按优先级顺序尝试不同的分隔符,尽量在自然边界处分块。
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 中文文档的递归分块
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=[
"\n## ", # 二级标题
"\n### ", # 三级标题
"\n\n", # 段落
"\n", # 行
"。", # 句号
"!", # 感叹号
"?", # 问号
";", # 分号
",", # 逗号
" ", # 空格
"", # 字符
],
)
chunks = splitter.split_text(document)
6. 基于LLM的分块(LLM-Based Chunking)
用LLM判断文档的语义边界,最精确但也最贵。
def llm_chunk(document: str) -> list[str]:
"""用LLM识别语义边界进行分块"""
prompt = f"""你是一个文档分块专家。将以下文档分成若干语义完整的块。
每个块应该:
1. 围绕一个主题展开
2. 大小在200-800字之间
3. 语义自洽,可以独立理解
用 === CHUNK === 分隔每个块。
文档:
{document}
"""
response = llm.invoke(prompt)
chunks = response.split("=== CHUNK ===")
return [c.strip() for c in chunks if c.strip()]
实测对比:六种策略全面PK
测试设置
- 数据集:1,000篇技术博客 + 500份产品文档 + 200篇学术论文
- Embedding模型:text-embedding-3-large
- LLM:GPT-4o
- 评估指标:检索准确率、上下文命中率、平均块大小
结果对比
| 策略 | 检索准确率 | 上下文完整率 | 块大小方差 | 处理速度 | API成本 |
|---|---|---|---|---|---|
| 固定大小 | 62.3% | 71.2% | 低 | ⚡⚡⚡⚡⚡ | $0 |
| 递归分块 | 68.7% | 76.8% | 中 | ⚡⚡⚡⚡ | $0 |
| 文档感知 | 76.5% | 85.3% | 中 | ⚡⚡⚡ | $0 |
| 语义分块 | 74.2% | 82.1% | 高 | ⚡⚡ | $2.5/1k docs |
| 层级分块 | 81.3% | 91.5% | 低 | ⚡⚡⚡ | $0 |
| LLM分块 | 83.7% | 89.2% | 高 | ⚡ | $15/1k docs |
关键发现
- 层级分块性价比最高:准确率与LLM分块接近,但无需额外API调用
- 文档感知分块是基线优选:零成本、效果不错、实现简单
- 语义分块并非银弹:计算开销大,但在异构文档上表现稳定
- 固定大小分块在均匀文本上并不差:日志、评论等均匀文本场景足够
选型决策树
你的文档是什么格式?
├── 结构化(Markdown/HTML/有标题)
│ └── 文档感知分块 → 如果块太大,加层级分块
├── 非结构化长文本(论文/报告)
│ ├── 预算充足 → LLM分块
│ └── 预算有限 → 语义分块
├── 短文本/均匀文本(评论/日志)
│ └── 固定大小分块 或 递归分块
└── 混合类型
└── 层级分块(通用性最强)
进阶优化技巧
1. 块前置元数据
# 给每个块加上文档来源、标题、章节信息
chunk_with_metadata = {
"content": chunk_text,
"metadata": {
"source": "tech_blog_2026.md",
"section": "## 架构设计",
"position": 3, # 在文档中的位置
"doc_title": "GraphRAG实践指南",
}
}
2. 块内摘要增强
# 为每个块生成一句话摘要,拼接到块前面
def enhance_with_summary(chunk: str) -> str:
summary = llm.invoke(f"用一句话总结:{chunk[:500]}")
return f"[摘要] {summary}\n\n{chunk}"
3. 滑动窗口Overlap优化
# 不是简单的字符overlap,而是语义overlap
# 在每个块的开头重复上一个块的最后一句话
def semantic_overlap(chunks: list[str]) -> list[str]:
enhanced = []
for i, chunk in enumerate(chunks):
if i > 0:
last_sentence = extract_last_sentence(chunks[i-1])
chunk = f"[上文] {last_sentence}\n\n{chunk}"
enhanced.append(chunk)
return enhanced
总结
分块没有"最优策略",只有"最适合你的策略"。2026年的最佳实践是:
- 从文档感知分块开始,这是零成本的高性价比选择
- 效果不满足时升级到层级分块,兼顾精度和上下文
- 预算充足时尝试语义分块或LLM分块,处理复杂文档效果最佳
- 始终做A/B测试,不同文档类型最优策略不同
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
