
为什么分块策略如此重要
RAG系统的核心流程是:先将知识库文档切分成小块(chunk),为每个块生成向量并存储,用户提问时检索最相关的块,再拼接上下文交给LLM生成答案。如果分块太大,向量表示会变得模糊,检索精度下降;分块太小,则丢失上下文语义,答案不完整。
举一个直观的例子:一篇技术文档中有一段关于”MySQL索引优化”的完整说明,如果按固定500字切块,可能正好把这段内容一分为二,导致检索时无法召回完整信息。因此,选择合理的分块策略是RAG工程化的第一步。
固定大小分块:简单但有局限
最简单的分块方式是按固定字符数或token数切分,通常设置一个重叠窗口(overlap)来缓解语义断裂问题。
from langchain.text_splitter import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separator="\n"
)
chunks = text_splitter.split_text(document_content)
print(f"共切分为 {len(chunks)} 个块")
print(f"第1个块前100字: {chunks[0][:100]}")
这种方式实现简单、速度快,适合结构化程度不高的文本(如聊天记录、日志)。但缺点也很明显:它不关心句子或段落的边界,可能把一句话从中间截断。
递归字符分块:尊重文本结构
递归字符分块是LangChain推荐的默认策略。它按层级分隔符(段落 → 换行 → 句号 → 空格)依次尝试切分,优先在自然断点处分块。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800,
chunk_overlap=100,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
chunks = splitter.split_text(document)
for i, chunk in enumerate(chunks[:3]):
print(f"--- Chunk {i+1} (长度: {len(chunk)}) ---")
print(chunk[:150])
print()
这种方法在大多数中文技术文档场景下表现良好,是快速上手RAG的首选方案。
语义分块:按含义切分
语义分块的核心思想是:相邻句子如果语义相似则归入同一块,语义差异大则切开。实现方式是先按句子拆分,计算相邻句子的向量余弦相似度,在相似度骤降处切分。
import numpy as np
from sentence_transformers import SentenceTransformer
def semantic_chunking(text, model_name="BAAI/bge-small-zh-v1.5", threshold=0.5):
model = SentenceTransformer(model_name)
# 按中文句号拆分
sentences = [s.strip() for s in text.split("。") if s.strip()]
embeddings = model.encode(sentences)
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
sim = np.dot(embeddings[i], embeddings[i-1]) / \
(np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i-1]))
if sim < threshold:
chunks.append("。".join(current_chunk) + "。")
current_chunk = [sentences[i]]
else:
current_chunk.append(sentences[i])
if current_chunk:
chunks.append("。".join(current_chunk) + "。")
return chunks
chunks = semantic_chunking("你的长文本内容...")
print(f"语义分块结果: 共 {len(chunks)} 个块")
语义分块的检索质量通常最优,但计算成本较高,适合对精度要求严格且文档量不大的场景。
基于文档结构的分块
对于有明确结构的文档(Markdown、HTML、PDF),利用标题层级进行分块是最自然的方式。每个标题及其下属内容作为一个完整的块,既保留了语义完整性,又附带了标题作为元数据。
import re
def markdown_chunking(md_text, max_size=1000):
"""按Markdown标题层级分块"""
sections = re.split(r'(?=^#{1,3} )', md_text, flags=re.MULTILINE)
sections = [s.strip() for s in sections if s.strip()]
chunks = []
for section in sections:
if len(section) <= max_size:
chunks.append(section)
else:
# 超长段落递归切分
sub_splitter = RecursiveCharacterTextSplitter(
chunk_size=max_size, chunk_overlap=100
)
chunks.extend(sub_splitter.split_text(section))
return chunks
# 使用示例
with open("technical_doc.md", "r") as f:
doc = f.read()
chunks = markdown_chunking(doc)
for c in chunks:
print(c[:80], "...")

分块策略对比与选型建议
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 固定大小 | 实现简单、速度快 | 语义断裂 | 日志、聊天记录 |
| 递归字符 | 平衡效果与速度 | 依赖分隔符选择 | 通用技术文档 |
| 语义分块 | 检索精度最高 | 计算开销大 | 高精度问答系统 |
| 结构化分块 | 保留文档结构 | 需要结构化输入 | Markdown/HTML文档 |
在实际项目中,建议采用混合策略:先用结构化分块处理有标题的文档,对超长段落再用递归字符分块兜底。同时设置合理的chunk_overlap(通常为块大小的10%-20%),避免边界处信息丢失。
实践中的优化技巧
最后分享几个工程实践中验证有效的优化点:
# 1. 分块后为每个块添加元数据,提升检索时的过滤能力
for i, chunk in enumerate(chunks):
metadata = {
"source": "技术文档.pdf",
"chunk_index": i,
"total_chunks": len(chunks),
"word_count": len(chunk)
}
# 2. 使用Parent-Child策略:小块检索,大块喂给LLM
small_chunks = splitter_small.split_text(doc) # 200字,用于检索
big_chunks = splitter_big.split_text(doc) # 1000字,用于生成
# 3. 定期评估分块质量
# 可用检索命中率、答案准确率作为指标,迭代优化chunk_size
分块没有万能方案,关键是根据你的文档类型和业务场景做实验、看数据、持续优化。
汤不热吧