学习笔记:详述 RAG 系统中文本分块的核心原理、主流策略、优化技巧以及工程实践
目录
- 为什么分块至关重要
- 分块的基本概念
- 分块的核心参数
- 分块大小的影响
- 分块策略详解
- 固定长度分块
- 递归字符分块
- 语义分块
- 结构分块
- 标题层级分块
- 句子分块
- LLM 分块
- 高级分块技术
- 重叠分块
- 父文档分块
- 小结分块
- 不同文档类型的分块策略
- Markdown 文档
- PDF 文档
- 代码文件
- HTML 文档
- 分块优化实践
- 如何选择合适的分块策略
- 分块参数的动态调整
- 评估分块效果
- 常见问题与解决方案
- 工程工具推荐
- 参考资料
为什么分块至关重要
文本分块(Chunking)是 RAG 系统的第一道门槛,直接影响后续检索和生成的效果。
分块决定检索质量
| 分块问题 | 检索后果 |
|---|---|
| Chunk 太小 | 上下文不完整,关键信息被切散 |
| Chunk 太大 | 引入噪声,稀释关键信息 |
| 分块方式不当 | 语义单元被破坏,检索不到相关内容 |
分块影响生成质量
- 上下文完整:合理的分块能保留完整的语义单元,LLM 更容易理解
- 信息密度:高信息密度的 chunk 能提高生成质量
- 引用追溯:清晰的 chunk 边界便于生成时引用原始文档
分块的基本概念
分块的核心参数
| 参数 | 说明 | 常用值 |
|---|---|---|
| chunk_size | 每个 chunk 的最大长度 | 256~2048 tokens |
| chunk_overlap | 相邻 chunk 之间的重叠 token 数 | 10%~20% chunk_size |
| separator | 分隔符 | 换行符、句子边界、段落标记 |
| length_function | 长度计算方式 | token 数量、字符数 |
分块大小的影响
分块大小选择指南
| 场景 | 推荐 chunk 大小 | 理由 |
|---|---|---|
| 代码检索 | 256~512 tokens | 代码逻辑紧凑,需要精确匹配 |
| 短问答 | 256~512 tokens | 问题简单,不需要过多上下文 |
| 文档问答 | 512~1024 tokens | 平衡上下文与精确度 |
| 长文档摘要 | 1024~2048 tokens | 需要完整上下文 |
| 多步骤推理 | 512~1024 tokens | 保留推理链条 |
分块策略详解
1. 固定长度分块
原理:按固定的字符数或 token 数切分文本
# 伪代码示例text="这是一段很长的文本..."chunk_size=500chunks=[]foriinrange(0,len(text),chunk_size):chunks.append(text[i:i+chunk_size])优点:
- 实现简单,逻辑清晰
- 处理速度快
- 输出 chunk 大小一致,便于管理
缺点:
- 可能切断句子、段落等语义单元
- 不考虑文本结构
- 可能丢失关键上下文
适用场景:
- 文本格式单一、结构简单
- 对处理速度要求高
- 初步原型验证
改进方向:
- 尽量在句子边界切分
- 保留一定的重叠
2. 递归字符分块
原理:按层级递归切分,尝试多种分隔符
LangChain 实现:
fromlangchain.text_splitterimportRecursiveCharacterTextSplitter text_splitter=RecursiveCharacterTextSplitter(separators=["\n\n","\n","。","!","?"," ",""],chunk_size=500,chunk_overlap=50,length_function=len,)分隔符优先级:
| 优先级 | 分隔符 | 说明 |
|---|---|---|
| 1 | \n\n | 段落分隔,保持最大语义完整性 |
| 2 | \n | 换行符,常用于列表 |
| 3 | 。!? | 句子结束符(中英文) |
| 4 | .!? | 英文句子结束符 |
| 5 | 单词边界 | |
| 6 | "" | 字符级别(兜底) |
优点:
- 尽可能保持语义完整
- 灵活适应不同文本结构
- 业界最常用的分块策略
缺点:
- 参数调优需要经验
- 对特殊文档格式效果可能不佳
适用场景:
- 通用文档处理
- 文本结构多样
- 生产环境首选
3. 语义分块
原理:基于文本语义相似性进行分块,将语义相近的内容聚合在一起
实现方式:
| 方式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| Embedding 聚类 | 用 Embedding 表示句子,聚类后分块 | 语义准确 | 计算成本高 |
| 滑动窗口 | 滑动窗口计算局部相似度 | 效率较高 | 窗口大小难确定 |
| LLM 判断 | 用 LLM 判断是否应该分块 | 效果好 | 成本高、延迟大 |
LLM-as-a-Judge 分块示例:
# 伪代码:使用 LLM 判断分块边界defshould_split(text_a,text_b):prompt=f""" 判断以下两段文本是否应该分开成不同的 chunk: 文本 A:{text_a}文本 B:{text_b}如果两段文本讨论的是不同主题或话题,返回 "YES"。 如果两段文本主题一致,只是内容展开,返回 "NO"。 """response=llm.invoke(prompt)return"YES"inresponse.text优点:
- 分块边界更符合语义
- 保留完整的主题内容
- 检索质量更高
缺点:
- 计算成本较高
- 实现复杂度大
- 需要调优阈值
适用场景:
- 对检索质量要求高
- 主题分明的长文档
- 计算资源充足
4. 结构分块
原理:根据文档的显式结构(标题、层级、章节)进行分块
LangChain 实现:
fromlangchain.text_splitterimportMarkdownTextSplitter splitter=MarkdownTextSplitter(chunk_size=500,chunk_overlap=50,)优点:
- 保留文档结构信息
- 分块边界符合阅读习惯
- 便于追溯原文位置
缺点:
- 依赖文档格式
- 无结构文档不适用
- 需要提取文档结构
适用场景:
- Markdown、reStructuredText 等结构化文档
- 技术文档、API 文档
- 有明确章节结构的文档
5. 标题层级分块
原理:按照文档的标题层级(如 H1、H2、H3)进行分块
优点:
- 保留层级上下文
- 便于定位和引用
- 用户体验好
缺点:
- 只适用于 HTML 等有标题标记的文档
- 层级深度不好控制
适用场景:
- 网站文档
- 帮助中心
- 带目录的文档
6. 句子分块
原理:以完整句子为基本单元进行分块
fromlangchain.text_splitterimportSentenceTextSplitter splitter=SentenceTextSplitter(chunk_size=5,# 5 个句子chunk_overlap=2,# 2 个句子重叠)优点:
- 保持句子完整性
- 语义自然连贯
- 便于阅读和理解
缺点:
- 句子长度差异大
- 某些语言分句困难
- chunk 大小不均匀
适用场景:
- 小说、散文等叙事性文本
- 对句子完整性要求高的场景
7. LLM 驱动分块
原理:使用 LLM 智能判断分块边界
# 伪代码:LLM 驱动分块defllm_based_splitting(document):prompt=f""" 分析以下文档,识别主题边界,并给出分块建议:{document[:2000]}# 只发送开头部分 输出格式: - 用 "=== Section N ===" 标记每个新主题的开始 - 确保每个 section 主题内聚、主题间区分明显 """result=llm.invoke(prompt)returnparse_sections(result.text)优点:
- 语义理解最准确
- 可处理复杂文档
- 分块质量最高
缺点:
- 成本高
- 延迟大
- 实现复杂
适用场景:
- 顶级质量要求
- 复杂文档结构
- 预算充足
高级分块技术
1. 重叠分块(Overlapping Chunks)
核心思想:相邻 chunks 之间保留重叠区域,避免关键信息被切断
重叠比例选择:
| 场景 | 重叠比例 | 说明 |
|---|---|---|
| 通用场景 | 10-20% | 平衡效果与存储 |
| 重要内容 | 20-30% | 需要高召回 |
| 计算敏感 | 5-10% | 减少重复计算 |
重叠分块的优缺点:
| 优点 | 缺点 |
|---|---|
| 减少信息丢失 | 存储空间增加 |
| 关键信息更完整 | 向量数据库存储更多 |
| 检索召回率提高 | 检索可能返回重复内容 |
2. 父子文档分块
原理:创建多层级的 chunk,父 chunk 包含子 chunk
检索流程:
1. 查询 → 子 Chunk 匹配 2. 子 Chunk → 父 Chunk 3. 父 Chunk → LLM 生成(保留完整上下文)优点:
- 细粒度检索 + 完整上下文
- 平衡精确性和完整性
- 支持不同粒度的查询
实现要点:
- 建立父子映射关系
- 存储时保留层级信息
- 检索时同时返回父子 chunk
3. 小结分块(Summary Chunking)
原理:为每个 chunk 自动生成摘要,检索时使用摘要匹配
检索优化:
# 检索时同时匹配原始文本和摘要query_vector=embed(query)# 方式1:分别检索后融合results_text=vector_db.search(query_vector,index="text")results_summary=vector_db.search(query_vector,index="summary")final_results=fusion_merge(results_text,results_summary)# 方式2:扩展查询expanded_query=f"{query}{summary}"优点:
- 摘要更精确匹配查询
- 提高检索召回率
- 支持多角度检索
缺点:
- 生成摘要有成本
- 存储空间增加
- 摘要质量依赖 LLM
不同文档类型的分块策略
Markdown 文档
特点:
- 有明确的标题层级
- 代码块需要特殊处理
- 列表、表格结构化
推荐策略:
fromlangchain.text_splitterimportMarkdownTextSplitter# 方式1:Markdown 专用分块器markdown_splitter=MarkdownTextSplitter(chunk_size=500,chunk_overlap=50,)# 方式2:组合策略fromlangchain.text_splitterimport(MarkdownHeaderTextSplitter,RecursiveCharacterTextSplitter,)# 先按标题分割header_splitter=MarkdownHeaderTextSplitter(headers_to_split_on=["#","##","###",])md_chunks=header_splitter.split_text(markdown_text)# 再对每个部分递归分块recursive_splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,)final_chunks=recursive_splitter.split_documents(md_chunks)注意事项:
- 代码块应作为独立 chunk 或排除
- 表格内容需要特殊解析
- 保持标题与内容的关联
PDF 文档
特点:
- 版面结构复杂
- 可能包含图片、表格
- 提取时可能丢失格式
推荐策略:
# 使用 PDF 专用加载器fromlangchain_community.document_loadersimportPyPDFLoader loader=PyPDFLoader("document.pdf")pages=loader.load()# 按页面或段落分块fromlangchain.text_splitterimportRecursiveCharacterTextSplitter splitter=RecursiveCharacterTextSplitter(chunk_size=500,chunk_overlap=50,separators=["\n\n","\n","。"," "],)chunks=splitter.split_documents(pages)PDF 提取工具对比:
| 工具 | 优点 | 缺点 |
|---|---|---|
| PyPDFLoader | 简单易用 | 格式保留有限 |
| PDFPlumber | 表格提取好 | 速度较慢 |
| PyMuPDF | 性能好 | 依赖特殊处理 |
| Unstructured | 智能分块 | 资源消耗大 |
代码文件
特点:
- 有明确的语法结构
- 函数、类有清晰边界
- 注释与逻辑需要分离
推荐策略:
fromlangchain.text_splitterimportLanguage# 按编程语言选择分块器python_splitter=RecursiveCharacterTextSplitter.from_language(language=Language.PYTHON,chunk_size=500,chunk_overlap=50,)# 或使用代码专用分块器fromlangchain.text_splitterimportCodeTextSplitter code_splitter=CodeTextSplitter(language="python",chunk_size=500,chunk_overlap=50,)代码分块策略:
优先级: 1. 类/函数定义(最高) 2. 代码块 3. 段落 4. 单行(兜底)注意事项:
- 保持函数/类的完整
- 保留必要的上下文
- 处理跨文件引用
HTML 文档
特点:
- DOM 结构明确
- 内容与样式混合
- 需要提取正文
fromlangchain.text_splitterimportHTMLHeaderTextSplitter# 按标题层级分割html_splitter=HTMLHeaderTextSplitter(headers_to_split_on=[("h1","Header 1"),("h2","Header 2"),("h3","Header 3"),("h4","Header 4"),])html_chunks=html_splitter.split_text(html_string)分块优化实践
如何选择合适的分块策略
策略选择矩阵
| 文档类型 | 推荐策略 | 备选策略 | 关键参数 |
|---|---|---|---|
| 技术文档 | 递归分块 + 结构 | 语义分块 | chunk_size=500-800 |
| 论文报告 | 递归分块 | 句子分块 | chunk_size=800-1000 |
| 聊天记录 | 句子分块 | 固定长度 | chunk_size=300-500 |
| 代码文件 | 代码专用分块 | 递归分块 | 按函数/类边界 |
| 网页内容 | HTML 分块 | 语义分块 | 保留标题层级 |
| 法律文档 | 递归分块 | 父子文档 | chunk_size=1000+,大重叠 |
分块参数的动态调整
基于查询类型的自适应分块:
defadaptive_chunking(query,document):# 分析查询类型query_type=classify_query(query)ifquery_type=="factoid":# 事实型查询:需要精确小块chunk_size=256overlap=50elifquery_type=="summary":# 摘要型查询:需要较大块chunk_size=1024overlap=100elifquery_type=="comparison":# 对比型查询:中等大小chunk_size=512overlap=80returnsplit_with_config(document,chunk_size,overlap)基于文档特征的自适应:
defanalyze_and_split(document):# 分析文档特征length=len(document)has_structure=detect_structure(document)avg_sentence_length=compute_avg_sentence_length(document)# 动态调整iflength<1000:# 短文档不分块return[document]elifhas_structureandlength>10000:# 长文档 + 有结构 → 父子分块returnparent_document_split(document)elifavg_sentence_length>50:# 长句子 → 句子分块returnsentence_split(document)else:# 默认递归分块returnrecursive_split(document)评估分块效果
关键指标:
| 指标 | 说明 | 评估方法 |
|---|---|---|
| 召回率 | 相关内容被检索到的比例 | 人工标注测试集 |
| 精确率 | 检索结果中相关内容的比例 | 人工标注测试集 |
| 上下文完整性 | chunk 是否保留完整语义 | 人工评估 |
| 块均信息量 | 每个 chunk 的信息密度 | 自动化指标 |
A/B 测试框架:
defevaluate_chunking_strategy(strategy,test_queries):results=[]forqueryintest_queries:# 检索retrieved_chunks=retrieve(query)# 评估relevance_scores=human_rate(retrieved_chunks,query)results.append({"query":query,"chunks":retrieved_chunks,"relevance":relevance_scores,})# 汇总metrics={"hit_rate":compute_hit_rate(results),"mrr":compute_mrr(results),"avg_relevance":compute_avg_relevance(results),}returnmetrics常见调优信号:
| 信号 | 问题 | 调整方向 |
|---|---|---|
| 检索不到相关内容 | chunk 太大 | 减小 chunk_size |
| 上下文不完整 | chunk 太小 | 增加 chunk_size + overlap |
| 检索到太多噪声 | chunk 太大或语义散 | 减小 size + 优化结构 |
| 相邻块主题跳跃 | 分块边界不对 | 改用递归/语义分块 |
常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 检索不到 | chunk 太大,关键词被稀释 | 减小 chunk_size |
| 检索太多噪声 | chunk 太大 | 减小 chunk_size,添加重排序 |
| 上下文不完整 | chunk 太小 | 增加 chunk_size,添加重叠 |
| 主题跳跃 | 分块不合理 | 使用递归/语义分块 |
| 关键信息被切分 | 分块边界不佳 | 增加 overlap,父子分块 |
| 代码被切断 | 未使用代码专用分块 | 使用 CodeTextSplitter |
| 表格信息丢失 | 表格处理不当 | 使用表格专用提取器 |
工程工具推荐
主流分块工具
| 工具 | 特点 | 适用场景 |
|---|---|---|
| LangChain TextSplitters | 丰富、支持多种语言 | 通用场景 |
| LlamaIndex NodeParser | 强大、配置灵活 | 高级用户 |
| Unstructured | 智能、端到端 | 复杂文档 |
| Spacy | NLP 能力 | 语义分块 |
组合使用建议
基础方案:LangChain RecursiveCharacterTextSplitter ↓ 优化方案:LlamaIndex NodeParser(更精细控制) ↓ 高级方案:Unstructured(复杂文档) ↓ 终极方案:自定义 + LLM 辅助参考资料
LangChain Text Splitters
https://python.langchain.com/docs/modules/data_connection/document_transformers/LlamaIndex Node Parsers
https://docs.llamaindex.ai/en/latest/api_reference/node_parsers.htmlSemantic Chunking for RAG
https://www.pinecone.io/blog/semantic-chunkingChunking Strategies for RAG
https://github.com/run-llama/llama_index/blob/main/docs/docs/module_guides/loading/node_parser.mdAdvanced RAG Techniques: Chunking
https://www.anyscale.com/blog/chunking-strategies-for-rag