你是怎么做分块的?块大小怎么选的?
结论:分块(Chunking)没有全局最优参数,只有"先按文档结构切,再按检索粒度定大小"这套流程。
默认从 300600 token(中文约 200400 字)起步,重叠 10%~15%,然后用评估集验证;有标题结构的文档,
一定要把标题路径带上,它往往比正文更能决定召回。
1. 决策顺序
文档进来
├─ 有标题层级(Markdown/HTML/Word 大纲)?──是──▶ 按标题切,超长再二次切分
├─ 是表格?──────────────────────是──▶ 整表保留,转 Markdown,禁止按行切
├─ 是代码?──────────────────────是──▶ 按函数/类切,保留签名与注释
└─ 纯文本、无结构 ──────────────────▶ 递归分隔符(段落→句号→空格)
└─ 仍超长 ──▶ 固定窗口 + 重叠
顺序很重要:结构优先于长度。为了凑 500 字把一个"总则"和一个"例外条款"切到两个块里,等于亲手制造矛盾答案。
2. 块大小怎么权衡
| 块大小(token) | 召回精度 | 上下文完整度 | 噪声 | 适用场景 |
|---|---|---|---|---|
| 128~256 | 高,几乎只命中相关句 | 低,易断章取义 | 少 | FAQ、参数表、客服话术 |
| 300~600 | 中高 | 中,够装一个完整论述 | 中 | 通用默认值 |
| 800~1200 | 中 | 高,能覆盖流程步骤 | 多 | 制度文件、操作手册 |
| 整篇(>2000) | 低,向量被"平均"掉 | 最高 | 极多 | 只适合"总结这份文档"类问题 |
块越大,一个向量要概括的信息越多,语义被稀释:标题讲"A 流程",正文却在讲"B 例外",合在一起的向量谁也代表不了。
3. 重叠(Overlap)解决什么
交界处的句子一定会被切断。重叠 10%~15% 保证"上一块的结尾"能在"下一块的开头"再出现一次。
| 重叠比例 | 效果 |
|---|---|
| 0 | 边界信息必丢,跨块问题答不上 |
| 10%~15% | 推荐,成本可接受 |
| >30% | top-k 里出现大量重复内容,白占预算 |
4. 代码:标题感知 + 窗口兜底
import re
HEAD = re.compile(r"^(#{1,3})\s+(.+)$", re.M)
def split_sections(md: str) -> list[str]:
"""按 1~3 级标题切段,标题留在段首当面包屑。"""
marks = list(HEAD.finditer(md))
if not marks:
return [md]
return [md[m.start(): marks[i + 1].start() if i + 1 < len(marks) else len(md)].strip()
for i, m in enumerate(marks)]
def window(text: str, target: int, overlap: int) -> list[str]:
"""无结构文本兜底:定长窗口 + 重叠。target/overlap 单位是字符。"""
step = max(1, target - overlap)
return [text[i:i + target] for i in range(0, len(text), step)]
def chunk_markdown(md: str, target: int = 500, overlap: int = 60) -> list[str]:
chunks: list[str] = []
for sec in split_sections(md):
if len(sec) <= int(target * 1.5): # 一个自然段装得下就不切
chunks.append(sec)
else:
chunks.extend(window(sec, target, overlap))
return [c for c in chunks if len(c.strip()) >= 50] # 丢掉标题残片
# 输出:每个块形如 "## 差旅报销\n住宿标准为……"
入库时把 h1/h2 也写成元数据字段,过滤与展示都用得上;后面做父文档检索(Parent Document Retriever)时,
还要额外记录 parent_id。
5. 固定大小分块的三个硬伤
| 问题 | 现象 | 后果 |
|---|---|---|
| 无视句子边界 | "逾期未提交的,视为" / "放弃报销权利" 分属两块 | 单块语义残缺,模型答非所问 |
| 无视结构 | 切点落在两个章节中间 | 两个主题混进一个向量 |
| 表格/代码被拦腰截断 | 表头在第一块,数据行在第二块 | 数值失去列名,检索到也没法用 |
它也有存在价值:实现简单、可对齐、适合日志或聊天记录这种本来就无结构的文本。不要因为简单就默认用它。
6. 怎么验证块大小选对了
造 30~50 条"问题 → 应命中的原文片段"标注,跑两件事:
| 指标 | 含义 | 观察什么 |
|---|---|---|
| 答案完整率 | top-k 中存在完整包含答案的块 | 偏低说明切太碎,答案跨块 |
| 召回率 recall@5 | 正确块是否进前 5 | 偏低说明切太大,向量被稀释 |
| 平均块 token | 成本与噪声 | 每块都顶到上限,通常意味着该加结构切分了 |
7. 常见追问
Q:按字符还是按 token 算? 按 token 算才准,中文 1 字约 0.6~0.8 token,用估算值起步没问题,但上线前要用分词器实测。
Q:要不要给每块加标题? 要。标题 + 正文 一起向量化,能显著改善"这段在讲什么"的召回;只存正文往往丢掉最强信号。
Q:代码块怎么切? 按函数或类切,保留 imports 和注释;别用固定窗口,函数头身分离后基本不可用。
小结
分块是 RAG 里性价比最高的一步:改两行切分逻辑,往往比换一个更贵的模型提升更大。结构优先、宁小勿碎、留重叠,然后拿数据验证。