上一篇文章讲了文档解析——怎么把原始文件变成结构化的内容块。
但文档解析出来的"块"往往还太大、太杂,不能直接喂给向量数据库。
你需要再切一刀。
这一步叫做分块(Chunking)。
它是 RAG 流水线里对检索效果影响最大的环节之一,却也是最容易被新手忽视的一步。很多人搭 RAG 效果不好,不是模型不行,不是 Prompt 写得差,而是分块策略选错了。
RAG系列·第2篇:分块策略——把文档切成什么形状,直接决定RAG效果
目录
1. 为什么分块直接影响检索效果 2. 分块的两种度量单位 3. 六种分块策略,逐一拆解 策略一:固定大小分块(Fixed-size Chunking) 策略二:句分割(Sentence Splitting) 策略三:递归分割(Recursive Chunking) 策略四:内容语义分块(Semantic Chunking) 策略五:专门结构分块(Structure-aware Chunking) 策略六:LLM 自主分块(LLM-based Chunking) 4. 所有策略横向对比 5. Overlap:分块时不能忽视的细节 6. 分块策略实战:怎么选 7. 一句话总结
1. 为什么分块直接影响检索效果
向量检索的本质:找到和用户问题语义最接近的内容块。
如果一块内容太大、主题太多,它的向量就会"平均化"——什么都有一点,但什么都不突出。检索时,很容易被不相关的块稀释掉。
正确做法:把"检索器"和"生成器"切成独立的块,各自主题清晰,检索时才不容易被稀释。
2. 分块的两种度量单位
在说具体策略之前,先搞清楚分块大小的两种度量方式:
| Token 数 | ||
| 字符数 | ||
| 句子数 |
通常用 token 数更准确,因为向量模型处理的是 token,不是字符。实践中,中文 1 个汉字 ≈ 1.2~1.5 个 token(GPT-4/Claude/DeepSeek 等现代模型实测约 1.2:1,GPT-3 等旧模型约 2:1)。也就是说,每 500 tokens 大约对应 330-420 个中文字符。
常见块大小参考:
小块(200-500 tokens):- 适合:精确问答、FAQ、条款类内容- 优点:主题集中,检索精准- 缺点:可能丢失上下文中块(500-1000 tokens):- 适合:通用技术文档、报告、说明书- 优点:平衡主题完整性和语义聚焦度- 缺点:不够精确也不够完整大块(1000-2000 tokens):- 适合:需要长上下文的分析类内容- 优点:保留更多上下文信息- 缺点:语义容易稀释,向量不够精准3. 六种分块策略,逐一拆解
策略一:固定大小分块(Fixed-size Chunking)
原理: 按固定 token 数或字符数,从头切到尾。
deffixed_size_chunking(text, chunk_size=500, chunk_overlap=50): chunks = [] start = 0while start < len(text): chunks.append(text[start:start + chunk_size]) start = start + chunk_size - chunk_overlapreturn chunks优点: 简单、速度快 缺点: 完全不考虑语义边界,可能把一句话从中间切开适用场景: 快速原型、对语义完整性要求不高的场景。
策略二:句分割(Sentence Splitting)
原理: 按完整句子切分,用句号、问号等作为分隔符。
defsentence_chunking(text, max_sentences=5, chunk_overlap=1): sentences = [s.strip() for s in re.split(r'[。!?.?!]', text) if s.strip()] chunks, i = [], 0while i < len(sentences): chunks.append('。'.join(sentences[i:i + max_sentences]) + '。') i += max_sentences - chunk_overlapreturn chunks优点: 语义完整性好 缺点: 块大小不稳定适用场景: 短文本、FAQ、问答对。
策略三:递归分割(Recursive Chunking)
原理: 按层级依次尝试不同的分隔符(段落→句子→字符),直到每块大小符合要求。
defrecursive_chunking(text, separators=None, chunk_size=500):if separators isNone: separators = ["\n\n", "\n", "。", " "]if len(text) <= chunk_size ornot separators:return [text] ifnot separators else [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)] sep, parts = separators[0], text.split(separators[0]) result, current = [], ""for part in parts: test = current + sep + part if current else partif len(test) <= chunk_size: current = testelse:if current: result.append(current) current = part if len(part) <= chunk_size else""if len(part) > chunk_size: result.extend(recursive_chunking(part, separators[1:], chunk_size))if current: result.append(current)return result
separators用None默认值避免 Python 经典的可变默认参数陷阱;所有分隔符都无法切到目标大小时,自动按固定大小兜底。
优点: 最大程度保留语义边界,兼顾块大小控制 缺点: 实现复杂度稍高适用场景: 大多数通用场景,是目前最推荐的基础策略。
策略四:内容语义分块(Semantic Chunking)
原理: 根据内容的语义相似度来决定在哪里切。先用 embedding 模型给每个句子打向量,识别"语义断点"——相邻句子语义突变的地方就是断点。
defsemantic_chunking(sentences, drop_threshold=1.0):"""基于语义断点的分块:相邻句子相似度显著下降处即为断点""" model = SentenceTransformer('all-MiniLM-L6-v2') embeddings = model.encode(sentences)# 计算相邻句子的语义相似度 similarities = [np.dot(embeddings[i], embeddings[i+1]) / (np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i+1]))for i in range(len(embeddings) - 1)]# 用统计方法确定断点:低于(均值 - drop_threshold×标准差)时断开 threshold = np.mean(similarities) - drop_threshold * np.std(similarities) breakpoints = [0] + [i+1for i, sim in enumerate(similarities) if sim < threshold]# 按断点分块return [''.join(sentences[breakpoints[i]:breakpoints[i+1] if i+1 < len(breakpoints) else len(sentences)])for i in range(len(breakpoints))]为什么用相对阈值? 不同 embedding 模型输出的相似度分布差异很大。用"均值 - 标准差"的相对检测,能自动适配不同模型。
效果示意:
原文:"...检索器负责从向量数据库中检索相关内容。生成器负责根据检索结果生成回答。接下来安装部署环境。首先需要安装Python..."语义分析:"生成器根据检索结果生成" → "接下来安装部署环境" 相似度骤降(话题转换)→ 在此处断开,分为"RAG组件"和"安装步骤"两块优点: 语义断点最准确,每块主题高度集中 缺点: 需要额外调用 embedding 模型,成本高、速度慢
适用场景: 对检索精度要求高的生产环境。
策略五:专门结构分块(Structure-aware Chunking)
原理: 根据文档的内在结构(Markdown标题层级、HTML标签、PDF章节)来切分。
defmarkdown_chunking(markdown_text):"""按Markdown标题层级分块,保留标题作为上下文""" blocks, heading, content = [], "", []for line in markdown_text.split('\n'):if line.startswith('#'):if content: blocks.append({"heading": heading, "content": '\n'.join(content)}) heading, content = line, []else: content.append(line)if content: blocks.append({"heading": heading, "content": '\n'.join(content)})return blocks适用场景: 有明确结构的文档(Markdown、技术文档、论文、合同)。配合向量化时,把标题也加进块内容可提升检索相关性。
策略六:LLM 自主分块(LLM-based Chunking)
原理: 让大模型自己判断在哪里切分最合理,并输出每块的主题标签和摘要。最智能、也成本最高的方案。
defllm_chunking(text, max_tokens=500): prompt = f"""将以下文档切分成语义完整的块(每块≤{max_tokens} tokens)。输出JSON数组,每个元素包含title、content、reason。文档:{text}"""return json.loads(call_llm(prompt))优点: 分块质量最高,每块有标题和摘要 缺点: 贵、慢适用场景: 对质量要求极高的核心知识库。
4. 所有策略横向对比
5. Overlap:分块时不能忽视的细节
Overlap(重叠)指的是相邻两块之间重复一部分内容,防止关键信息落在两个块的边界上。
一般推荐:**Overlap 占 chunk 大小的 10%-30%。
chunk_size = 500 tokens, overlap = 50-150 tokens块1:[0-500] 块2:[450-950] 块3:[900-1400]overlap 不是越大越好:太小→边界信息丢失;太大→浪费存储、降低检索多样性。
语义连贯文档(无明显段落边界)→ overlap 20-30% 结构清晰文档(段落独立)→ overlap 10-15%
6. 分块策略实战:怎么选
决策框架:
文档有清晰结构?→ 有→优先"结构分块"+overlap | 没有→下一步精度要求极高?→ 是→语义分块或LLM自主分块 | 一般→下一步需要快速出效果?→ 是→"递归分割"(最稳健的起步方案)常见组合:文档解析(提取结构)→ 结构分块(按章节切)→ 小块内部递归分割 → overlap 15%
7. 一句话总结
分块没有最优解,只有最适合当前文档和场景的选择。固定大小是最快的起步,递归分割是最稳的基础,语义分块是 精度优先的选择,LLM 自主分块是效果最好的方案。
定好分块策略后,下一步就是给每个块生成向量,存入向量数据库——那才是真正可以被检索的时刻。
动手试一下:用 LangChain 做递归分块
上面手写的 recursive_chunking 帮你理解原理,实际项目用 LangChain 的 RecursiveCharacterTextSplitter:
from langchain.text_splitter import RecursiveCharacterTextSplittersplitter = RecursiveCharacterTextSplitter( chunk_size=200, chunk_overlap=30, separators=["\n\n", "\n", "。", " "])chunks = splitter.split_text(text)三行代码搞定递归分块,还能自动处理 overlap 和分隔符优先级。原理理解了,实战交给工具。
下一篇:向量化(Embedding)——让文字变成可计算的向量 · RAG系列·第3篇
夜雨聆风