RAG 中的 Chunk 切分策略
在构建 RAG(Retrieval-Augmented Generation)系统时,Chunk 切分是最容易被忽视但影响最关键的步骤之一。
很多人把 RAG 的失败归咎于:
LLM 模型不够强
检索算法不先进
向量数据库选型错误
但真正的常见原因是:
Chunk 切分策略选择不当
本文将系统讲清楚:
为什么 RAG 必须进行 chunk 切分
5 种主流 chunking 方法及适用场景
chunk_size 和 chunk_overlap 如何配置
不同文档类型应该用什么策略
如何评估和优化 chunk 效果
生产环境的工程化实践
一、为什么 RAG 必须进行 Chunk 切分?
1.1 Embedding 模型的上下文窗口限制
所有 embedding 模型都有最大输入长度:
OpenAI text-embedding-3-small: 8191 tokens
OpenAI text-embedding-3-large: 8191 tokens
Cohere embed-english-v3.0: 512 tokens
Llama 2 embeddings: 2048 tokens
如果输入超过这个限制:
超出的 tokens 会被截断
重要上下文信息可能丢失
检索质量严重下降
1.2 检索精度 vs 上下文完整性的权衡
假设有一个 50 页的技术文档:
如果整个文档作为一个 chunk:
✅ 上下文完整
❌ 向量表示过于"平均化"
❌ 检索时可能召回不相关的内容
如果切分成 500 字符的小块:
✅ 检索精准
❌ 单个 chunk 可能缺少必要上下文
❌ LLM 生成答案时信息不完整
这就是:
RAG 的核心矛盾 - 召回粒度 vs 信息完整性
1.3 长上下文 LLM 也不能解决全部问题
即使使用 Claude 4 Sonnet(200k 上下文):
Lost in the Middle 问题 :埋藏在长文档中间的相关信息被忽略
延迟增加 :处理更长输入,推理变慢
成本上升 :更多 tokens = 更高的 API 费用
因此,chunk 切分仍然是必要的。
二、Chunk 切分的 5 种主流方法
2.1 Fixed-size Chunking(固定大小切分)
原理
设定一个固定的 token 数量或字符数,简单粗暴地切分。
优点
✅ 实现简单,一行代码搞定
✅ 性能可预测
✅ 适合大多数通用场景
缺点
❌ 可能切分在句子中间
❌ 不考虑语义边界
❌ 同一话题可能被分散到多个 chunk
代码示例(LangChain)
1 2 3 4 5 6 7 8 9 from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter( chunk_size=1000 , chunk_overlap=200 , length_function=len , ) chunks = text_splitter.split_text(your_text)
适用场景
2.2 Content-aware Chunking(内容感知切分)
原理
利用文档的自然结构(段落、章节、标题)来切分。
子方法
a. 句子级切分
1 2 3 4 5 import nltkfrom langchain_text_splitters import NLTKTextSplittertext_splitter = NLTKTextSplitter(chunk_size=1000 , chunk_overlap=200 ) chunks = text_splitter.split_text(your_text)
优点 :保持语义完整性
缺点 :句子长短不一,chunk 大小不固定
b. 段落级切分
1 2 3 4 5 text_splitter = RecursiveCharacterTextSplitter( separators=["\n\n" , "\n" , " " , "" ], chunk_size=1000 , chunk_overlap=200 , )
优先级 :双换行(段落)→ 单换行(行)→ 空格 → 字符
优点 :保留文档结构
缺点 :某些段落可能很长
适用场景
有清晰结构的文章
Markdown/HTML 文档
需要语义连贯性的场景
2.3 Recursive Character Chunking(递归字符切分)
原理
按分隔符优先级递归切分,直到达到目标大小。
工作流程
1 2 3 4 5 6 7 8 9 原文 ↓ (尝试 \n\n 切分) 大段落列表 ↓ (如果段落太长,尝试 \n 切分) 行列表 ↓ (如果行太长,尝试 空格 切分) 单词列表 ↓ (如果单词太多,按字符切分) 最终 chunks
LangChain 实现
1 2 3 4 5 6 7 8 9 10 from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter( chunk_size=1000 , chunk_overlap=200 , separators=["\n\n" , "\n" , " " , "" ], add_start_index=True , ) chunks = text_splitter.split_documents(documents)
为什么是"推荐选择"?
平衡性 :既考虑了结构,又控制了大小
通用性 :适用于大多数文本类型
LangChain 默认 :有良好的生态支持
2.4 Semantic Chunking(语义切分)
原理
使用 embedding 计算语义相似度,在主题转变处切分。
实现步骤
1 2 3 4 5 1. 将文档切分成句子 2. 计算每句的 embedding 3. 滑动窗口组合句子 4. 计算相邻组合的相似度 5. 在相似度突降处切分(主题转换)
代码示意
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from sentence_transformers import SentenceTransformerimport numpy as npmodel = SentenceTransformer('all-MiniLM-L6-v2' ) sentences = [s.strip() for s in text.split('.' ) if s.strip()] embeddings = model.encode(sentences) similarities = np.dot(embeddings[:-1 ], embeddings[1 :].T) threshold = 0.3 chunks = [] current_chunk = [sentences[0 ]] for i in range (1 , len (sentences)): if similarities[i-1 ] < threshold: chunks.append('. ' .join(current_chunk)) current_chunk = [sentences[i]] else : current_chunk.append(sentences[i]) chunks.append('. ' .join(current_chunk))
优点
✅ 语义完整性最好
✅ 每个 chunk 都是自包含的主题
✅ 召回质量高
缺点
❌ 计算成本高(需要 embedding)
❌ 处理速度慢
❌ chunk 大小不均匀
适用场景
高质量要求 的 RAG 系统
知识库问答
学术文档检索
2.5 Document Structure Chunking(文档结构切分)
原理
解析文档的原始格式,按结构元素切分。
各类文档的处理
PDF 文档
识别标题层级(H1, H2, H3)
按章节切分
保留表格、图片位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from langchain_community.document_loaders import PyPDFLoaderfrom langchain_text_splitters import MarkdownHeaderTextSplitterloader = PyPDFLoader("document.pdf" ) docs = loader.load() markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=[ ("#" , "Header 1" ), ("##" , "Header 2" ), ("###" , "Header 3" ), ] ) splits = markdown_splitter.split_text(markdown_text)
HTML 文档
解析 <h1>, <h2>, <h3> 标签
识别 <p>, <ul>, <table> 等块级元素
保留链接和元数据
1 2 3 4 5 6 7 8 9 10 11 from langchain_text_splitters import HTMLHeaderTextSplitterhtml_splitter = HTMLHeaderTextSplitter( headers_to_split_on=[ ("h1" , "Header 1" ), ("h2" , "Header 2" ), ("h3" , "Header 3" ), ] ) splits = html_splitter.split_text(html_content)
Markdown 文档
识别 #, ##, ### 标题
按章节切分
保留代码块
代码文件
优点
✅ 保留原始结构
✅ 上下文最完整
✅ 适合专业文档
缺点
❌ 需要特定解析器
❌ 实现复杂度高
❌ 不同格式需要不同策略
适用场景
2.6 Contextual Chunking(上下文切分)
原理
为每个 chunk 生成上下文摘要,保持全局信息。
Anthropic 的 Contextual Retrieval
1 2 3 4 5 1. 读取完整文档 2. 切分成基础 chunks 3. 用 LLM 为每个 chunk 生成简短上下文描述 4. 将描述添加到 chunk 内容中 5. 对增强后的 chunk 进行 embedding
示例
原始 chunk:
1 向量数据库使用索引加速搜索。FAISS 是一个高效的近似最近邻搜索库。
添加上下文后:
1 2 上下文:本文介绍向量数据库和检索技术,重点是性能优化 内容:向量数据库使用索引加速搜索。FAISS 是一个高效的近似最近邻搜索库。
优点
✅ 每个 chunk 都有全局上下文
✅ 召回质量最高
✅ 减少检索噪声
缺点
❌ 成本最高(需要 LLM 生成上下文)
❌ 处理速度最慢
❌ 文档更新后需要重新生成
适用场景
三、关键参数配置
3.1 chunk_size(块大小)
常见选择
chunk_size
适用场景
优点
缺点
128-256 tokens
高精度检索、细粒度问题
召回精准
上下文不足
512 tokens
平衡选择
通用性好
需要仔细调优
1000-1500 tokens
问答系统、长文档
上下文丰富
召回范围大
2000+ tokens
长文档总结
信息完整
精度下降
选择原则
1 2 3 4 5 6 7 8 9 10 11 1. 看用户查询类型 - 短具体问题 → 较小 chunk(256-512) - 开放性问题 → 较大 chunk(1000+) 2. 看文档类型 - 技术文档 → 较小 chunk(精确) - 叙事文章 → 较大 chunk(连贯) 3. 看 LLM 上下文窗口 - 确保总召回 tokens < LLM 上下文 * 50% - 给 Prompt 预留空间
经验公式
1 2 3 4 5 6 chunk_size ≈ (目标召回数量 × 平均答案长度) / 2 示例: - 目标召回 3 个 chunk - 平均答案 300 tokens - chunk_size ≈ (3 × 300) / 2 = 450 tokens
3.2 chunk_overlap(块重叠)
为什么需要 overlap?
假设有两个句子:
1 2 Chunk A: "用户可以使用 API 密钥访问服务..." Chunk B: "API 密钥应该定期更..."
用户问:“如何管理 API 密钥?”
如果没有 overlap:
可能只召回 Chunk B
但 A 包含了"访问服务"的信息
答案不完整
有了 overlap:
常见 overlap 设置
overlap 比例
适用场景
10%
文档连贯性强、句子长
15-20%
通用推荐
25%+
文档碎片化、需要更多冗余
经验公式
1 2 3 4 5 chunk_overlap = chunk_size × 0.15 ~ 0.20 示例: - chunk_size = 1000 - overlap = 150 ~ 200
重要提示
overlap 不是越大越好:
过小:信息丢失
适中: 最佳平衡点(15-20%)
过大:冗余信息多,检索噪声增加
四、不同文档类型的策略选择
4.1 文档类型决策树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 开始 ↓ 文档是代码吗? ├─ 是 → 按函数/类切分 └─ 否 ↓ 文档有清晰结构吗(标题、章节)? ├─ 是 → 文档结构切分 └─ 否 ↓ 是长文档吗(>50 页)? ├─ 是 → Semantic + Fixed-size 混合 └─ 否 ↓ 是通用文本吗? ├─ 是 → Recursive Character(推荐) └─ 否 → 根据特点定制
4.2 具体策略
技术文档
方法 :文档结构切分 + 适度 overlap
chunk_size :500-800 tokens
overlap :15%
理由 :保持上下文,又保证精度
学术论文
方法 :章节切分(Abstract, Introduction, Conclusion 分开)
chunk_size :1000-1500 tokens
overlap :20%
理由 :每个章节是独立主题
新闻文章
方法 :递归字符切分
chunk_size :256-512 tokens
overlap :10%
理由 :新闻主题集中,小块即可
对话记录
方法 :按对话轮次切分
chunk_size :3-5 轮对话
overlap :1 轮
理由 :对话有自然边界
代码库
方法 :按文件 + 函数切分
chunk_size :1-3 个函数
overlap :0
理由 :代码有清晰结构,不需要 overlap
五、如何评估 Chunk 效果
5.1 评估指标
召回质量
手工评估:
准备 50-100 个测试问题
检查召回的 chunks 是否包含答案
计算召回率(Recall@k)
自动化评估:
1 2 3 4 5 6 7 8 9 10 from sklearn.metrics import recall_scoredef evaluate_recall (retrieved_chunks, gold_chunks, k=5 ): """计算 Top-k 召回率""" hits = sum (1 for gold in gold_chunks if gold in retrieved_chunks[:k]) return hits / len (gold_chunks) recall_3 = evaluate_recall(retrieved, gold_standard, k=3 ) recall_5 = evaluate_recall(retrieved, gold_standard, k=5 )
检索效率
平均响应时间 :应该 < 500ms
数据库大小 :chunk 数量可控
索引构建时间 :可接受范围内
5.2 A/B 测试框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 strategies = { "fixed_500" : RecursiveCharacterTextSplitter(chunk_size=500 , overlap=100 ), "fixed_1000" : RecursiveCharacterTextSplitter(chunk_size=1000 , overlap=200 ), "semantic" : SemanticChunker(threshold=0.3 ), } for strategy_name, splitter in strategies.items(): chunks = splitter.split_documents(docs) vector_store.index_documents(chunks) results = run_test_set(test_questions, vector_store) print (f"{strategy_name} : Recall={results['recall' ]:.3 f} " )
5.3 优化流程
1 2 3 4 5 6 7 1. 建立测试集(50-100 个真实问题) 2. 实现多种 chunk 策略 3. 并行测试,记录指标 4. 选择表现最好的策略 5. 在生产环境小规模测试 6. 收集用户反馈 7. 迭代优化
六、生产环境工程化实践
6.1 配置管理
配置示例(YAML)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 chunking: strategy: recursive chunk_size: 1000 chunk_overlap: 200 separators: - "\n\n" - "\n" - " " - "" document_types: markdown: strategy: structural chunk_size: 800 pdf: strategy: structural chunk_size: 1000 code: strategy: file_based chunk_size: 500
6.2 Chunk 扩展(Chunk Expansion)
原理
检索到 chunks 后,自动扩展其上下文。
实现方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def expand_chunks (retrieved_chunks, all_chunks, window=1 ): """扩展召回的 chunks,包含前后邻居""" expanded = [] for chunk in retrieved_chunks: idx = all_chunks.index(chunk) start = max (0 , idx - window) end = min (len (all_chunks), idx + window + 1 ) expanded_chunk = " " .join( c.page_content for c in all_chunks[start:end] ) expanded.append(expanded_chunk) return expanded
优点
✅ 低延迟检索(不增加索引)
✅ 高质量上下文
✅ 灵活性强
缺点
❌ 增加 LLM 输入 tokens
❌ 可能引入无关信息
6.3 缓存策略
Embedding 缓存
1 2 3 4 5 6 from functools import lru_cache@lru_cache(maxsize=10000 ) def get_embedding (text: str ): """缓存 embedding 结果""" return embedding_model.embed(text)
Chunk 缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import hashlibdef chunk_cache_key (text: str , size: int , overlap: int ): """生成 chunk 唯一标识""" data = f"{text} :{size} :{overlap} " .encode() return hashlib.md5(data).hexdigest() cache_key = chunk_cache_key(original_text, 1000 , 200 ) if cached := redis.get(cache_key): chunks = json.loads(cached) else : chunks = splitter.split_text(original_text) redis.set (cache_key, json.dumps(chunks), ex=3600 )
七、常见问题与解决方案
Q1: 如何处理多语言文档?
问题 :中英文混合文档,切分可能出问题。
解决方案 :
1 2 3 4 5 6 7 8 from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter( chunk_size=1000 , chunk_overlap=200 , separators=["\n\n" , "\n" , "。" , "!" , "?" , " " , "" ], )
Q2: 如何处理表格和图片?
问题 :表格被切分后信息不完整。
解决方案 :
1 2 3 4 5 6 7 8 def is_table (chunk: str ) -> bool : return "|" in chunk and "\n|" if is_table(chunk): chunks.append(extract_full_table(document))
Q3: 如何动态调整 chunk 大小?
问题 :固定大小不能适应所有文档。
解决方案 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def adaptive_chunking (document: str ): """根据文档内容动态调整""" if is_technical_doc(document): size = 500 overlap = 100 elif is_news_article(document): size = 300 overlap = 50 else : size = 1000 overlap = 200 splitter = RecursiveCharacterTextSplitter( chunk_size=size, chunk_overlap=overlap, ) return splitter.split_text(document)
Q4: 如何处理实时更新的文档?
问题 :文档更新后,chunks 需要重新生成。
解决方案 :
1 2 3 4 5 6 7 8 9 10 def document_version (doc: str ) -> str : return hashlib.sha256(doc.encode()).hexdigest() if current_version != stored_version: chunks = splitter.split_text(doc) vector_store.update(document_id, chunks) redis.set (f"doc:{doc_id} :version" , current_version)
八、最终总结
核心原则
没有银弹 :不同场景需要不同策略
从简单开始 :先 Fixed-size,再迭代优化
数据驱动 :用测试集评估,不要凭感觉
工程化优先 :配置化、缓存、监控
推荐流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 第一阶段(MVP): → 使用 RecursiveCharacterTextSplitter → chunk_size=1000, overlap=200 → 快速验证 RAG 流程 第二阶段(优化): → 根据文档类型定制策略 → 调整参数 → 建立 A/B 测试 第三阶段(生产): → 配置化策略 → 添加缓存 → 监控指标 → 持续优化
一句话总结
Chunk 切分不是技术问题,而是产品设计问题
关键不是"怎么切",而是:
“为你的用户和文档类型,选择什么策略”
参考资源: