chunk 方式不对,embedding 模型再强也白费。你的 RAG 系统里,90% 的检索失败是 chunk 切出来的问题,不是搜的问题。
一个花了三天才定位到的 bug
有个团队做了个论文阅读 RAG 系统。工程师花了大量精力调 embedding 模型、试各种向量数据库、优化 prompt……但检索质量始终上不去。
问题出在哪?用户问的问题,检索出来的文档片段要么不完整、要么夹杂不相关信息。
我让他们随便找一篇 PDF 原文看看 chunk 结果。一看就发现了问题——他们的 PDF 解析器按固定 500 字符切分,切到图片/表格/公式时,直接从中间拦腰切断。结果呢?
Chunk 42: "...而 Transformer 架构的核心创新在于自注意力机" Chunk 43: "制和位置编码。我们提出了一种新的训练方" Chunk 44: "...法,在 WMT2014 英德语对上获得了 28.4 BLEU"一个完整的核心段落被切成三块。用户的 query 如果恰好匹配的是 chunk 43 里的内容,这个 chunk 去检索,它既没有开头也没有结尾,信息量少了一大半。
换了三种 chunk 策略跑了一轮对比:
| 策略 | Recall@5 | 有效回答率 |
|---|---|---|
| 固定 500 字(无重叠) | 0.52 | 41% |
| 固定 500 字(20% 重叠) | 0.61 | 53% |
| 语义分块(按段落切) | 0.78 | 76% |
关键信息:有时候问题不在向量检索,在 chunk 切错了。
为什么 chunk 如此重要?
RAG 系统的检索链条是:文档 → chunk → embedding → 向量检索 → LLM 生成。
chunk 在这条链上处于"地基"的位置——后面的每一步都建立在你切出来的 chunk 上。
chunk 的好坏影响三个方面:
1. 语义完整性
每个 chunk 应该是一个自包含的语义单元。如果一段逻辑拆到两个 chunk 里,检索时只命中其中一个,LLM 得到的信息就是残缺的。
2. Chunk 数量
切得太碎 → chunk 数量爆炸 → 向量数据库膨胀 → 检索耗时增加 → 召回质量下降。
切得太粗 → 每个 chunk 包含太多无关信息 → embedding 向量被"平均化" → 相似度计算不准确。
3. LLM 上下文窗口
chunk 过大会挤占 LLM 的上下文窗口。比如 GPT-4 Turbo 有 128k 的窗口,但你塞 50 个 2000 字的 chunk,一下子就填满了,留给 system prompt 和对话历史的空间就少了。
这是 RAG 工程中最经典的"取舍"问题。下面我们看几种主流的 chunk 策略。
策略一:固定长度分块(Fixed-size Chunking)
一句话:最简单,但容易出错。
原理
按照固定的字符数(或 token 数)从头到尾切,通常会加上重叠窗口。这是所有方法里最"无脑"的。
deffixed_size_chunktext: str, chunk_size: int = 500, overlap: int = 50liststr""" 固定长度分块,带重叠窗口。 """0lenwhilemin# 滑动步长 = chunk_size - overlapreturn"这是一篇很长的文档。"20030030printf"总长度: {len(text)} 字 → {len(chunks)} 个 chunk"printf"Chunk 0 长度: {len(chunks[0])} 字"printf"Chunk 0: {chunks[0][:50]}..."LangChain 实现:
fromimport# 虽然叫 Recursive,实际上默认行为也是固定 token 数切50050"\n\n""\n""。""!""?"";"","" """优点
- 实现简单,零思考
- 速度快(O(n))
- 任何语言/格式都能用
缺点
- 可能从句子/段落中间切断
- 对文档结构无感知
- 重叠窗口虽然缓解了边界问题,但引入了重复数据
最佳实践
# 如果你要用固定长度分块,至少要做到:# 1. 在段落边界上切('\n\n')# 2. 只对"足够大"的段落做二次分割# 3. 选择合适的分隔符优先级500100# 推荐 chunk_size 的 10-20%"\n\n""\n""。""!""?"";"len什么时候用:快速原型、数据格式统一(如纯日志文本)、你知道内容结构规整。
策略二:递归字符分块(Recursive Character Chunking)
一句话:LangChain 默认方案,兼顾简单和效果。
原理
递归分块和固定长度分块的区别在于:它不是无脑切,而是尝试在段落/句子边界上切,如果一段太长,才递归地切小。
defrecursive_chunk_by_separators text: str, chunk_size: int = 500, chunk_overlap: int = 50, separators: list[str] = Noneliststr""" 递归分块:优先在高级分隔符(段落)上切, 不行再降级到低级分隔符(句子)。 """ifisNone"\n\n""\n""。""!""?"";"# 从第一级段落分隔符开始0""forin# 如果这一段太长,递归到下一级分隔符iflenif""# 用下一级分隔符递归切分1eliflenlen0else# 带上重叠内容if0else""0ifreturn实际使用直接用 LangChain 就好:
fromimport# 中文推荐的分隔符优先级50050"\n\n""\n""。""!""?"";"","" """Falsewithopen"document.md""r"asprintf"生成了 {len(chunks)} 个 chunk"# 效果比固定长度好 15-20%关键参数
- chunk_size: 500 是最常用的起点。英文按 token 算(~~200-300),中文按字符算(~~500-800)。
- chunk_overlap: chunk_size 的 10-20%。太小边界信息丢失,太大量太大。
- separators: 中文要加上"。" “!” "?“作为句子分隔符,否则递归会退化到"按字符切”。
什么时候用
绝大多数 RAG 场景的默认选择。如果不知道用什么,先用递归分块。
策略三:语义分块(Semantic Chunking)
一句话:不要按字符数切,要按"逻辑段落"切。
原理
语义分块的思想是:embedding 模型来判断两段文本是否属于同一个话题。如果两段文本的语义距离超过某个阈值,就在那里切开。
fromimportimportasdefsemantic_chunk text: str, model_name: str = "BAAI/bge-small-zh-v1.5", threshold: float = 0.65, min_chunk_size: int = 100, max_chunk_size: int = 1000liststr""" 语义分块:按句子之间的语义变化点来切分。 """# 1. 先按句子分割importr'(?<=[。!?\n])'forinififlen1return# 2. 计算相邻句子的语义相似度Trueforinrangelen11# 3. 在相似度下降超过阈值的地方切分0forinrange1len# 语义变化超过阈值,且当前 chunk 足够长if1andleneliflenlenelse# 超长了强制切ifreturn# 使用示例"""Transformer 架构是 2017 年提出的序列建模方法。它完全基于注意力机制,不使用循环和卷积。自注意力机制可以捕捉任意两个位置之间的关系。这使得它能够有效处理长距离依赖问题。BERT 是基于 Transformer 的预训练模型。GPT 也是基于 Transformer 的。这两种模型在应用上有显著差异。BERT 使用编码器架构,适合理解类任务。GPT 使用解码器架构,适合生成类任务。"""0.6forinenumerateprintf"\n--- Chunk {i} ({len(chunk)} 字) ---"print100跑一下看看效果——这个例子里,"Transformer 架构介绍"和"BERT/GPT 对比"两个主题会在语义变化点自动切分开。
优点
- 尊重语义边界——chunk 内话题一致
- 自适应——不需要预设 chunk_size 参数
- 召回质量通常比固定长度高 10-20%
缺点
- 需要额外的 embedding 调用来判断切分点
- 阈值需要针对你的文档类型调优(0.5-0.75 常见范围)
- 处理短文本时容易切出过多小 chunk
什么时候用
文档结构松散、话题变化频繁的场景(如论文、新闻、技术文档)。语义分块在这些场景下碾压固定长度。
策略四:Agent 分块(LLM-based Chunking)
一句话:让 LLM 决定怎么切,效果最好,但最贵。
原理
既然 LLM 能理解文档内容,为什么不直接让 LLM 来切分?给 LLM 一个 prompt,告诉它「把这份文档切成语义完整的段落」,它会在真正的内容边界上切分。
fromimportdefagent_chunktext: str, model: str = "gpt-4o-mini"liststr""" 用 LLM 进行智能分块。 """f"""你是一个文档分块专家。请分析以下文档,将其切分为语义完整的段落。 切分规则:1. 每个 chunk 应该是一个自包含的语义单元2. 不要切分表格、代码块、公式3. 如果某一段特别长(>20句),可以在逻辑转折点切开4. 每个 chunk 保持 300-1500 字之间5. 保留标题层级输出格式:每个 chunk 用 ---CHUNK_BOUNDARY--- 分隔文档内容:{text}""""role""user""content"0.1# 低温度,保持一致性0"---CHUNK_BOUNDARY---"returnforinif进阶:结构化分块
更常见的 Agent chunking 用法是结构化方式——不是让 LLM 切分,而是根据文档的结构标记来确定分块边界:
fromimportdefmarkdown_semantic_chunkmarkdown_text: str""" 基于 Markdown 结构的分块:保留标题层级和上下文。 """"#""H1""##""H2""###""H3"False# 每个 doc 保留了标题上下文(metadata)forin3printf"标题路径: {doc.metadata}"printf"内容前100字: {doc.page_content[:100]}"print"---"return如果你要处理 HTML、LaTeX、代码文档,结构化分块是最好的——它完全保留文档原来的逻辑结构。
优点
- 分块质量最高——LLM 理解文档语义后切分
- 可定制——可以加业务规则(“不要分拆合同条款”)
- 结构化输出——可以附带元信息(标题、标签)
缺点
- 慢——每个文档要一次 LLM 调用
- 贵——大规模文档处理成本高
- 不一致——相同输入可能得到不同切分结果
什么时候用
高质量要求的小规模数据(< 1 万条文档)、法律/医疗等对语义完整性要求严格的场景。
策略五:按文件类型特异化分块
不同的文件类型,最好的 chunk 方式完全不同:
代码文档
fromimportfromimport# Python 代码分块:按函数/类切分1000100# Markdown 分块:按标题切分50050PDF / 论文
# 论文:建议按章节标题 + 段落分块# 不要按页切!PDF 页数和逻辑段落没有关系# 用 PyMuPDF 解析后,每个段落作为基本单元import# PyMuPDFopen"paper.pdf"""forin# 提取文本块(不是逐行)"blocks"forin4"\n"# 然后用递归分块或语义分块处理表格 / CSV
# 表格数据:每条记录作为一个 chunkimportas"products.csv"forin# 把一行数据格式化成自然语言描述f"产品名: {row['name']},价格: {row['price']},类别: {row['category']}"终极对比表
| 维度 | 固定长度 | 递归分块 | 语义分块 | Agent 分块 | 结构化分块 |
|---|---|---|---|---|---|
| 实现难度 | ⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
| 速度 | 快 | 快 | 中 | 慢 | 快 |
| 成本 | 免费 | 免费 | 低(1次embedding) | 高(LLM调用) | 免费 |
| 语义完整性 | 差 | 中 | 好 | 最好 | 好(依赖结构) |
| 自适应 | ❌ | 部分 | ✅ | ✅ | ✅ |
| 表格/代码支持 | 差 | 差 | 中 | 好 | 好 |
| 适用场景 | 原型 | 默认选择 | 松散文档 | 高质量要求 | 结构化文档 |
实战:分块 + 检索的完整评估流程
理论讲完了,来点实际的。没有评测的选择都是瞎选。这里给一个可运行的评估脚本:
fromimportfromimportimportasfromimportListDictdefevaluate_chunk_strategy documents: Dict[str, str], test_queries: List[tuple], chunk_size: int = 500, chunk_overlap: int = 50, embedding_model: str = "BAAI/bge-small-zh-v1.5", top_k: int = 5float""" 评估不同 chunk 参数的 Recall@K。 test_queries: [(query_text, [relevant_doc_ids])] """"\n\n""\n""。""!""?"# 生成所有 chunk# 记录每个 chunk 来自哪个文档forinlenifnotreturn0.0# 编码所有 chunkTrue0forinTrue1setforinifset1returnlen# 使用示例"doc_1""Transformer 架构完全基于注意力机制...""doc_2""Pgvector 是 PostgreSQL 的向量检索扩展...""注意力机制和 RNN 有什么关系""doc_1""PostgreSQL 怎么做向量检索""doc_2"forin3005008001000int0.1printf"chunk_size={size}, overlap=10% → Recall@5: {recall:.3f}"最佳实践:
- 从你的真实数据中抽 50-100 条 query + 对应 ground truth 文档
- 用上面的脚本自动跑 chunk_size 200/300/500/800/1000 五组
- 选 recall 曲线的拐点——一般是 recall 不再明显增长的 chunk_size
- 最后在这个 chunk_size 附近微调 overlap 比例(5%/10%/15%/20%)
写在最后
我见过的 RAG 项目踩的最大的坑,就是在 chunk 策略上花的时间 < 1 小时。
很多人花一周选 embedding 模型,花三天选向量数据库,然后 chunk 策略用了一个默认参数就上线了。
这是倒过来的。在 RAG 系统的优化优先级里,chunk 策略的影响力 > embedding 模型 > 向量数据库。道理很简单——chunk 决定了你喂给检索系统的"基本单元"是什么。如果这个基本单元本身就切错了,后面的每一步都在放大这个错误。
几个经验送给你:
- 从递归分块开始——chunk_size=500, overlap=50,跑起来再说
- 一定要做 recall 评测——用你的真实数据,不要凭感觉
- 不同文件类型用不同策略——代码用函数边界切,论文用段落切,Markdown 用标题结构切
- chunk_size 和 embedding 维度要匹配——维度高的模型可以用更大 chunk(信息更多),维度低的模型 chunk 小一点
最后一句大实话:先随便切,跑通全流程,再去优化 chunk。有 benchmark 数据的优化才是真优化,没有 benchmark 的优化叫"调参娱乐"。
学AI大模型的正确顺序,千万不要搞错了
🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!
有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!
就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋
📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇
学习路线:
✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经
以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!
我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~