Day 22 / 共 68 天 · 阶段4 RAG 检索增强

文本切分 chunking

昨天(Day 21)你拿到了 RAG 六步全景地图。今天钻进第一步实操——②切(chunking):怎么把长文档切成"刚好能用"的小块。这一步看似简单,却是决定 RAG 效果好坏的第一道关:切坏了,后面 embedding、检索再强也救不回来。

📍 你在 68 天里的位置(阶段4 · RAG 检索增强 D21-28)
D21 RAG 全景 D22 chunking D23 embedding D24 向量库 D25 检索 D26 rerank D27 评测
💡 今天的世界观:把一本厚书拆成"知识卡片" 今天所有比喻都围绕把厚书做成一叠复习卡片(抽认卡):① 整本书太厚没法快速查,得拆成一张张小卡片,每张讲一个知识点;② 卡片太大(半本书塞一张)等于没拆,太小(一句话一张)又前言不搭后语——大小要"刚刚好";③ 为了防止一个概念被拦腰切断,相邻卡片抄一点上一张的结尾,这就是 overlap(重叠)。记住"拆卡片、大小刚好、边缘留点重叠"。
L01

为什么必须切?不切行不行

🤔 痛点你有一份 50 页的员工手册。如果整本当成"一块"扔进 RAG,会同时坏两件事:一是它可能太长塞不进模型窗口;二是就算塞进去,用户只问"年假几天",你却把整本手册都给模型看,又贵又吵,关键那句被淹没在无关内容里
💡 本质一句话切分 = 把长文档拆成一张张"只讲一个小主题"的知识卡片,好让检索时能精准取出最相关的那一两张,而不是把整本书搬出来。

三个必须切的理由:

  • 检索要精准:块越聚焦一个主题,"年假"这个问题就越能命中"年假那张卡",而不是命中"整本书"。
  • 省钱省窗口:只把相关的几小段塞进 Prompt,而不是整本文档(回忆 Day 18、Day 21)。
  • embedding 需要:明天你会学到,embedding 是"给一段文字算一个语义坐标"。一段文字主题越单纯,这个坐标才越准——半本书混在一起,坐标就"糊"了。
👶 一句大白话查一个知识点,你会翻整本教科书吗?当然是翻到那一页。切分就是提前把书拆成一页页,好让机器一秒翻到对的那页。
L02

切的本质:一个 chunk = 一张自洽的卡片

💡 本质好的 chunk(块)有一个标准:它单独拿出来,也能大致读懂在讲什么——像一张合格的知识卡片,不依赖前后文也自成一小段意思。

切分要在两个目标之间找平衡:

切分 = 在"够聚焦"和"够完整"之间找平衡点 太小 一句话一张 意思不完整 刚刚好 ✓ 一个主题一张卡 太大 半本书一张 主题太杂
图注:切分没有唯一正确答案,是在"够聚焦(利检索)"和"够完整(利理解)"之间找那个甜点。

常见的切分粒度,从粗到细:按章节 → 按段落 → 按句子 → 按固定字数。实践中最常用的是"按固定长度切,并在句子/段落边界附近断开",兼顾简单和不切碎语义。

L03

最常用:按长度切 + overlap 重叠

💡 本质按长度切 = 每张卡片写固定字数就换一张(比如每 300 字一块)。overlap = 每张新卡都抄一点上一张的结尾,免得一个句子/概念被拦腰切断后两边都读不懂。

两个关键参数:

  • chunk_size(块大小):每块多长,常见 200-500 字(或按 token 算)。
  • overlap(重叠):相邻两块重复多少内容,常取 chunk_size 的 10%-20%,比如 300 字块配 50 字重叠。
overlap:相邻块留一段重叠,防止语义被切断 原文(一整条长文本) 块1 块2 块3 绿色 = 重叠区(块1尾=块2头,块2尾=块3头)
图注:块与块之间的绿色重叠区,保证跨边界的句子在至少一个块里是完整的。
📝 例子:overlap 救了一个被切断的句子 原文:…年假满 1 年 10 天。|切点| 病假需提供证明…。若无重叠,"年假满 1 年 10 天"结尾和"病假"开头分在两块,块1 结尾可能只剩"年假满 1 年"。加了 50 字重叠后,块2 开头会重复"…满 1 年 10 天。病假需…",这样无论检索命中哪块,年假那句都是完整的。
L04

更聪明:按语义/结构切

🤔 痛点纯按字数切,可能在一句话中间"咔"一刀,或者把"标题"和它对应的"正文"切散。有没有更懂文章结构的切法?
💡 本质按语义/结构切 = 顺着文章天然的"接缝"下刀——先按标题、再按段落、再按句子,尽量不破坏原有的意义单元。像沿着书本章节的天然分页去拆,而不是拿尺子量着裁。

几种常见思路:

切法怎么切适合
递归按分隔符优先按"段落→句子→字"逐级切,尽量在自然边界断开(最常用)大多数文档,通用
按标题/结构按 Markdown 标题、章节层级切,保留"标题+其下正文"在一起手册、文档、带小标题的文章
按语义相似度相邻句子"意思接近"就合并,"话题一变"就断开连续叙述、无明显结构的长文

👶 小白:那我是不是应该永远用最聪明的"语义切"?

👨‍🏫 老师:不一定。语义切更准,但更慢、更复杂(有的还要额外调模型判断话题边界)。工程上很常见的选择是:先用"递归按分隔符 + 固定大小 + overlap"这套朴素方案跑起来,再根据评测结果(D27)决定要不要上更复杂的切法。别一上来就追求最花哨的——先能跑、能评,再优化。

L05

切太大 vs 切太小:两头都会翻车

chunk_size 是个"跷跷板",两个极端都有代价:

切太大(如 2000 字/块)切太小(如 20 字/块)
检索精度❌ 一块混多主题,命中了也夹带大量无关内容❌ 意思太碎,单块看不懂,容易漏检
成本/窗口❌ 每次塞进 Prompt 的内容多,又贵又占窗口✅ 单块小,但可能要取很多块才凑够信息
典型症状答案"注水"、抓不住重点答案缺上下文、前言不搭后语
📝 例子:同一个问题两种切法的差别"报销单在哪提交?"
切太大:命中的块是整个"财务制度"章节(2000 字),模型要在一大段里找那句话,还把无关的"预算流程"也读了 → 答案啰嗦、可能跑偏。
切太小:命中"在系统提交"这 6 个字,但"哪个系统?"的上下文在别的块里没被取到 → 答案不完整。
刚刚好(约 300 字/块):命中"报销单在 OA 系统『我的报销』里提交,需上传发票"这一小段 → 精准完整。
👶 一句大白话卡片太大像"把半本书抄一张卡",太小像"一张卡只写俩字",都不好背。一张卡讲清一个点,才好用。
L06

动手:手写一个带 overlap 的切分器

💡 本质不借助任何库,20 行就能实现"按长度切 + 重叠"。理解了这个,再用框架的切分器就毫无神秘感。
def split_text(text: str, size: int = 300, overlap: int = 50):
    """把 text 切成每块约 size 字、相邻块重叠 overlap 字。"""
    chunks = []            # 存放切好的块
    start = 0              # 当前块从第几个字开始
    while start < len(text):
        end = start + size          # 这一块的结束位置
        chunks.append(text[start:end])   # 切片 text[start:end] 取一段
        # 下一块的起点 = 当前终点往回退 overlap,形成重叠
        start = end - overlap
        if start <= 0:              # 防御:overlap 比 size 还大时死循环
            break
    return chunks

doc = "第一段讲年假。" * 40 + "第二段讲病假。" * 40   # 造一段长文本
parts = split_text(doc, size=100, overlap=20)
print("切出块数:", len(parts))          # 看切成了几块
print("第1块:", parts[0][:30], "...")   # 瞄一眼第1块开头
print("第2块:", parts[1][:30], "...")   # 第2块开头会重复第1块结尾(重叠)
重点看两行:chunks.append(text[start:end]) 是"裁下一张卡";start = end - overlap 是"下一张卡往回挪一点,制造重叠"。把 overlap 改成 0 再跑一次,对比第2块开头是否还重复第1块结尾——你会瞬间理解 overlap 干了啥。
L07

今日小结 + 动手 10 分钟

🧠 今天你应该能回答

  • 为什么长文档必须切?(检索精准、省窗口省钱、embedding 才准)
  • 一个好 chunk 的标准是什么?(单独拿出来也大致自洽)
  • chunk_size 和 overlap 各是什么?overlap 解决什么问题?
  • 按长度切和按语义/结构切的区别?工程上先用哪种?
  • 切太大、切太小分别会出什么问题?

✋ 动手(约 10 分钟)

# 把 L06 的 split_text 存成 chunk.py,用真实文本试三组参数
python3 chunk.py
  1. 找一段你熟悉的长文本(几百字即可),分别用 size=50size=300size=1000 切,各打印块数和第一块内容。
  2. 观察:size 越小块越多、每块越碎;size 越大块越少、每块越杂。感受"跷跷板"。
  3. overlap 在 0 和 50 之间切换,看相邻块的衔接处有没有重复——理解 overlap 的作用。
  4. 想一想:如果你的文档是"带很多小标题的手册",L04 里哪种切法更合适?
明天预告 · Day 23:向量与 embedding。今天我们把文档切成了一张张卡片。明天讲六步里的③嵌:怎么给每张卡片算一个"语义坐标"——把文字变成一串数字(向量),让"意思相近"变成"距离相近"。这是 RAG 能"按意思找"而不是"按关键词找"的核心魔法。
← Day 21 RAG 全景 Day 23 · 向量与 embedding →