目录
- 文本分块的设计动机
- 固定长度分块
- 段落分割
- 语义分块
- 递归拆分
- 文本分块的边界与失效模式
摘要
文本分块(Chunking)是将长文档拆分为适合检索和生成的小块的过程,是 RAG 系统中的关键预处理步骤。本文从文本分块的设计动机出发,分析固定长度分块、段落分割、语义分块和递归拆分四种主流策略,以及在 RAG 系统中的工程实践。
1. 文本分块的设计动机
RAG 系统需要从知识库中检索相关信息。如果文档不做分块,整个文档作为一个块,检索器无法精确定位到相关段落。如果分块太小,每个块缺乏上下文信息,检索器无法理解语义。文本分块的目标是:在块的大小和语义完整性之间取得平衡。
1.1 为什么需要分块
| 问题 | 不切分 | 切分 |
|---|---|---|
| 检索精度 | 低(无法精确定位) | 高(精确定位相关段落) |
| 上下文长度 | 长(超出 LLM 窗口) | 短(在窗口内) |
| 语义完整性 | 好(完整文档) | 中(可能切断上下文) |
| 检索效率 | 低(一个块太大) | 高(块大小适中) |
1.2 分块的核心挑战
文本分块的核心挑战是平衡块大小(影响检索效率和上下文窗口占用)和语义完整性(影响检索质量)。
1.3 分块策略的演进
固定长度切分(早期 RAG)→ 段落分割(2022)→ 语义分块(2023)→ 递归拆分 + 自适应分块(2024)。
1.4 分块对 RAG 质量的影响
| 分块策略 | 检索召回率 | 生成质量 | 适用场景 |
|---|---|---|---|
| 固定长度 | 低 | 中 | 简单场景 |
| 段落分割 | 中 | 高 | 结构化文档 |
| 语义分块 | 高 | 高 | 通用场景 |
| 递归拆分 | 高 | 高 | 复杂文档 |
1.5 分块的局限性
分块策略的设计需要权衡多个因素。
2. 固定长度分块
2.1 固定长度分块的实现
固定长度分块是最简单的分块策略,按固定 Token 数或字符数切分:
deffixed_length_chunking(text,chunk_size=512,overlap=50):"""固定长度分块"""chunks=[]start=0whilestart<len(text):end=min(start+chunk_size,len(text))chunk=text[start:end]chunks.append(chunk)start=end-overlap# 重叠部分returnchunks2.2 重叠策略
重叠(Overlap)确保块边界附近的上下文信息不会丢失:
| 重叠大小 | 上下文保留 | 冗余度 | 适用场景 |
|---|---|---|---|
| 0 | 无重叠 | 无 | 短文本 |
| 10-50 | 少量上下文 | 低 | 一般场景 |
| 50-100 | 中等上下文 | 中 | 长文本 |
| 100-200 | 大量上下文 | 高 | 需要精确匹配 |
2.3 固定长度分块的优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单 | 可能切断语义完整的段落 |
| 计算快 | 块语义不完整 |
| 可预测块大小 | 检索质量差 |
3. 段落分割
3.1 段落分割的实现
段落分割根据文档的自然结构(段落、标题、章节)切分:
defparagraph_chunking(text,separators=["\n\n","\n",".","。","!","?"]):"""段落分割"""chunks=[]current_chunk=""forseparatorinseparators:ifseparatorintext:segments=text.split(separator)forsegmentinsegments:segment=segment.strip()iflen(segment)>50:# 过滤过短段落chunks.append(segment)breakreturnchunks3.2 段落分割的适用场景
| 场景 | 适合 | 原因 |
|---|---|---|
| 新闻报道 | 是 | 段落结构清晰 |
| 学术论文 | 是 | 有章节标题 |
| 技术文档 | 是 | 有明确标题 |
| 对话 | 否 | 对话结构不清晰 |
3.3 段落分割的优缺点
| 优点 | 缺点 |
|---|---|
| 语义完整 | 块大小不固定 |
| 实现简单 | 段落可能过长或过短 |
| 保留文档结构 | 依赖文档格式 |
4. 语义分块
4.1 语义分块的原理
语义分块根据文本的语义边界切分,确保每个块具有完整的语义。语义分块使用嵌入模型检测语义变化点。
4.2 语义分块的实现
defsemantic_chunking(text,embedding_model,max_chunk_size=1000):"""语义分块"""# 1. 将文本分割为句子sentences=split_sentences(text)# 2. 计算每个句子的嵌入sentence_embeddings=[embedding_model.encode(s)forsinsentences]# 3. 计算相邻句子间的语义相似度similarities=[]foriinrange(1,len(sentence_embeddings)):sim=cosine_similarity(sentence_embeddings[i-1],sentence_embeddings[i])similarities.append(sim)# 4. 在语义相似度低的地方切分chunks=[]current_chunk=[]foriinrange(len(sentences)):current_chunk.append(sentences[i])ifi<len(similarities)andsimilarities[i]<0.5:chunks.append(" ".join(current_chunk))current_chunk=[]ifcurrent_chunk:chunks.append(" ".join(current_chunk))returnchunks4.3 语义分块的参数
| 参数 | 描述 | 默认值 | 效果 |
|---|---|---|---|
| 相似度阈值 | 判定语义边界的阈值 | 0.5 | 越高分块越少 |
| 最大块大小 | 块的最大 Token 数 | 1000 | 防止块过大 |
| 最小块大小 | 块的最小 Token 数 | 50 | 防止块过小 |
4.4 语义分块的优缺点
| 优点 | 缺点 |
|---|---|
| 语义完整 | 实现复杂 |
| 检索质量高 | 计算开销大 |
| 自适应 | 对嵌入模型敏感 |
5. 递归拆分
5.1 递归拆分的原理
递归拆分从最大的分隔符开始分割,如果块仍然太大,递归使用更小的分隔符继续分割。
5.2 递归拆分的实现
defrecursive_chunking(text,separators,chunk_size=1000,chunk_overlap=200):"""递归拆分"""# 如果没有分隔符,按固定长度拆分ifnotseparators:returnfixed_length_chunking(text,chunk_size,chunk_overlap)separator=separators[0]remaining_separators=separators[1:]# 使用当前分隔符分割chunks=[]segments=text.split(separator)forsegmentinsegments:iflen(segment)<=chunk_size:chunks.append(segment)else:# 如果块仍然太大,递归拆分chunks.extend(recursive_chunking(segment,remaining_separators,chunk_size,chunk_overlap))returnchunks5.3 递归拆分的配置
# 递归拆分配置RECURSIVE_CHUNK_CONFIG={"separators":["\n\n","\n",".","!","?",",","。"," ",""],"chunk_size":512,"chunk_overlap":50}5.4 递归拆分的优缺点
| 优点 | 缺点 |
|---|---|
| 适应性强 | 实现复杂 |
| 语义完整 | 可能产生过多块 |
| 块大小可控 | 参数调优困难 |
6. 文本分块的边界与失效模式
6.1 块大小选择
| 块大小(Token) | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 128 | 检索精确 | 上下文不足 | 短文本问答 |
| 256 | 平衡 | 上下文中等 | 通用场景 |
| 512 | 上下文丰富 | 检索精度降低 | 长文本 |
| 1024 | 上下文完整 | 检索精度低 | 长文档分析 |
6.2 分块质量的评估
| 指标 | 描述 | 评估方法 |
|---|---|---|
| 语义完整性 | 块内信息是否完整 | 人工评估 |
| 检索召回率 | 检索到相关块的比例 | 标注测试集 |
| 生成质量 | 基于块生成的回答质量 | 人工评估 |
6.3 分块效果的对比
| 分块策略 | 检索召回率 | 生成质量 | 计算开销 |
|---|---|---|---|
| 固定长度 | 75% | 7.0 | 低 |
| 段落分割 | 82% | 7.8 | 低 |
| 语义分块 | 88% | 8.2 | 高 |
| 递归拆分 | 85% | 8.0 | 中 |
6.4 分块策略的选择
| 文档类型 | 推荐策略 | 原因 |
|---|---|---|
| 结构清晰 | 段落分割 | 保留文档结构 |
| 长文本 | 递归拆分 | 适应性强 |
| 学术论文 | 段落分割 | 章节结构清晰 |
| 非结构化 | 语义分块 | 自动检测边界 |
7. 分块在 RAG 中的工程实践
7.1 分块后处理
分块后需要处理:去重(删除内容重复的块)、过滤(过滤过短或无关的块)以及增强(为块添加元数据,如来源、标题、位置)。
7.2 分块与检索的协同
分块策略直接影响检索质量。块过大导致检索不精确,块过小导致上下文不足。需要在分块大小和检索质量之间迭代优化。
7.3 分块策略的自动化
使用 LLM 辅助分块策略选择:LLM 分析文档结构,自动选择最优分块策略和参数。
defauto_chunking(text,llm):"""自动分块"""prompt=f""" 请分析以下文档的结构,并推荐最适合的分块策略和参数。 文档:{text[:2000]}... 请输出推荐的策略(fixed_length/paragraph/semantic/recursive)和参数。 """recommendation=llm.generate(prompt)strategy=parse_recommendation(recommendation)ifstrategy=="paragraph":returnparagraph_chunking(text)elifstrategy=="semantic":returnsemantic_chunking(text,embedding_model)elifstrategy=="recursive":returnrecursive_chunking(text,RECURSIVE_CHUNK_CONFIG["separators"])else:returnfixed_length_chunking(text)8. 分块策略的评估与优化
8.1 分块质量的评估指标
| 指标 | 描述 | 计算方法 |
|---|---|---|
| 语义完整性 | 块内信息是否自包含 | 人工评估(1-5 分) |
| 检索召回率 | 相关块被检索到的比例 | 在标注测试集上计算 |
| 生成质量 | 基于块生成的回答质量 | 人工评估或自动指标 |
| 块大小一致性 | 块大小的标准差 | 计算各块大小的标准差 |
8.2 分块策略的 A/B 测试
defchunking_ab_test(documents,strategies,test_queries,ground_truth):"""分块策略 A/B 测试"""results={}forstrategyinstrategies:chunks=strategy(documents)# 评估检索质量retrieval_scores=evaluate_retrieval(chunks,test_queries,ground_truth)# 评估生成质量generation_scores=evaluate_generation(chunks,test_queries,ground_truth)results[strategy.name]={"retrieval":retrieval_scores,"generation":generation_scores,"chunk_count":len(chunks)}returnresults8.3 分块参数的调优
分块参数(块大小、重叠大小、分隔符)需要根据具体文档和任务调优:
| 参数 | 调优范围 | 对质量的影响 |
|---|---|---|
| 块大小 | 128-1024 | 大小影响检索精度和上下文完整性 |
| 重叠大小 | 0-200 | 影响边界上下文保留 |
| 分隔符 | 多个级别 | 影响语义完整性 |
| 最小块大小 | 20-100 | 影响检索质量 |
8.4 分块策略的自适应选择
根据文档类型自动选择最优分块策略:
defadaptive_chunking(documents):"""自适应分块策略选择"""fordocindocuments:doc_type=detect_document_type(doc)ifdoc_type=="article":chunks=paragraph_chunking(doc)elifdoc_type=="code":chunks=code_chunking(doc)elifdoc_type=="conversation":chunks=semantic_chunking(doc,embedding_model)else:chunks=recursive_chunking(doc,separators)store_chunks(chunks)9. 分块在特定文档类型中的应用
9.1 代码分块
代码文档的分块需要保留代码的结构:
| 代码语言 | 分块策略 | 说明 |
|---|---|---|
| Python | 函数/类 | 按函数定义切分 |
| Java | 类/方法 | 按类和方法切分 |
| HTML | 标签/区块 | 按 HTML 标签切分 |
| Markdown | 标题/段落 | 按标题层级切分 |
9.2 PDF 文档分块
PDF 文档的分块需要处理:文本提取(从 PDF 中提取文本)、表格识别(识别表格结构)、图像提取(提取图像用于多模态检索):
defpdf_chunking(pdf_path,embedding_model):"""PDF 文档分块"""# 1. 提取文本text=extract_text_from_pdf(pdf_path)# 2. 提取表格tables=extract_tables_from_pdf(pdf_path)# 3. 提取图像images=extract_images_from_pdf(pdf_path)# 4. 合并分块all_chunks=[]all_chunks.extend(recursive_chunking(text,["\n\n","\n","."]))all_chunks.extend(tables)all_chunks.extend(images)returnall_chunks9.3 多语言文档分块
多语言文档的分块需要处理不同语言的分隔符差异:
| 语言 | 分隔符 | 说明 |
|---|---|---|
| 中文 | 。!? | 句号、感叹号、问号 |
| 英文 | . ! ? | 句号、感叹号、问号 |
| 日文 | 。!? | 句号、感叹号、问号 |
| 阿拉伯文 | . ! ? | 从右向左阅读 |
10. 分块与检索的深度协同
10.1 分块重叠的重要性
重叠策略确保分块边界附近的上下文信息不会丢失,这对检索质量至关重要。
10.2 分块元数据管理
| 元数据字段 | 描述 | 示例 |
|---|---|---|
| source | 文档来源 | “document_123.pdf” |
| page | 页码 | 15 |
| section | 章节标题 | “3.2 分块策略” |
| chunk_index | 块索引 | 3 |
| total_chunks | 总块数 | 20 |
10.3 分块与嵌入的配合
分块策略直接影响嵌入质量:块太小导致嵌入向量缺乏语义信息,块太大导致嵌入向量过于通用。优化分块与嵌入的协同:
defoptimize_chunk_embedding(documents,embedding_model,chunk_sizes):"""优化分块与嵌入的配合"""best_score=0best_size=0forsizeinchunk_sizes:chunks=fixed_length_chunking(documents,chunk_size=size)embeddings=[embedding_model.encode(chunk)forchunkinchunks]avg_distance=compute_average_distance(embeddings)# 距离越大表示块间差异越大,检索质量越高ifavg_distance>best_score:best_score=avg_distance best_size=sizereturnbest_size11. 分块系统的工程实现
11.1 分块处理流水线
生产环境中的分块处理流水线:
classChunkingPipeline:"""分块处理流水线"""def__init__(self,chunking_strategy,embedding_model,vector_db):self.strategy=chunking_strategy self.embedding_model=embedding_model self.vector_db=vector_dbdefprocess_document(self,document):"""处理单个文档"""# 1. 预处理cleaned=self.clean(document)# 2. 分块chunks=self.strategy.chunk(cleaned)# 3. 后处理chunks=self.post_process(chunks,document.metadata)# 4. 生成嵌入并存储forchunkinchunks:embedding=self.embedding_model.encode(chunk["text"])self.vector_db.insert(embedding=embedding,metadata=chunk)returnlen(chunks)defprocess_batch(self,documents,batch_size=100):"""批量处理文档"""total_chunks=0foriinrange(0,len(documents),batch_size):batch=documents[i:i+batch_size]fordocinbatch:total_chunks+=self.process_document(doc)returntotal_chunks11.2 分块系统的监控
| 指标 | 描述 | 告警阈值 |
|---|---|---|
| 平均块大小 | 所有块的平均大小 | > 1024 或 < 128 |
| 块数 | 文档产生的块数 | > 1000 |
| 处理时间 | 单文档处理时间 | > 10s |
| 失败率 | 分块失败的文档比例 | > 1% |
11.3 分块系统的可扩展性
| 文档数量 | 分块策略 | 处理时间 | 存储需求 |
|---|---|---|---|
| 1K | 任何策略 | 分钟级 | 100MB |
| 10K | 固定长度 | 分钟级 | 1GB |
| 100K | 段落分割 | 小时级 | 10GB |
| 1M | 固定长度 | 小时级 | 100GB |
总结
文本分块是 RAG 系统的基础设施。不同的分块策略适用不同的文档类型和任务需求。固定长度分块简单高效,段落分割保留文档结构,语义分块自动检测语义边界,递归拆分适应性强。在实际应用中,通常需要根据文档类型和任务需求组合使用多种分块策略,并通过 A/B 测试不断优化分块参数。
外部引用补充
- 代码分块策略:https://python.langchain.com/docs/modules/data_connection/document_transformers/code_splitter
- PDF 分块技术:https://arxiv.org/abs/2303.04226
- 多语言分块:https://arxiv.org/abs/2303.04226
- 分块与检索协同:https://arxiv.org/abs/2312.10997
- 分块系统监控:https://arxiv.org/abs/2303.04226