切分是 RAG 链路上第一个、也是影响最大的环节。这节在同一份文档、同一套问题上对比四种切法。
可复现前提: 数据集为 200 篇内部技术文档 + 150 条人工标注问答,脚本 bench/chunking 跑一轮约 12 分钟。
四种策略
1. 固定长度切分。 每 500 字一段,重叠 50 字。最简单,也最容易把一句话劈成两半。
def fixed_chunks(text, size=500, overlap=50):
out, i = [], 0
while i < len(text):
out.append(text[i : i + size])
i += size - overlap
return out2. 按句切分。 按标点断句,再把相邻句子拼到目标长度。中文要注意"。!?"以及引号内的标点。
3. 按结构切分。 按 Markdown 标题层级切,每段带上它的标题路径。我们的文档都有规范结构,这一版效果最好。
4. 按语义切分。 用嵌入向量算相邻句子的相似度,在"话题转折处"下刀。计算量大,提升有限。
实测
| 策略 | recall@5 | 切分耗时(200 篇) | 平均段长 |
|---|---|---|---|
| 固定长度 | 0.68 | 0.4s | 500 |
| 按句 | 0.74 | 3s | 420 |
| 按结构 | 0.86 | 6s | 380 |
| 按语义 | 0.87 | 210s | 460 |
结论
按结构切分性价比最高。 语义切分只高 1 个百分点,却慢 35 倍。
但有个前提:你的文档得真有结构。我们后来接入了一批扫描件转出来的纯文本,没有标题层级,按结构切就退化成了按句切。
一个关键细节:把标题路径拼进每段
chunk_text = f"{' > '.join(heading_path)}\n\n{body}"这一行让 recall@5 从 0.79 涨到 0.86。原因很直白:片段脱离上下文后,标题是唯一能说明"这段在讲什么"的线索。