RAG 的召回质量上限由切分决定,不是由向量库决定。可直接套用的结论:事实型问答用 64–128 token 的小块,需要跨段理解的场景用 512–1024 token 的大块,重叠率取块大小的 10%–20%,并且优先按标题等结构边界切而不是按字符数硬切。切错了,后面换再好的 embedding 模型也救不回来。
为什么切分是 RAG 的第一性问题
检索是「块级」的:你召回什么块,模型就只能基于什么块回答。切分带来两类失败,方向相反:
- 块太大:一个块里塞进多个主题,向量被稀释成「平均语义」,相似度算不准;同时浪费上下文窗口,把噪声一起喂给模型。
- 块太小:句子被拦腰截断,语义不完整,命中了也没法回答;需要跨块推理的问题直接失效。
多篇针对长文档检索的对比实验给出了一致的规律:最优块大小取决于你的查询类型,而且不同 embedding 模型对块大小的敏感度不同——有的模型(如 Stella 一类)在大块上更能利用全局上下文,有的(如 Snowflake 一类)在小块上做细粒度实体匹配更准。这意味着「抄一份通用参数」本身就是错的。
四种切分策略怎么选
| 策略 | 做法 | 适用 | 代价 |
|---|---|---|---|
| 固定大小切分 | 按 token/字符数切,带 10%–20% 重叠 | 基线方案、格式杂乱的语料 | 会切断段落与标题,语义不完整 |
| 结构(标题)切分 | 按 Markdown/HTML 标题层级拆分,块内保完整小节 | 文档、手册、 Wiki | 依赖源格式规整;小节过长需二次切分 |
| 语义切分 | 按相邻句 embedding 相似度骤降处切分 | 高精度场景(法律、医疗) | 需额外推理,长文档易漏掉跨块信息 |
| 父子块(Parent-Child) | 小块用于检索,命中后回填整篇父文档作上下文 | 多跳推理、长文档、答案分散 | 上下文变大、噪声增加、处理更慢 |
最实用的组合:结构切分打底 + 父子块兜底。先按标题切开保证语义完整,再对超长小节做二次固定切分;检索用小粒度子块保证精度,生成时用父块保证上下文完整。这套在中文技术文档上效果最稳。
落地步骤与可套用参数
- 先解析再切分:PDF/HTML 用专业解析库把文档拆成结构化元素(标题、正文、列表、表格),表格保留 HTML 表示。别用裸文本硬切,否则表格会碎成一堆无意义数字。
- 按结构边界切:使用层级分隔符(
#标题 → 代码块 → 分隔线 → 段落 → 句子 → 字符),让切分点尽量落在天然边界上。 - 用 tokenizer 计数,不用字符数:字符数与 token 数在中文里差距极大,用字符设 1000 可能实际超 1500 token,超过模型
max_seq_length后会被静默截断,直接丢信息。 - 块大小对齐 embedding 模型上限:查模型的
max_seq_length(常见 512),块大小取其 70%–90%,重叠取块大小的 1/10 。短问答场景可降到 128–256 。 - 带上元数据一起存:每个块记录文档 ID 、所属章节、页码、源 URL 。元数据既能在检索时做过滤,也是生成答案时给出引用的唯一凭据。
用 tokenizer 计数的正确写法
from langchain.text_splitter import RecursiveCharacterTextSplitter
from transformers import AutoTokenizer
MARKDOWN_SEPARATORS = [
"\n#{1,6} ", "```\n", "\n\\*\\*\\*+\n", "\n---+\n", "\n___+\n",
"\n\n", "\n", " ", "",
]
EMBED_MODEL = "thenlper/gte-small" # max_seq_length = 512
def split_documents(chunk_size, docs, tokenizer_name=EMBED_MODEL):
splitter = RecursiveCharacterTextSplitter.from_huggingface_tokenizer(
AutoTokenizer.from_pretrained(tokenizer_name),
chunk_size=chunk_size,
chunk_overlap=int(chunk_size / 10), # 10% 重叠
add_start_index=True,
strip_whitespace=True,
separators=MARKDOWN_SEPARATORS,
)
out, seen = [], set()
for d in docs:
for c in splitter.split_documents([d]):
if c.page_content not in seen: # 去重,避免重叠造成重复索引
seen.add(c.page_content)
out.append(c)
return out
chunks = split_documents(448, docs) # 512 的 ~87%,留截断余量
为什么必须检查块长度分布?
切完之后画一张 token 长度直方图,是唯一能验证参数是否合理的手段。如果右侧有明显长尾超过模型
max_seq_length,说明这些块在 embedding 时会被截断,尾部内容在检索阶段完全不可见——这是「明明文档里有,就是搜不到」最常见的根因。调小 chunk_size 直到长尾消失。上线前必做的三件事
- 重排(Rerank):向量召回 top-K 后用 cross-encoder 重排,是性价比最高的一步提升,通常比调块大小见效更快。
- 建评测集:准备 20–50 条真实问题,标注预期命中文档。用 Faithfulness(是否只依据检索到的证据)、 Context Recall(所需信息是否被召回)等指标量化,别凭感觉调参。
- 引用与拒答:提示词里明确要求模型给出来源引用,并在证据不足时直接说「不知道」。没有引用的答案无法人工核验,在生产环境里等于不可用。
中文场景别直接套英文参数:同样 512 token,中文承载的信息量约为英文的 1.5–2 倍。中文技术文档建议从 256–384 token 起步做网格搜索,比照搬 512 更贴近实际。
相关阅读
延伸阅读:RAG 与向量数据库块大小到底设多少?
没有通用值。经验起点:事实型短问答 64–128 token;需要上下文理解的长问答 512–1024 token;中文技术文档可从 256–384 token 起步做网格搜索。用你的评测集跑一遍,看 Context Recall 和 Faithfulness 哪个更高。
重叠率设多少合适?
取块大小的 10%–20%。低于 10% 易在边界处切断关键句,高于 20% 会显著增加索引体积和重复召回。父子块策略下可以更低,因为上下文由父块补齐。
能用字符数代替 token 数吗?
不能,中文尤其不行。中文一个字往往对应 1 个以上 token,按字符设 1000 实际可能远超模型上限被静默截断。必须用对应 embedding 模型的 tokenizer 计数。
表格和代码块怎么处理?
解析阶段就把表格保留为结构化元素(含 HTML 表示),代码块作为独立分隔符层级优先保护,避免被从中间切开。切碎的表格和代码在检索中几乎不可用,还会污染向量空间。
参考来源:Hugging Face 官方 Cookbook 《 Advanced RAG 》、 HF Papers 《 Rethinking Chunk Size For Long-Document Retrieval: A Multi-Dataset Analysis 》及 HF 社区 RAG 切分策略配置集。本文为基于上述公开材料的原创改写与工程化整理。








评论 (0)