简介:这是一份聚焦RAG(检索增强生成)场景的切片策略源码指南,面向正在搭建知识库问答系统的开发者与算法工程人员,旨在解决长文档如何合理切割以提升检索召回率与生成答案质量的核心问题。资源共5个文件,压缩包仅13KB,包含2个Python演示脚本、1个依赖说明文本及项目配置文件;其中py脚本分别对应语义切片与LLM语义切片的可运行示例,便于读者直接运行观察切分效果。指南系统梳理了改进的固定长度切片、语义切片、LLM语义切片、层次切片和滑动窗口切片五种方案,从核心思想、工作流程、优缺点到适用场景均给出清晰对比,并附有基于FAISS搭建本地知识库的流程图与选型建议,帮助读者按业务需求快速选定合适策略。已有195人学习,适合对RAG流程有基础认知、希望深入理解并实践切片策略的开发者参考。 做RAG最让人头疼的不是模型选型,而是文档怎么切。我见过太多项目,embedding模型选得不错、向量库也搭得没问题,最后检索结果差得离谱,查来查去,问题全都出在切片策略上。切片切得太小,语义被拦腰斩断;切得太大,检索出来的上下文塞满了无关信息,给大模型喂了一堆噪音。这篇文章围绕RAG切片策略,把固定窗口、递归字符、语义切片、结构切片这几种主流思路讲透,附带一套我常用的Python源码实现,帮你在实际项目中快速定位最适合自己的切法。适合正在搭建知识库问答、文档增强检索的工程师,以及想系统理解RAG切片原理的产品和算法同学。
1. 切片这件事,为什么值得认真对待
1.1 切多小才合适:embedding长度限制与检索精度
任何一个RAG系统的第一道工序,都是把原始文档切成若干个小块,再逐个向量化存入向量库。embedding模型有固定的token上限,OpenAI的text-embedding-3-small支持8191个token,但本地常用的bge系列、m3e系列上限普遍在512或1024个token。超过上限的部分会被直接截断,截断意味着语义碎片化,后面的内容在检索时压根不会参与匹配。
但切得太小同样有问题。一个300字的段落被切成5段,每段只有60字,向量里包含的有效语义太少,和高频词相关的噪声会被放大,检索时很容易召回一堆相似的废片。切得太大呢?向量会把整块内容的语义"平均化",关键信息被稀释,召回精度同样下降。这里有个生活化的类比:切片就像切西瓜,切得太碎,汁水全流了,一口下去全是渣;切得太大,又一口咬不下,吃相难看。找到那个"刚好能咬一口"的粒度,就是切片策略要解决的核心问题。
1.2 切片是检索和生成的连接点
整个RAG链路是:文档加载 → 切片 → 向量化 → 检索召回 → 拼接上下文 → 大模型生成。很多人把注意力放在embedding模型和向量库上,却忽略了切片才是连接"检索"和"生成"的枢纽。切片决定了检索的最小单元,也决定了最终拼进prompt的上下文结构。
大模型的上下文窗口是有限的,哪怕现在主流模型都支持128K token,你也不可能把整本手册都塞进去。切片之后,每一条recall结果就是一个独立的信息块,切片切得好,检索回来的每块信息都能直接服务答案生成;切得不好,模型拿到的是残缺的句子、断开的表格、被截半的代码,再强的推理能力也救不回来。可以说,切片的质量上限,就是RAG系统的质量上限。
2. 四种主流切片策略及适用场景
2.1 固定大小切片:最简单但最容易"腰斩"句子
固定大小切片是最朴素的做法,按字符数或token数机械切分,每段设定一个固定长度,再加一个overlap重叠区。比如每512字符切一段,重叠64字符。实现代码很短:
def fixed_size_chunk(text: str, chunk_size: int = 512, overlap: int = 64) -> list[str]: chunks = [] start = 0 while start < len(text): end = min(start + chunk_size, len(text)) chunks.append(text[start:end]) start += chunk_size - overlap return chunks这种方式的优点是快、稳定、可复现,处理日志、流水记录、格式统一的短文本时非常好用。但缺点同样明显:它完全不理解语义边界,一个长句会在任意位置被切成两半,段落被硬生生拆开。我见过一个项目用固定切片处理法律文书,结果"甲方应于本合同签订之日起15日内……"被从中间切断,后半句跑到下一块里,检索时怎么都召回不全,最后生成的答案自然缺胳膊少腿。
2.2 递归字符分割:LangChain默认方案,性价比之王
递归字符分割是目前最常用的方案,也是LangChain内置的默认切分器。它的思路是维护一组有序分隔符,按照优先级从高到低递归切分:先按段落标记\n\n切,切出来的块还太大,就按\n切,还不行就按句号、逗号、空格,直到所有块都小于设定的chunk_size。
这样做的好处是,尽可能保留了文档的天然语义边界——段落优于句子,句子优于短语。对于中英文混合的技术文档、新闻、博客,大部分情况下都能得到不错的结果。递归字符分割不识别语义,但它的"按分隔符递归"天然规避了固定切片那种"腰斩句子"的问题,属于成本最低的合格方案。后面第4章我会给出完整的源码和参数调优建议。
2.3 语义切片:按语义边界切,贵但值得
如果对检索质量有极高要求,可以试试语义切片。它的核心思路是:先对文档里的每个句子做embedding编码,计算相邻句子之间的相似度,在相似度明显下降的位置切分。这样切出来的每个块,内部语义高度连贯,块与块之间的边界刚好落在话题转换处。
我实际测过语义切片的召回效果,确实比递归字符分割要稳一些,尤其是处理概念密集的学术论文、技术白皮书时,语义边界往往和段落边界错位,用递归分割会漏掉一些跨段落的关联内容。但代价也很明显:每个句子都要调用一次embedding接口,离线切分耗时是递归分割的几十倍,如果是花钱调API,成本会直线上升。语义切片适合对知识库质量要求极高的场景,比如医疗问答、金融合规审查,一般场景先别急着上。
2.4 结构切片:Markdown/HTML标题切分,信息最完整
文档本身是带结构的——Markdown有标题层级,HTML有H1-H6,PDF有章节和大纲。结构切片利用这些信息,按标题层级把文档切分成树状结构,每个节点单独作为一个检索单元,同时保留父级标题作为上下文元数据。
这套策略在处理API文档、Wiki知识库、产品手册时效果拔群。举个例子,一个Markdown文档有三级标题,按##切出来的块会自然携带该章节下所有###内容,而且元数据里可以记录完整路径,检索命中后能直接告诉用户"这个答案出自第3章第2节"。信息完整性是四种策略里最高的,但前提是文档格式必须规范,如果原始文档没有清晰结构,这套方法就无从下手。
四种策略的对比我整理成了一张表,方便快速选型:
| 策略 | 优点 | 缺点 | 适用场景 | 离线速度 | 成本 |
|---|---|---|---|---|---|
| 固定大小 | 实现简单、极快 | 语义边界随意切断 | 日志、流水、短文本 | 极快 | 极低 |
| 递归字符 | 兼顾语义边界与长度 | 不识别真正语义 | 通用文档、新闻、博客 | 快 | 低 |
| 语义切片 | 语义完整、检索质量高 | 耗时、成本高 | 学术论文、医疗金融问答 | 慢 | 高 |
| 结构切片 | 信息完整、可追溯 | 依赖文档结构规范 | Markdown/HTML知识库 | 快 | 低 |
3. 核心指标与选型依据
3.1 检索命中率(Recall@K)
选切片策略不能靠感觉,需要量化指标。最核心的指标是召回率,评估方式:准备一组query,人工标注每一条query对应的正确答案落在哪个切片里,检索后看Top-K结果是否包含该切片。比如准备了50条query,采用某个切片参数后,有42条query的正确答案出现在检索结果前5条里,那Recall@5就是84%。
实际操作中,我会固定embedding模型和向量库不变,只修改切片策略和参数,对比同一个评测集上的Recall@5和Recall@10。这个指标直接反映了切片对"找得到"的影响,是最不受下游生成环节干扰的纯检索指标。如果换了切片策略后Recall@K没有提升,说明改动是无效的,别被个例效果迷惑。
3.2 上下文利用率与忠实度
检索召回率高不代表生成质量好。有些切片虽然能命中,但块里大量内容与问题无关,塞进prompt后模型很容易被噪声带跑。这时候要看上下文利用率——统计生成答案中实际引用到的切片内容,占所有输入切片内容的比例。利用率低说明切片粒度太大,需要调小chunk_size。
另一个指标是忠实度(Faithfulness),直观理解就是:生成答案是不是严格基于检索到的上下文,有没有编造。RAG系统最常见的翻车就是模型脑补,上下文里没有的信息被"一本正经地胡说八道"。切片质量差时,喂给模型的上下文残缺不全,模型为了凑答案只能编造,忠实度断崖式下降。用RAGAS或自建的简单规则就能评估忠实度,这部分建议纳入日常回归测试。
3.3 切分成本与延迟
除了质量指标,还要算经济账。固定长度切片和递归切片属于纯规则处理,毫秒级完成,几乎不产生额外成本。语义切片需要调用embedding模型对每个句子编码,假设一篇5000字的文档有300个句子,就要300次embedding调用,成本是递归切片的几十倍。如果知识库有百万级文档,这个成本差距会被放大到不可忽视的程度。
延迟同样值得关注。离线切分慢一点还能忍,但如果你的系统需要实时处理用户上传的文档(比如在线知识库工具),每次上传都要等语义切片跑完,用户体验会很糟糕。我的建议是:线上系统默认用递归字符分割,需要更高精度时,提前离线做语义切片,把切好的结果缓存起来,别让用户在线等。
4. 实操:一个基于递归切片的完整实现
4.1 环境准备与依赖
这个实现我用的是Python 3.10和LangChain的文本分割库。如果你不想引入整套LangChain,也可以用纯Python自己实现,但langchain-text-splitters这个库把递归分割做得很成熟,边界情况处理得比较完善,没必要重复造轮子。
pip install langchain-text-splitters4.2 关键源码:自定义递归分割器
下面这段代码我直接在项目里用过,改了几轮,现在这个版本对中文文档的支持比较稳定:
from typing import List from langchain_text_splitters import RecursiveCharacterTextSplitter def build_recursive_splitter( chunk_size: int = 512, chunk_overlap: int = 80, separators: List[str] | None = None, ) -> RecursiveCharacterTextSplitter: if separators is None: # 中文场景,分隔符优先级从高到低 separators = ["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""] return RecursiveCharacterTextSplitter( separators=separators, chunk_size=chunk_size, chunk_overlap=chunk_overlap, length_function=len, keep_separator=True, )注意几个关键点。length_function=len表示按字符数计算长度,而不是按token数。对于中文,一个字差不多就是1个token的语义量,按字符数算简单直接,处理中文时不会出现"20个英文单词只占20字符,但中文20个字符已是完整句子"这种偏差。keep_separator=True很关键,切分时把分隔符保留在块尾,这样"第一段结尾的句号"不会丢,拼回语义时更完整。
实际调用示例:
splitter = build_recursive_splitter(chunk_size=512, chunk_overlap=80) text = "你的长文档内容……" chunks = splitter.split_text(text) for i, chunk in enumerate(chunks): print(f"chunk {i}: {chunk}")4.3 chunk_size、overlap、separators 怎么调
这三个参数是递归切片的核心旋钮,我给出实测下来的经验值。
chunk_size:普通中文文档我建议设在450到600字符之间。太小的chunk(比如128字符)会导致语义碎片化,向量表示不稳定;太大(超过1000字符)又会让检索结果里混入大量无关内容。英文技术文档建议200-400个token,可以调用tiktoken按token数精确切分。
chunk_overlap:一般取chunk_size的10%-20%。overlap的作用是保留边界处的上下文衔接,比如前一块结尾在讲"虽然A方案有优势",后一块开头是"但在成本上劣势明显",没有overlap的话,模型看到后一块会完全不知道"但"在转折什么。80字符的overlap对512字符的chunk来说,刚好能覆盖一两句话的边界上下文,性价比很高。overlap不是越大越好,太大等于重复内容过多,浪费上下文窗口。
separators:中文文档一定要在默认分隔符里加上中文标点。默认的LangChain分隔符是按英文习惯设计的,只有\n\n、\n、空格,对中文文本几乎等于降级成固定切片,句子会被随意切断。我的经验是,把"。"、"!"、"?"、";"、","放在较高优先级,这样才能保证"宁可细分,不破长句"。
4.4 验证切片质量:小规模评测集
参数调完,怎么知道调得好不好?我的习惯是,搭一个二三十条的微型评测集。具体做法:从知识库里抽取几篇有代表性的文档,人工标注出20到30个"问题-答案所在段落"对。然后写一个简单的检索脚本,用相同的embedding模型和向量库,对比不同参数下的Recall@5。
我通常用下面这段代码快速对比:
def evaluate_recall(chunks, eval_queries, embed_func, top_k=5): chunk_vecs = [embed_func(c) for c in chunks] hit = 0 for q, golden_chunk in eval_queries: q_vec = embed_func(q) sims = [cosine_similarity(q_vec, c_vec) for c_vec in chunk_vecs] top_indices = sorted(range(len(sims)), key=lambda i: sims[i], reverse=True)[:top_k] if golden_chunk in [chunks[i] for i in top_indices]: hit += 1 return hit / len(eval_queries)这个简易评估法不完美,但它能快速告诉你在同一组数据上,到底是chunk_size=512好,还是768好。二三十条query就够判断趋势了,不用追求统计学显著性,重点是去掉"拍脑袋调参"的习惯。
5. 常见问题与排查技巧实录
5.1 表格切片后被拆散
现象:Markdown表格在切片后只剩半张表,检索时模型拿到的全是残缺的单元格,生成答案时经常把"价格"和"数量"对不上号。原因很简单,普通文本分隔符不认识表格结构,|和---不会被特殊对待。
解决办法有两种。第一种是给分隔符加上表格专有标记,比如禁用"|"作为切点;第二种更稳妥,在切片前用表格解析器把Markdown表格转成结构化的文本表示,每个表格整体作为一句话或一个块。我常用markdownify或pandas.read_html先把表格提取出来,再单独处理。
5.2 代码块被拦腰截断
技术文档里经常嵌代码块,用递归分割时,代码块可能被某个分隔符从中间切开,语法结构完全破坏,后面的代码片段失去上下文。比如一段Python代码被切成两半,检索时模型只看到半个for循环,generate出来的代码自然语法错误。
解决方法是把代码块起始标记```加到分隔符数组里,并且优先级高于普通换行。这样切分时遇到代码块边界会优先完整保留代码块,而不是从内部随机切断。如果文档里代码块数量多,可以考虑用结构感知切分,先解析出代码块区域,整体作为一个chunk对待。
5.3 中文标点与分词边界问题
很多从英文项目迁移过来的同学,直接沿用空格分词的分隔符配置,切中文文档时发现效果稀烂。原因很简单,中文句子之间没有空格,默认分隔符里只有空格和换行,最终就退化成固定长度切片,长句被硬切。
把中文标点加入separators只是第一步,还要注意逗号优先级的问题。逗号在中文里使用频率极高,如果它的优先级设得太高,句子会被切得稀碎;设得太低,长段落又切不动。我的实践是:句号、问号、感叹号优先级最高,分号其次,逗号再往后放,但比空格高。
5.4 元数据丢失问题
切片后,如果每块只保存纯文本,会丢掉"这段文字来自哪个文档、哪个章节"的关键信息。等到检索命中后,用户追问来源,系统只能回一句"我也不知道在哪"。更麻烦的是,后续做引用溯源、权限控制时,没有元数据寸步难行。
解决思路:切分时把source、title、page、section等字段作为metadata保存到向量库里。LangChain的RecursiveCharacterTextSplitter支持create_documents方法,可以给每个切块附加元数据,这个习惯我从一开始就保留了。
常见问题我整理成了一张速查表:
| 问题现象 | 根因 | 解决方向 |
|---|---|---|
| 表格内容残缺 | 分隔符不识别表格结构 | 先解析表格,整体作为一个块 |
| 代码块被截断 | 未识别代码块标记 | 分隔符加入``` 或整体保留 |
| 中文长句被切断 | 缺少中文标点分隔符 | 调整separators优先级 |
| 无法溯源/权限失控 | 切块未保留元数据 | 使用create_documents附加metadata |
最后再分享一个我做项目时反复验证过的体会:切片策略别一上来就追语义切片,80%的场景用递归字符分割就足够了,关键是肯花时间调separators和overlap,并且用一个小规模评测集去验证效果。先把基础方案跑通、跑稳,如果评测数据显示确实存在语义断层问题,再针对性升级到语义切片或结构切片也不迟。切片这事没有银弹,但沿着"量化评估 → 对症调整"这条路走,不会踩太深的坑。
本文还有配套的精品资源,点击获取