RAG分块策略实战:5种可运行Python方案

梳理5种生产级RAG分块策略及选型规则,附可运行Python代码。
文章指出分块是RAG教程最常被忽略却直接决定检索质量的一步,块过大、过小、跨语义边界切分是三种主要失效模式。作者给出覆盖约95%生产流水线的5种策略:固定大小、递归字符、句子边界、文档结构与语义分块,每种都配有可直接运行的Python代码、适用与不适用场景说明。核心结论是分块策略应取决于源文档结构与查询形态,而非某个默认token数;带Markdown分隔符的递归分块可作为多数生产语料的默认选择,其余四种更适合特定场景,最终决定前应在留出评估集上对两三种候选方案做基准对比。
分块(Chunking)是RAG教程中最常被一笔带过的步骤——多数教程只丢下一句“把文档切成500个token的块”,而检索效果差往往就出在这里。正确的分块策略取决于源文档的结构和查询形态,而非某篇博客的默认数字。本文梳理了覆盖约95%生产级RAG流水线的5种分块策略,每种都配有可直接运行的Python代码。
前置阅读:如果你还没搭好完整流水线,可以先看“从零构建RAG流水线”,本文对应其中第1步。

为什么分块如此关键
块是检索的基本单位。用户提问后,检索器返回与问题embedding最接近的top-K块。有三种问题都由块的形态直接导致:
- 块太大:embedding把具体性平均掉了。块会被太多弱相关的查询召回,把真正相关的块挤出top-K。
- 块太小:块丢失上下文。检索到的段落单独读没问题,但LLM无法回答,因为答案的落脚点落在没被召回的其他块里。
- 块跨语义边界切分:定义在一个块里,例子在另一个块里,只召回其中一个,LLM只拿到半个答案。
下面5种策略以不同方式应对这三种失效模式。
Embedding(嵌入) 是把一段文本映射成一个高维向量的过程——语义相近的文本,其向量在空间中的方向也相近。检索时,用户问题同样被编码为向量,系统通过余弦相似度等度量找出与问题向量最接近的top-K个块。这解释了一个反直觉的现象:块越大,向量覆盖的主题越多,其方向就越“居中”,与任何具体问题的相似度都不够突出;块越小,向量越聚焦,但承载的语义信息也越少。因此分块本质上是在“检索精度”和“上下文完整性”之间做权衡,这个权衡没有普适最优解,取决于你的查询是偏细节型还是偏概括型。
Top-K召回 指检索阶段返回相似度最高的前K个块,K通常取3到10。它的关键约束是:K一旦固定,块的数量越多,单个块被选中的概率就越低;块越大,挤占名额的风险越高。这正是文中“块太大把真正相关的块挤出top-K”的机制来源。
通用准备工作
所有策略共用同一套脚手架。先安装依赖:
pip install langchain-text-splitters sentence-transformers numpy
定义一个用于展示分块结果的辅助数据类:
from dataclasses import dataclass
from typing import Callable
@dataclass
class Chunk:
text: str
index: int
strategy: str
metadata: dict
def show(chunks: list[Chunk]) -> None:
for c in chunks:
preview = c.text[:80].replace("\n", " ")
print(f"[{c.strategy} #{c.index:02d}] ({len(c.text)} chars) {preview}...")
策略1:固定大小分块
按N个字符(或token)切分,相邻块之间保留M个字符重叠。不感知结构、句子或语义。速度快、可复现,语料均匀时往往是合理默认。
def fixed_size_chunk(text: str, size: int = 400, overlap: int = 50) -> list[Chunk]:
chunks = []
start = 0
i = 0
while start < len(text):
end = min(start + size, len(text))
chunks.append(Chunk(
text=text[start:end],
index=i,
strategy="fixed",
metadata={"start": start, "end": end},
))
start = end - overlap
i += 1
return chunks
适用场景:均匀散文、日志、聊天记录、行边界不承载语义的源码。
不适用场景:结构化文档中任意切断会把标题和内容分离,或把代码块从函数中间劈开。
策略2:递归字符分块
按优先级列表遍历分隔符——段落、换行、句子、词、字符——在保证块不超限的前提下,用最高优先级的分隔符切分。这是LangChain的RecursiveCharacterTextSplitter,也是混合结构内容的合理默认。
from langchain_text_splitters import RecursiveCharacterTextSplitter
def recursive_chunk(text: str, size: int = 400, overlap: int = 50) -> list[Chunk]:
splitter = RecursiveCharacterTextSplitter(
chunk_size=size,
chunk_overlap=overlap,
separators=["\n\n", "\n", ". ", " ", ""],
)
raw = splitter.split_text(text)
return [
Chunk(text=c, index=i, strategy="recursive", metadata={})
for i, c in enumerate(raw)
]
适用场景:结构化程度不一的散文——文档、文章、Notion页面、带列表和代码块的Markdown。通常也是团队文档的默认选择。
不适用场景:内部没有断行的超长段落(会退化为字符级切分);分隔符列表与文档格式不匹配(如源码的层级是class → method → statement)。
策略3:句子边界分块
先把文档切成句子,再贪心地把连续句子组成块直到触及大小预算。块永远不会在句子中间切断,embedding更干净。
import re
def sentence_chunk(text: str, size: int = 400) -> list[Chunk]:
# 简化的句子切分。生产中建议用spaCy或NLTK处理缩写等情况。
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
chunks: list[Chunk] = []
current: list[str] = []
current_len = 0
i = 0
for s in sentences:
if current_len + len(s) > size and current:
chunks.append(Chunk(
text=" ".join(current),
index=i,
strategy="sentence",
metadata={"n_sentences": len(current)},
))
i += 1
current = [s]
current_len = len(s)
else:
current.append(s)
current_len += len(s) + 1
if current:
chunks.append(Chunk(
text=" ".join(current),
index=i,
strategy="sentence",
metadata={"n_sentences": len(current)},
))
return chunks
适用场景:问答语料、FAQ文档、访谈转录、对话。任何以一两句话为自然语义单位的语料。
不适用场景:代码(分号终止语句,但函数才是意义单位);大量缩写会干扰句子分词器的文本;没有明确句末标点的非英文语种。
策略4:文档结构分块
用文档自身的结构标记——Markdown标题、HTML的<h1>/<h2>、docstring小节——来定义块边界。每个块是一个自包含的小节。块大小会不均匀,只要单节不超过embedding模型的上下文窗口就没问题。
def markdown_section_chunk(text: str, min_size: int = 100) -> list[Chunk]:
lines = text.split("\n")
chunks: list[Chunk] = []
buffer: list[str] = []
current_heading = ""
i = 0
def flush():
nonlocal i
if not buffer:
return
section = "\n".join(buffer).strip()
if len(section) >= min_size:
chunks.append(Chunk(
text=section,
index=i,
strategy="structure",
metadata={"heading": current_heading},
))
i += 1
for line in lines:
if re.match(r"^#{1,6}\s", line):
flush()
buffer = [line]
current_heading = line.lstrip("# ").strip()
else:
buffer.append(line)
flush()
return chunks
适用场景:技术文档、Markdown知识库、结构化报告、政策文件、教材章节。作者刻意用标题组织思想的语料。
不适用场景:无结构散文(小说、邮件、聊天);单节太大无法作为单块embedding的文档;章节标题过于泛化(“概述”“简介”)无法帮embedding携带上下文时。
策略5:语义分块
对每个句子做embedding,然后观察相邻句子embedding之间的余弦相似度。相似度低于阈值处即为主题边界,在该处切分。块按语义连贯性分组,而不是按字符数。
代价比其他四种都高(建索引时每句一次embedding调用),但当语料密集、主题在章节内平滑漂移时,能产出最干净的块。
import numpy as np
from sentence_transformers import SentenceTransformer
_model = None
def _embedder():
global _model
if _model is None:
_model = SentenceTransformer("all-MiniLM-L6-v2")
return _model
def semantic_chunk(text: str, similarity_threshold: float = 0.5) -> list[Chunk]:
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
if len(sentences) < 2:
return [Chunk(text=text, index=0, strategy="semantic", metadata={"n_sentences": len(sentences)})]
embeddings = _embedder().encode(sentences)
sims = []
for i in range(len(sentences) - 1):
a, b = embeddings[i], embeddings[i + 1]
sims.append(float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))))
chunks: list[Chunk] = []
buffer = [sentences[0]]
i = 0
for idx, sim in enumerate(sims):
if sim < similarity_threshold:
chunks.append(Chunk(
text=" ".join(buffer),
index=i,
strategy="semantic",
metadata={"split_similarity": round(sim, 3)},
))
i += 1
buffer = [sentences[idx + 1]]
else:
buffer.append(sentences[idx + 1])
if buffer:
chunks.append(Chunk(
text=" ".join(buffer),
index=i,
strategy="semantic",
metadata={},
))
return chunks
相似度阈值是可调参数:值越低,块越大越少(更容忍主题漂移);值越高,块越小越多(对主题连贯要求更严)。可以从0.5起步,用评估集微调。
适用场景:长论文、学术文章、主题渐进漂移但无明确结构标记的密集技术写作。新闻存档、播客转录、会议记录。
不适用场景:短文档(句子太少无法聚类);已有良好结构的内容(文档结构分块更便宜且更贴合现实);异构语料中没有任何单一阈值能跨文档通用。
余弦相似度 衡量两个向量方向的接近程度,取值在-1到1之间,与向量长度无关,因此适合比较不同长度文本的embedding。计算方式是两向量点积除以各自模长的乘积。本文semantic_chunk函数中的np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))正是这一公式的直接实现,对已归一化的sentence-transformers输出而言,点积本身已近似等于余弦相似度。
all-MiniLM-L6-v2 是sentence-transformers库中一个约22MB的轻量级模型,输出384维向量,在速度和语义质量之间取得了不错的平衡,常用于本地原型。它的上下文窗口有限(约256个token),因此语义分块通常按“句子”而非“段落”逐一编码。需要注意,该模型是为英文语料训练的,多语言场景应换用paraphrase-multilingual-MiniLM等模型。
如何选策略:粗略规则
把策略匹配到源文档的主要形态:
- 均匀散文、日志、聊天记录 → 固定大小
- 混合Markdown / HTML / Notion风格文档 → 递归
- 问答、FAQ、对话、访谈 → 句子边界
- 带标题的技术文档、教材、政策、规范 → 文档结构
- 无结构标记的长论文、学术文章、会议记录 → 语义
在做最终决定前,拿两三种候选方案在留出的评估集上做基准对比。实践中,带Markdown分隔符的递归分块可以作为多数生产语料的默认选择,其余四种更适合特定场景。
本文未覆盖的内容
生产环境中的分块长尾远不止这五种策略。未涉及的部分包括:从语料形态到策略的30秒决策表;多语言混合脚本、表格被粗暴提取器破坏的PDF、嵌套条款结构的法律合同等边缘情况;块元数据schema;分块器版本管理;以及增量重建索引策略——检测源文档编辑后真正变化的块。
相关阅读:从零构建RAG流水线、向量数据库对比(pgvector、Qdrant、Pinecone、Chroma)、何时不该用RAG,以及RAG生产环境FAQ。
相关推荐

Claude Code免费不限量方案:Agnes AI+CC Switch配置指南
无需付费订阅,用免费AI平台Agnes AI配合开源工具CC Switch为Claude Code提供后端,实现文本、图片、视频模型不限量免费调用。本文梳理安装配置全流程、实测效果与使用边界,帮助开发者低成本上手Claude Code。

国内新手零成本上手Claude Code完整指南
面向国内新手的 Claude Code 入门指南:两种安装方式(命令行与 IDE 插件)、用国产免费模型 LongCat 替代官方 API 的完整配置步骤,以及用自然语言开发小工具的实操演示与技巧。

当NSA都介入:开源CVE与API密钥劫持背后的AI供应链危机
一天内多个开源项目曝出严重CVE,PHP漏洞惊动FBI,Railway与OpenCode遭入侵致API密钥被窃。本文拆解漏洞窗口、AI加速利用与API密钥黑市背后的AI供应链危机,并给出开发者防御清单。