第7章:高级分块策略¶
合适的分块策略可以让检索质量提升5-10%。本章将带你掌握多种高级分块技术。
📚 学习目标¶
学完本章后,你将能够:
- 理解分块策略的重要性
- 掌握5+种高级分块方法
- 知道如何优化分块参数
- 能够根据场景选择合适策略
- 完成分块效果对比实验
预计学习时间:3小时 难度等级:⭐⭐⭐☆☆
前置知识¶
- 完成模块1第3章(基础分块)
- 理解文档结构分析
- 掌握基础分块方法
环境要求: - LlamaIndex - llama-hub(高级分块器)
7.1 分块策略的重要性¶
为什么分块这么重要?¶
核心问题:文档切分方式直接影响检索质量
示例:分析一段长文档
原文(2000字):
├─ 段落1:介绍AI历史(300字)
├─ 段落2:机器学习基础(500字)
├─ 段落3:深度学习原理(800字)
└─ 段落4:实际应用(400字)
问题查询:"深度学习的基本原理是什么?"
策略1(固定长度500字):
块1: "...AI历史...机器学习基础[切断]..."
块2: "...基础...深度学习原理..."
❌ 问题:上下文信息被切断
策略2(按段落):
块1: "AI历史"(300字)
块2: "机器学习基础"(500字)
块3: "深度学习原理"(800字)
✅ 优势:保持语义完整,检索精准
分块对性能的影响¶
7.2 分块策略详解¶
策略1:语义分块¶
原理¶
根据语义边界(句子、段落)进行切分,而不是固定字符数。
优势: - ✅ 保持语义完整 - ✅ 避免切断重要信息 - ✅ 提升检索相关性
实现代码¶
# 文件名:07_01_semantic_chunking.py
"""
语义分块实现
"""
from llama_index.core.node_parser import SemanticSplitterNodeParser
from llama_index.embeddings.openai import OpenAIEmbedding
def semantic_chunking(documents, breakpoint_threshold=0.6):
"""
语义分块
Args:
documents: 文档列表
breakpoint_threshold: 断点阈值
Returns:
分块列表
"""
print("="*60)
print("语义分块演示")
print("="*60 + "\n")
# 创建嵌入模型
embed_model = OpenAIEmbedding()
# 创建语义分块器
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_threshold=breakpoint_threshold,
embed_model=embed_model
)
# 分块
print("开始语义分块...")
nodes = splitter.get_nodes_from_documents(documents)
print(f"✅ 生成了 {len(nodes)} 个块\n")
# 显示分块信息
for i, node in enumerate(nodes[:5], 1):
print(f"块 {i}:")
print(f" 长度: {len(node.text)} 字符")
print(f" 内容: {node.text[:100]}...")
print()
return nodes
# 使用示例
if __name__ == "__main__":
from llama_index.core import Document
# 示例文档
doc = Document(text="""
人工智能(AI)是计算机科学的一个分支。
它致力于创建能够执行通常需要人类智能的任务的系统。
机器学习是AI的核心技术之一。
通过算法,计算机可以从数据中学习并做出预测。
深度学习是机器学习的子领域。
它使用多层神经网络来模拟人脑的工作方式。
深度学习在图像识别、自然语言处理等领域取得了突破性进展。
""")
nodes = semantic_chunking([doc])
策略2:上下文分块头¶
原理¶
为每个块添加上下文信息,说明其在原文中的位置和关系。
结构:
原始文档:
第1章:介绍
第2章:方法
第3章:实验
分块后添加上下文:
块1: "第1章内容..."
元数据:{"chapter": "1", "title": "介绍"}
块2: "第2章的算法部分..."
元数据:{"chapter": "2", "section": "算法", "before": "第2章方法概述", "after": "第2章的实验部分"}
实现代码¶
# 文件名:07_02_contextual_headers.py
"""
上下文分块头实现
"""
from llama_index.core.node_parser import (
MarkdownNodeParser,
CodeSplitter
)
def contextual_chunking_markdown(file_path: str):
"""
Markdown文档的上下文分块
Args:
file_path: Markdown文件路径
"""
print("="*60)
print("上下文分块头演示")
print("="*60 + "\n")
# 使用Markdown解析器
parser = MarkdownNodeParser(
include_prev_next_rel=True,
include_metadata=True
)
# 加载并解析
from llama_index.core import SimpleDirectoryReader
reader = SimpleDirectoryReader(input_files=[file_path])
docs = reader.load_data()
nodes = parser.get_nodes_from_documents(docs)
print(f"✅ 生成了 {len(nodes)} 个块")
# 显示上下文信息
for i, node in enumerate(nodes[:3], 1):
print(f"\n块 {i}:")
print(f" 内容: {node.text[:80]}...")
print(f" 元数据: {node.metadata}")
if hasattr(node, 'relationships'):
print(f" 关系: {node.relationships}")
return nodes
# 使用示例
if __name__ == "__main__":
# 创建示例Markdown文件
sample_md = """
# 第一章:简介
这是第一段内容。
## 1.1 背景
背景介绍...
## 1.2 目标
主要目标...
# 第二章:方法
方法论说明...
"""
with open("sample.md", "w", encoding="utf-8") as f:
f.write(sample_md)
nodes = contextual_chunking_markdown("sample.md")
策略3:递归分块¶
原理¶
尝试多种分隔符,按优先级切分。
分隔符优先级:
实现代码¶
# 文件名:07_03_recursive_chunking.py
"""
递归分块实现
"""
from llama_index.core.node_parser import RecursiveCharacterTextSplitter
def recursive_chunking(
documents,
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", "。", " ", ""]
):
"""
递归分块
Args:
documents: 文档列表
chunk_size: 块大小
chunk_overlap: 重叠大小
separators: 分隔符列表(按优先级)
Returns:
分块列表
"""
print("="*60)
print("递归分块演示")
print("="*60 + "\n")
print("分隔符优先级:")
for i, sep in enumerate(separators, 1):
sep_repr = repr(sep) if sep else "(无)"
print(f" {i}. {sep_repr}")
# 创建分块器
splitter = RecursiveCharacterTextSplitter(
chunk_size=chunk_size,
chunk_overlap=chunk_overlap,
separators=separators
)
# 分块
nodes = splitter.get_nodes_from_documents(documents)
print(f"\n✅ 生成了 {len(nodes)} 个块")
return nodes
# 对比实验
if __name__ == "__main__":
from llama_index.core import Document
doc = Document(text="""
第1段内容。第2段内容!
第3段内容?第4段内容。
第5段内容,第6段内容,第7段内容...
很长的段落,没有明显的分隔符,需要继续切分。这个段落包含了很多信息,需要合理地分成多个块。
""")
print("测试1: 只有段落分隔符")
nodes1 = recursive_chunking([doc], separators=["\n\n", "\n"])
print(f"块数: {len(nodes1)}")
print("\n测试2: 段落+句子分隔符")
nodes2 = recursive_chunking([doc], separators=["\n\n", "\n", "。"])
print(f"块数: {len(nodes2)}")
print("\n测试3: 所有分隔符")
nodes3 = recursive_chunking([doc])
print(f"块数: {len(nodes3)}")
策略4:父文档检索¶
原理¶
分块时保持父子关系,检索时返回小块,但使用父文档的上下文。
实现代码¶
# 文件名:07_04_parent_document.py
"""
父文档检索实现
"""
from llama_index.core.node_parser import (
SentenceSplitter,
HierarchicalNodeParser
)
from llama_index.core import StorageContext, load_index_from_storage
def parent_document_retrieval(documents, chunk_size=500):
"""
父文档检索
Args:
documents: 文档列表
chunk_size: 子块大小
Returns:
索引
"""
print("="*60)
print("父文档检索演示")
print("="*60 + "\n")
# 1. 创建父文档解析器
node_parser = HierarchicalNodeParser.from_defaults(
chunk_sizes=[2048, 512, 128] # 父、子、孙块大小
)
# 2. 生成层次节点
nodes = node_parser.get_nodes_from_documents(documents)
print(f"✅ 生成了 {len(nodes)} 个节点")
print(f" 父节点: {sum(1 for n in nodes if n.metadata.get('chunk_type') == 'parent')}")
print(f" 子节点: {sum(1 for n in nodes if n.metadata.get('chunk_type') == 'child')}")
# 3. 构建索引(包含父子关系)
storage_context = StorageContext.from_defaults()
index = VectorStoreIndex(
nodes=nodes,
storage_context=storage_context
)
print("\n✅ 索引构建完成")
# 4. 使用父文档检索器
from llama_index.core.postprocessor import ParentDocumentRetrieverPostprocessor
# 这里需要配置检索器使用父文档
# ...
return index
# 使用示例
if __name__ == "__main__":
from llama_index.core import Document
doc = Document(text="""
这是一段很长的文档,包含多个主题。
第一个主题是关于人工智能的详细介绍,包括历史、发展和应用。
第二个主题转向机器学习,讲解基本概念和常用算法。
最后一个主题讨论深度学习,特别是神经网络和卷积网络。
""")
index = parent_document_retrieval([doc])
策略5:代码文档分块¶
原理¶
代码文档需要特殊处理,保持函数和类的完整性。
# 不好的分块:
def complex_function(
param1, param2,
param3): # ← 切断!
"""函数说明"""
pass
# 好的分块:
# 块1: 完整的函数定义
def complex_function(param1, param2, param3):
"""函数说明"""
pass
实现代码¶
# 文件名:07_05_code_chunking.py
"""
代码文档分块
"""
from llama_index.core.node_parser import CodeSplitter
def code_chunking(code_files):
"""
代码文档分块
Args:
code_files: 代码文件列表
Returns:
分块列表
"""
print("="*60)
print("代码文档分块演示")
print("="*60 + "\n")
# 支持的语言
language = "python" # 或 "javascript", "go", "java"等
# 创建代码分块器
splitter = CodeSplitter(
language=language,
chunk_lines=40, # 每块约40行
chunk_lines_overlap=15, # 重叠15行
max_chars=1500, # 最大字符数
)
# 加载代码
from llama_index.core import SimpleDirectoryReader
reader = SimpleDirectoryReader(
input_files=code_files,
required_exts=[".py"]
)
documents = reader.load_data()
# 分块
nodes = splitter.get_nodes_from_documents(documents)
print(f"✅ 生成了 {len(nodes)} 个代码块")
# 显示示例
for i, node in enumerate(nodes[:3], 1):
print(f"\n块 {i}:")
print(f" 类型: {node.metadata.get('language')}")
print(f" 内容: {node.text[:150]}...")
return nodes
# 使用示例
if __name__ == "__main__":
# 创建示例代码
sample_code = """
class Calculator:
\"\"\"计算器类\"\"\"
def __init__(self):
self.result = 0
def add(self, a, b):
\"\"\"加法\"\"\"
self.result = a + b
return self
def subtract(self, a, b):
\"\"\"减法\"\"\"
self.result = a - b
return self
def multiply(self, a, b):
\"\"\"乘法\"\"\"
self.result = a * b
return self
def divide(self, a, b):
\"\"\"除法\"\"\"
if b == 0:
raise ValueError("不能除以零")
return a / b
"""
with open("sample.py", "w") as f:
f.write(sample_code)
nodes = code_chunking(["sample.py"])
7.3 分块参数优化¶
chunk_size优化¶
实验设计¶
# 文件名:07_06_chunk_size_optimization.py
"""
chunk_size优化实验
"""
import matplotlib.pyplot as plt
import numpy as np
def optimize_chunk_size(documents, queries, relevant_docs):
"""
优化chunk_size参数
Args:
documents: 文档列表
queries: 查询列表
relevant_docs: 真实相关文档
"""
print("="*60)
print("chunk_size优化实验")
print("="*60 + "\n")
# 测试不同的chunk_size
chunk_sizes = [200, 400, 600, 800, 1000, 1200, 1500]
results = {
"chunk_size": [],
"hit_rate": [],
"avg_chunks": [],
"avg_length": []
}
for size in chunk_sizes:
print(f"测试 chunk_size={size}")
# 分块
splitter = SentenceSplitter(
chunk_size=size,
chunk_overlap=int(size * 0.1)
)
nodes = splitter.get_nodes_from_documents(documents)
# 评估
metrics = evaluate_retrieval(nodes, queries, relevant_docs)
# 记录
results["chunk_size"].append(size)
results["hit_rate"].append(metrics["hit_rate"])
results["avg_chunks"].append(len(nodes) / len(documents))
results["avg_length"].append(
sum(len(n.text) for n in nodes) / len(nodes)
)
print(f" Hit Rate: {metrics['hit_rate']:.2%}\n")
# 可视化
plt.figure(figsize=(12, 4))
# Hit Rate
plt.subplot(1, 3, 1)
plt.plot(results["chunk_size"], results["hit_rate"], marker='o')
plt.xlabel("chunk_size")
plt.ylabel("Hit Rate")
plt.title("Hit Rate vs chunk_size")
plt.grid(True, alpha=0.3)
# 平均块数
plt.subplot(1, 3, 2)
plt.plot(results["chunk_size"], results["avg_chunks"], marker='s', color='orange')
plt.xlabel("chunk_size")
plt.ylabel("平均块数")
plt.title("平均块数 vs chunk_size")
plt.grid(True, alpha=0.3)
# 平均长度
plt.subplot(1, 3, 3)
plt.plot(results["chunk_size"], results["avg_length"], marker='^', color='green')
plt.xlabel("chunk_size")
plt.ylabel("平均长度(字符)")
plt.title("平均长度 vs chunk_size")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("./outputs/chunk_size_optimization.png", dpi=300)
plt.show()
return results
def evaluate_retrieval(nodes, queries, relevant_docs):
"""评估检索质量"""
# 简化版评估
# 实际应使用真实检索器
return {"hit_rate": 0.7} # 占位
chunk_overlap优化¶
# 文件名:07_07_overlap_optimization.py
"""
chunk_overlap优化实验
"""
def optimize_overlap(documents, chunk_size=1000):
"""
优化chunk_overlap参数
Args:
documents: 文档列表
chunk_size: 块大小
Returns:
最优overlap值
"""
print("="*60)
print("chunk_overlap优化实验")
print("="*60 + "\n")
# 测试不同的overlap
overlaps = [0, 50, 100, 150, 200, 250, 300]
results = []
for overlap in overlaps:
print(f"测试 overlap={overlap}")
# 分块
splitter = SentenceSplitter(
chunk_size=chunk_size,
chunk_overlap=overlap
)
nodes = splitter.get_nodes_from_documents(documents)
# 评估
# 这里简化评估,实际需要检索测试
score = len(nodes) / overlap if overlap > 0 else len(nodes)
results.append({
"overlap": overlap,
"num_chunks": len(nodes),
"score": score
})
print(f" 块数: {len(nodes)}")
# 选择最优
best = max(results, key=lambda x: x["score"])
print(f"\n✅ 最优 overlap: {best['overlap']}")
return best["overlap"]
7.4 分块策略对比实验¶
完整对比¶
# 文件名:07_08_strategy_comparison.py
"""
分块策略完整对比
"""
from llama_index.core.node_parser import (
SentenceSplitter,
SemanticSplitterNodeParser,
RecursiveCharacterTextSplitter
)
def compare_strategies(document, queries):
"""
对比不同分块策略
Args:
document: 测试文档
queries: 测试查询
"""
print("="*70)
print("分块策略完整对比")
print("="*70 + "\n")
strategies = {
"固定长度": SentenceSplitter(
chunk_size=500,
chunk_overlap=0
),
"固定+重叠": SentenceSplitter(
chunk_size=500,
chunk_overlap=50
),
"递归分块": RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", " ", ""]
)
# 语义分块需要API密钥
}
comparison = {}
for name, splitter in strategies.items():
print(f"\n{'='*70}")
print(f"策略: {name}")
print('='*70)
# 分块
nodes = splitter.get_nodes_from_documents([document])
# 显示信息
print(f"块数: {len(nodes)}")
print(f"平均长度: {np.mean([len(n.text) for n in nodes]):.0f}")
print(f"长度标准差: {np.std([len(n.text) for n in nodes]):.0f}")
# 评估(简化)
# 实际应该进行真实检索测试
score = len(nodes) / (1 + np.std([len(n.text) for n in nodes]))
comparison[name] = {
"num_chunks": len(nodes),
"avg_length": np.mean([len(n.text) for n in nodes]),
"score": score
}
# 总结对比
print("\n" + "="*70)
print("策略对比总结")
print("="*70)
print(f"\n{'策略':<12} {'块数':<8} {'平均长度':<12} {'评分':<10}")
print("-" * 70)
for name, metrics in comparison.items():
print(f"{name:<12} {metrics['num_chunks']:<8} "
f"{metrics['avg_length']:<12.0f} {metrics['score']:<10.2f}")
return comparison
# 使用示例
if __name__ == "__main__":
from llama_index.core import Document
doc = Document(text="""
人工智能(AI)是计算机科学的重要分支。
它的研究目标是创建能够模拟人类智能的计算机系统。
机器学习是AI的核心技术之一,通过算法让计算机从数据中学习。
深度学习是机器学习的子领域,使用多层神经网络。
自然语言处理(NLP)是AI的重要应用领域。
计算机视觉让机器能够"看懂"图像和视频。
语音识别和语音合成也是AI的重要应用。
AI技术正在改变我们的生活方式。
""")
queries = ["AI的核心技术有哪些?", "深度学习是什么?"]
results = compare_strategies(doc, queries)
总结¶
本章要点回顾¶
- 分块的重要性
- 影响检索质量20%+
- 保持语义完整性
-
避免信息切断
-
5种高级策略
- 语义分块:保持语义完整
- 上下文分块头:添加位置信息
- 递归分块:多级分隔符
- 父文档检索:保持父子关系
-
代码分块:保持函数完整
-
参数优化
- chunk_size: 根据内容类型选择
- chunk_overlap: 通常为10-20%
- 分隔符: 递归使用多种分隔符
学习检查清单¶
- 理解分块策略的重要性
- 掌握5种高级分块方法
- 能够选择合适的策略
- 完成参数优化实验
- 实际应用到项目中
下一步学习¶
- 下一章:第8章:查询增强技术
- 提示:优化完数据,接下来优化查询
实践练习¶
- 基础练习
- 在你的数据上测试5种分块策略
- 对比Hit Rate差异
-
选择最优策略
-
进阶练习
- 组合多种策略
- 创建自定义分块器
-
可视化分块效果
-
挑战项目
- 实现自适应分块
- 优化领域特定文档
- 达到Hit Rate > 0.80
返回目录 | 上一章:嵌入模型深入 | 下一章:查询增强技术
本章结束
合适的分块是数据质量的基础。选择正确的分块策略可以让你的RAG系统性能提升5-10%,这是最容易实现也是最容易被忽视的优化!