RAG 答不准,先检查文本切块边界
文章用 API 密钥轮换问答说明:切块可能拆散主题与关键事实,即使文档已入库也难以正确检索。提供小脚本检查切割位置,帮助在调整嵌入、重排和提示词前定位问题。
AI 深度解读
文档明明写着「旧 API key 在轮换后还能使用 24 小时」,RAG 助手却答不出来,问题可能出在切块:标题留在上一块,时效落进下一块,还混入撤销密钥的说明。信息进了索引,却没有以完整、可理解的形式被检索。
作者用一段 Python 脚本,把固定长度切块和按标题切块摆在一起检查。它值得读的地方是把排查顺序落到了实处:先确认模型拿到的材料是否完整,再考虑 embeddings、重排器和提示词。
为什么答案在索引里,助手还是找不到
检索增强生成(RAG)通常先把文档切成块,为每块生成向量,再根据问题检索相关内容。后续环节处理的都是这些块。原始页面写得再清楚,也不能保证某个被召回的片段保留了完整答案。
作者归纳了三类边界问题:
- 固定字符数可能截断句子,甚至把一个单词拆开。
- 操作说明和所属标题分离,读者不知道操作针对哪个对象。
- 块太小,缺少信息;块太大,混入多个主题,匹配不够集中。
这些问题通常不会触发程序报错。流水线照常运行,质量却悄悄下降。
作者构造了一页虚拟文档,包含 API keys 下的三个小节:Rotate a key、Revoke a key 和 Limits。其中,轮换后的旧 key 可继续使用 24 小时;撤销立即生效且不可恢复;每个项目最多有 5 个活跃 key,轮换不算新增。
这些数字是演示文档里的设定,不是某款产品的真实规则。示例要验证的是:切块之后,问题的主题和回答所需的事实还在不在一起。
答案进入索引,不等于回答问题所需的上下文进入了同一个块。
用一个脚本,看清切口落在哪里
下面的脚本把同一份 Markdown 分别按固定长度和标题切分,再打印风险标记与首尾文本。它只使用 Python 标准库,适合检查切块结果。
import re
import sys
def fixed_size(text, size):
"""Cut every `size` characters, no overlap."""
return [text[i:i + size] for i in range(0, len(text), size)]
def by_heading(text):
"""Split on markdown headings; keep the heading path with each chunk."""
chunks, path, body = [], {}, []
def flush():
content = " ".join(body).strip()
if content:
heading = " > ".join(path[k] for k in sorted(path))
chunks.append(f"[{heading}] {content}")
body.clear()
for line in text.splitlines():
m = re.match(r"^(#+)\s+(.*)", line)
if m:
flush()
level = len(m.group(1))
path = {k: v for k, v in path.items() if k < level}
path[level] = m.group(2)
elif line.strip():
body.append(line.strip())
flush()
return chunks
def flags(chunk, is_last):
text = chunk.strip()
out = []
if not text.startswith(("#", "[")):
out.append("NO HEADING")
if not is_last and not text.endswith((".", "!", "?", ":")):
out.append("CUT MID-SENTENCE")
if len(text.split()) < 8:
out.append("TOO SMALL")
return out
def report(name, chunks):
print(f"== {name}: {len(chunks)} chunks")
for i, c in enumerate(chunks, 1):
f = flags(c, i == len(chunks))
one_line = " ".join(c.split())
print(f"C{i} ({len(c.split())} words) {f or ['ok']}")
print(f" starts: {one_line[:38]!r}")
print(f" ends: {one_line[-38:]!r}")
text = open(sys.argv[1]).read()
report("fixed-size, 120 chars", fixed_size(text, 120))
print()
report("by heading", by_heading(text))
重点看 chunks.append(f"[{heading}] {content}"):标题路径被直接放进每块文本,帮助片段独立表达所属主题。固定长度方案则每 120 个字符切一次,没有重叠。
作者使用 Python 3.13.5,执行 python3 chunk_check.py sample.md。固定长度方案生成 4 块,词数依次为 25、24、22、16;前三块都被标记为 CUT MID-SENTENCE,后三块都被标记为 NO HEADING。
关键事实「24 hours」落在 C2,开头却是 y keeps working,后半部分又转向撤销操作。C3 混合撤销说明和额度标题,C4 从 ve up to 5 active keys 开始。原本清晰的页面,变成了需要跨块拼接才能读懂的材料。
按标题切分后得到 3 块,分别有 35、29、23 个词,全部显示 ok。轮换标题和 24 小时的事实终于在一起。
不过,ok 只表示通过这几个简单检查。NO HEADING 检查开头是否为 # 或 [;CUT MID-SENTENCE 检查结尾是否为 . ! ? :,并豁免最后一块;TOO SMALL 检查是否少于 8 个词。首尾各展示 38 个字符,方便人工定位。
脚本没有检查「过大」,也不会真正理解句子。中文标点和不依赖空格分词的文本,都需要另行适配。
标题要带上,长章节还得继续拆
作者没有主张把所有文档都改成按标题切块。一个章节可能长到不适合整块处理,另一些文档根本没有标题。
更实用的做法是保留结构,再处理长度:每块附带所属标题或标题路径;长章节优先按段落拆分;只有单个段落仍然过长时,才退回硬性长度切分。
标题路径的作用,是补回片段失去的定位信息。例如「点击 Rotate,然后确认」单独出现时不够明确,加上 [API keys > Rotate a key],才知道操作对象是什么。原文借 Anthropic 的 Contextual Retrieval 说明这类上下文缺失,但没有在脚本中实现那套方案。
检查也不该只跑一次。作者建议抽取 10 到 20 份代表性文档,包含长表格、FAQ、更新日志、PDF 转文本等麻烦样本。先统计标记,再阅读被标记的内容,判断问题普遍存在,还是集中在少数文档。
每次只改一个切块设置,并重新运行检索测试。可以用小规模 recall@k 检查:对已有问题,前 k 个检索结果是否找回需要的材料。
原文还介绍了浏览器里的 RAG Chunk Visualizer & Cost Calculator,可显示彩色块、token 估算和质量标记,并称文本留在本机。另有 6 页的 RAG Quick-Start 和 46 页的完整指南。这些是补充资源介绍,不是切块策略有效性的实验依据。
我的判断
我赞成把切块检查放在排查前面。它成本低,能直接暴露「主题和事实分家」的问题,也能避免把输入材料缺损误判成模型能力不足。
但这篇的证据范围很窄:作者实际展示的是一份自造文档上的切分结果,没有真实问题集上的召回率对照,也没有生成答案的准确率数据。按标题切分在这个例子里更完整,不能据此认定它在一般团队的知识库里效果更好。
固定长度、带重叠窗口的切分,以及结构优先再限制长度,都是可以比较的方案。原文只测试了无重叠的固定长度方案,没有给出这些替代做法之间的对照数据。策略建议仍属于经验之谈。
这段 Markdown 解析也只是草图。它逐行识别 #,没有处理围栏代码块等完整语法情形;正文中的空行会被合并,结构信息也可能损失。对于包含代码示例的技术文档,不能直接把它当成生产解析器。
风险标记负责找疑点,检索测试才负责判断改动有没有帮助。
此外,「事实应与主题同块」是很好的默认目标,但原文没有讨论相邻块扩展、父子块检索等补充上下文的办法。如果已有流水线会补回上下文,还需要结合实际送给模型的材料判断问题。
能拿来做什么
- 给文档助手排查错答时,先打印实际召回块的首尾和标题路径,确认用户问的对象与关键事实是否一起出现,再决定是否调整提示词。
- 从技术文档库抽取 10 到 20 份样本,加入代码块、FAQ、表格和 PDF 转文本,运行检查并人工读异常片段;处理中文前,先调整标点和长度规则。
- 调整切块时先保留标题路径,长章节按段落继续拆分,并保留一组真实问题做
recall@k对照,每次只改变一个设置。 - 在 Node 或 Python 的入库流水线中加入同类检查报告,持续观察句中截断和极短片段,让文档更新引入的边界问题更容易被发现。