☰
RAG 分块策略实测:固定滑窗、段落感知怎么选,80 行 Python 附自检
2026/9/28 20:06:44 网站建设 项目流程

RAG 分块策略实测:固定滑窗、段落感知怎么选,80 行 Python 附自检

之前写过最小 ReAct Agent和Transformer/解码系列,里面的检索都是假占位——这篇把真东西补上:RAG 的第一环,分块。收藏榜上 RAG 进阶帖和向量数据库帖都在收收藏,但进阶帖讲的是父子分块/层级索引那些大设计,基础分块写对的人反而不多。本篇只做两件事:三种策略的代码,和一个能抓住丢字事故的自检。

结论先放这儿

策略怎么切优点缺点
固定滑窗按长度 + 重叠保证不超长,实现最简语义经常被腰斩
段落感知段落聚合,段内按句拆边界贴语义实现多几行
硬切兜底超长单句直接切保证不超长语义最差,只做最后手段

两句话铁律:分隔符优先级从大到小(段落 > 句子 > 硬切),分块代码必须有覆盖率自检。

一、为什么分块是 RAG 第一坑

块太大:向量语义被稀释,检索不精准;块太小:上下文残缺,检到了也答不对。多数人死在第三种事故上:分块代码把字符弄丢了——丢的往往是一句话的开头或结尾,检索照常出结果,答案悄悄变差,不报错,很难查。

所以本篇的自检核心就一条:分块结果拼回去,必须还原原文。

二、完整代码(单文件,直接跑)

# chunking.py — 固定滑窗 vs 段落感知分块 # 依赖:无(纯标准库) import re def fixed_chunks(text, size=200, overlap=20): """固定长度 + 重叠滑窗""" if overlap >= size: raise ValueError("overlap 必须小于 size,否则 step<=0 死循环") step = size - overlap return [text[i:i + size] for i in range(0, len(text), step)] or [""] def split_long(text, max_size): """单段超长:按句拆(保留标点),塞不下的单句硬切兜底""" chunks, buf = [], "" for s in re.split(r"(?<=[。;,])", text): if not s: continue if len(buf) + len(s) <= max_size: buf += s else: if buf: chunks.append(buf) while len(s) > max_size: # 单句超长:硬切 chunks.append(s[:max_size]) s = s[max_size:] buf = s if buf: chunks.append(buf) return chunks def paragraph_chunks(text, max_size=200): """段落感知:\n\n 聚合,段内按句拆,相邻小块合并""" chunks = [] for para in text.split("\n\n"): para = para.strip() if not para: continue for c in split_long(para, max_size): if chunks and len(chunks[-1]) + len(c) + 2 <= max_size: chunks[-1] += "\n\n" + c # 相邻小块合并,减少碎块 else: chunks.append(c) return chunks if __name__ == "__main__": text = "\n\n".join( f"第{i}段。" + "这是测试内容," * (30 if i % 3 == 0 else 5) for i in range(20) ) + "超长无标点句子" * 100 # 触发硬切兜底 # 1) 固定滑窗:去重叠后拼回应原文(丢字/重字当场报) fc = fixed_chunks(text, size=200, overlap=20) rebuilt = fc[0] + "".join(c[20:] for c in fc[1:]) assert rebuilt == text, "滑窗分块丢字或重字" assert all(len(c) <= 200 for c in fc) # 2) 段落感知:无丢字 + 不超长 sc = paragraph_chunks(text, max_size=200) assert "".join(sc).replace("\n\n", "") == text.replace("\n\n", ""), "段落分块丢字" assert all(len(c) <= 200 for c in sc) print(f"固定滑窗: {len(fc)} 块 段落感知: {len(sc)} 块 self-check ok")

两条 assert 各管一类事故:滑窗的覆盖率检查抓重叠偏移算错(最容易写错的地方),段落的无丢字检查抓分隔符切丢内容。块数差异也能看出来:段落感知的块更少更整齐。

三、三个踩坑(都见过真实事故)

  1. token ≠ 字符:中文 1 字对应 1~3 token,chunk_size按字符数设 200,实际 token 可能是 400+,向量库的 token 上限直接报错。预算按 token 算,字符数只是代理
  2. overlap ≥ size 死循环:step = size - overlap ≤ 0,range 一步不走直接卡死。代码里 raise 掉,别信调用方
  3. 表格/代码块被腰斩:Markdown 表格按行切后,每块都是没头的几列,检索命中了也没法用。结构化内容先按块整体提取再分块,别让通用切分器碰它

四、下一步:检索块和上下文块拆开

本篇三策略有个共同假设:检索用什么块,就返回什么块。进阶玩法是把两个职责拆开——子块做检索锚点(短、准),命中后沿映射返回父块(完整上下文),即父子分块/放大检索。这个需要稳定的 parent_id 映射,映射不稳会出现“命中 A 返回 B”的事故,值得单开一篇。

总结

铁律压成三句:

  1. 分隔符优先级从大到小,硬切只做兜底
  2. 块大小按 token 预算算,字符数只是代理
  3. 分块代码必须有覆盖率自检,丢字是最隐蔽的 RAG 事故

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询