引言
分块(Chunking)是RAG系统中承上启下的关键环节:上承文档解析,下接向量检索和生成。分块策略直接影响检索精度和生成质量——太大或太小的块都会导致问题。本文对主流分块策略进行系统对比评估,并提供选型建议。
分块为何重要
分块大小的影响
块过大的问题:单个向量难以准确表示大段内容的语义;检索时相似度分数被稀释;上下文窗口浪费在不相关信息上;增大embedding计算成本。
块过小的问题:语义不完整,检索到的块可能缺少上下文;需要检索更多块才能获得完整信息;增加索引存储和检索开销。
分块边界的影响
即使大小合适,切分位置不当也会导致问题:在句子中间切断导致语义破碎;在段落中间切分丢失段落级语义;切断实体与其描述的关联。
主流分块策略
策略一:固定大小分块
按固定token数切分,通常配合一定重叠:
def fixed_size_chunk(text, chunk_size=512, overlap=50):
"""固定大小分块"""
tokens = tokenizer.encode(text)
chunks = []
for i in range(0, len(tokens), chunk_size - overlap):
chunk_tokens = tokens[i:i + chunk_size]
chunk_text = tokenizer.decode(chunk_tokens)
chunks.append(chunk_text)
if i + chunk_size >= len(tokens):
break
return chunks
优势:实现简单、速度极快、块大小可控。
劣势:可能在句子中间切断、不感知内容结构、不同文档可能需要不同大小。
适用场景:快速原型、格式统一的文档、对速度要求高的场景。
策略二:句子级分块
以句子为最小单位进行分块:
import nltk
def sentence_chunk(text, max_chunk_size=512):
"""按句子分块"""
sentences = nltk.sent_tokenize(text)
chunks = []
current_chunk = []
current_size = 0
for sent in sentences:
sent_size = len(tokenizer.encode(sent))
if current_size + sent_size > max_chunk_size and current_chunk:
chunks.append(" ".join(current_chunk))
current_chunk = [sent]
current_size = sent_size
else:
current_chunk.append(sent)
current_size += sent_size
if current_chunk:
chunks.append(" ".join(current_chunk))
return chunks
优势:保持句子完整性、语义边界更合理。
劣势:块大小不均匀、长句子可能导致超大块、段落结构可能被破坏。
适用场景:自然语言文本、新闻文章、博客内容。
策略三:段落级分块
按段落切分,合并过小段落:
def paragraph_chunk(text, max_chunk_size=512, min_chunk_size=100):
"""按段落分块"""
paragraphs = text.split("\n\n")
chunks = []
current_chunk = ""
current_size = 0
for para in paragraphs:
para_size = len(tokenizer.encode(para))
if para_size > max_chunk_size:
# 段落过大,降级为句子分块
sub_chunks = sentence_chunk(para, max_chunk_size)
chunks.extend(sub_chunks)
elif current_size + para_size > max_chunk_size and current_chunk:
chunks.append(current_chunk)
current_chunk = para
current_size = para_size
else:
current_chunk = current_chunk + "\n\n" + para if current_chunk else para
current_size += para_size
if current_chunk and current_size >= min_chunk_size:
chunks.append(current_chunk)
elif current_chunk and chunks:
chunks[-1] += "\n\n" + current_chunk
return chunks
优势:保持段落完整性、适合结构化文档、语义连贯性好。
劣势:段落定义不一致(不同文档的段落分隔不同)、超大段落需特殊处理。
适用场景:结构化文档、报告、论文、技术文档。
策略四:语义分块
基于嵌入相似度动态确定分块边界:
from sentence_transformers import SentenceTransformer
import numpy as np
def semantic_chunk(text, threshold=0.5, min_chunk_size=100):
"""基于语义相似度分块"""
model = SentenceTransformer('all-MiniLM-L6-v2')
# 按句子切分
sentences = nltk.sent_tokenize(text)
# 计算相邻句子的嵌入相似度
embeddings = model.encode(sentences)
# 计算相邻句子的余弦相似度
similarities = []
for i in range(len(embeddings) - 1):
sim = cosine_similarity(embeddings[i], embeddings[i+1])
similarities.append(sim)
# 在相似度低于阈值处切分
chunks = []
current_start = 0
for i, sim in enumerate(similarities):
if sim < threshold:
chunk = " ".join(sentences[current_start:i+1])
if len(tokenizer.encode(chunk)) >= min_chunk_size:
chunks.append(chunk)
current_start = i + 1
# 添加最后一个块
if current_start < len(sentences):
chunk = " ".join(sentences[current_start:])
chunks.append(chunk)
return chunks
优势:语义连贯性最好、自适应内容变化、减少上下文断裂。
劣势:计算开销大(需要嵌入模型)、阈值调优困难、速度慢。
适用场景:高质量检索需求、长文档、异构内容混合。
策略五:递归分块
递归地按层级分隔符切分:
def recursive_chunk(text, max_chunk_size=512, separators=None):
"""递归分块"""
if separators is None:
separators = ["\n\n\n", "\n\n", "\n", ". ", " ", ""]
if len(tokenizer.encode(text)) <= max_chunk_size:
return [text]
for sep in separators:
if sep in text:
parts = text.split(sep)
chunks = []
current = ""
for part in parts:
candidate = current + sep + part if current else part
if len(tokenizer.encode(candidate)) <= max_chunk_size:
current = candidate
else:
if current:
chunks.append(current)
# 递归处理超大部分
if len(tokenizer.encode(part)) > max_chunk_size:
chunks.extend(recursive_chunk(part, max_chunk_size, separators[separators.index(sep)+1:]))
current = ""
else:
current = part
if current:
chunks.append(current)
return chunks
return [text]
优势:保持层级结构、灵活适应不同文档格式、在大小和语义间平衡。
劣势:实现较复杂、分隔符选择需要经验。
适用场景:Markdown文档、代码文件、混合格式文档。
策略六:文档结构感知分块
利用文档的结构信息(标题层级)进行分块:
def structure_aware_chunk(document):
"""基于文档结构的分块"""
chunks = []
for section in document.sections:
# 每个标题下内容作为一个块
section_text = f"# {section.title}\n\n{section.content}"
if len(tokenizer.encode(section_text)) > MAX_SIZE:
# 超大section降级处理
sub_chunks = recursive_chunk(section.content)
# 为每个子块添加标题上下文
for sub in sub_chunks:
chunks.append(f"# {section.title}\n\n{sub}")
else:
chunks.append(section_text)
return chunks
优势:保持文档逻辑结构、每个块有清晰的主题、检索结果更易理解。
劣势:需要文档结构解析前置、依赖文档格式规范。
适用场景:技术文档、产品手册、学术论文。
对比评估
| 策略 | 实现复杂度 | 语义连贯性 | 速度 | 块大小均匀性 | 适用文档类型 |
|---|---|---|---|---|---|
| 固定大小 | 极低 | 低 | 极快 | 高 | 通用 |
| 句子级 | 低 | 中 | 快 | 中 | 自然语言 |
| 段落级 | 中 | 中高 | 快 | 低 | 结构化 |
| 语义分块 | 高 | 高 | 慢 | 低 | 高质量需求 |
| 递归分块 | 中高 | 中高 | 中 | 中 | 混合格式 |
| 结构感知 | 中高 | 高 | 中 | 低 | 结构化文档 |
选型建议
- 快速原型:固定大小分块,chunk_size=512, overlap=50
- 自然语言文档:句子级或段落级分块
- 技术文档:结构感知分块,利用标题层级
- 混合格式:递归分块
- 高精度检索:语义分块,投入计算换取质量
- 生产环境推荐:递归分块 + 结构感知的混合策略
评估方法
检索质量评估
构建评估集,对比不同分块策略的检索效果:
- 召回率:相关文档被检索到的比例
- 精确率:检索结果中相关块的比例
- MRR:相关结果的平均排名
生成质量评估
使用相同的检索结果和LLM,对比不同分块策略下生成答案的质量:事实准确性、信息完整性、答案相关性。
结语
分块策略没有银弹,最佳策略取决于文档类型、检索需求和计算资源。实践中建议:从简单策略开始建立基线,在评估集上对比不同策略的效果,根据结果选择或组合策略。记住,分块只是RAG的一个环节,它需要与嵌入模型、检索策略和生成模型协同优化,才能获得最佳效果。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
