
简介面向AI应用开发者的RAG切片策略示例源码包围绕检索增强生成中的长文档切分难题覆盖改进固定长度、语义、LLM语义、层次与滑动窗口五种切片方法的代码实现、工作流程与选型场景。压缩包共5个文件以两个Python演示脚本为主配合依赖清单与工程配置可直接在本地搭建环境运行验证整体仅13KB轻量易读。已有195人学习。通过阅读和运行调试脚本可以直观理解不同切片策略在上下文连贯性、检索颗粒度、计算开销与语义保持上的差异并对照其与FAISS本地知识库结合的流程为实际项目选择合适的分块方式提供代码级参考。尤其在长文本场景下可对比不同切片设置的检索效果减少试错成本。适合正在搭建RAG问答系统、需要优化文档切分效果的开发者快速上手。 做RAG项目的人最容易在切片策略上翻车。你兴冲冲把文档灌进知识库换了更强的Embedding模型加了Rerank重排甚至把向量库从开源版换成了分布式版本最后召回效果依旧稀碎。我先说结论如果不先把切片策略做对后面所有环节的优化都是在帮一个烂结构打补丁。所谓RAG切片策略就是把原始文档切成便于向量检索的小块再入库的过程。切太大了一段里混着好几个主题语义被稀释切太小了答案被拦腰截断模型拿到的只是半截信息。这篇文章我会把RAG里最常见也最容易被低估的切片策略从原理到源码完整走一遍包括固定长度切分、递归字符切分、标题感知切分、语义切分以及各自适用的边界和参数经验值最后再用真实项目中踩过的坑收尾。适合正在搭知识库、做文档问答、或者已经上线但召回质量不理想的朋友参考。1. 为什么一到生产环境召回准确率就崩了1.1 检索召回的本质切出“能独立回答问题的语义单元”先把RAG的链路在脑子里过一遍用户提问 - 问题向量化和文档向量做相似度计算 - 取Top K个最相似的切片 - 把切片文本拼到Prompt里 - 大模型基于这些切片生成回答。你会发现切片是检索的基本单元。向量检索的召回对象不是“整篇文档”而是那一块块切片。如果某个切片本身就不是一个完整的语义块那后面的重排、生成做得再好模型也只能拿着残缺材料强行作答。我把这个问题类比成书架管理如果书被按页码拆散每页单独编了一个索引入库用户想找“如何在Python里合并两个字典”检索可能命中的是某一页但那一页只有正文的三分之一没有代码示例也没有最终结果。你总不能指望模型凭半页内容补全整段逻辑吧切片策略的本质就是在回答一个问题用户问题对应的答案在文档里以什么粒度存在我就以什么粒度切分。粒度对了召回和生成的体感会同步提升粒度错了后面无论怎么调都是在原地打转。1.2 切片不当的典型症状先帮自己做个诊断在生产环境里切片策略没做好一般会表现出下面四种症状。如果你的知识库也出现类似情况基本可以怀疑切片环节出了问题。答非所问chunk太大一个块里塞了多个主题query只和这块文本的中间部分语义相关相似度被其他无关内容稀释。模型拿到的是一整坨信息回答容易飘。信息残缺chunk太小答案被切断成两半。比如一段900字的操作说明偏被切成500400模型拿到的是上半段关键结论在下半段于是开始一本正经地胡说。上下文断裂按固定字符数硬切切点落在句子中间或者表格线条中间。喂给模型的片段读起来像乱码拼接别说模型了人看着都莫名其妙。定位偏差切片丢失了章节标题、文件名、页码等元数据。检索明明命中了某一段内容但无法追溯到来源文档回答完了没法解释依据这在企业场景下等于白做。这四种症状不是独立出现的实际项目里经常是两三种叠加。我在下面几个章节里会针对每一种情况给出对应的切分实现和修复策略。2. 动手写切片代码前先想清这三件事2.1 文档形态决定切分粒度的上限很多教程上来就教调用某个Splitter读者跟着敲完就以为完事了。但真正动过手的人应该深有体会文档解析这一步直接决定切片策略能不能生效。建议先花几分钟摸清你的语料里到底有哪几类文档形态再决定怎么切散文类说明文、技术博客、公众号文章语义密度均匀用递归字符或语义切分都行。Markdown / HTML天然带标题层级和段落标题感知切分优先级最高。表格类财报表格、设备参数表、合同清单按行切会丢表头按表切可能超过上下文通常需要把表头拼进每一行或者转成自然语言描述后再切。代码仓库按字符切会把函数、注释、字符串截得稀碎工程上用language-aware切分器按语法边界切。合同条款 / 法规条文按条、款、项作为切分单元比按字数切科学得多。一句话总结先看结构再定参数。没有一种通用切法能通吃所有文档所谓的通用切片器本质上也只是靠分隔符列表去“猜”结构。2.2 先确定检索单元再谈切片参数在做任何切分之前先问团队一个问题用户问这个知识库时一次回答最理想情况下需要多长的一段原文这一段原文就是你的检索单元。比如做产品说明书问答用户问“如何校准传感器”答案可能是一整套步骤需要把一整节操作说明完整给到模型那么检索单元就是“一个小节”做客服工单知识库用户问“退款到账要多久”答案可能就一两句话检索单元就是“一个要点段落”。建议先对知识库里最常见的20个真实问题做标注统计看看标准答案在原文里的平均长度是多少。这个统计值会直接决定chunk_size的基准线。检索单元定下来之后切片参数才有参照物。否则随便设个500、800纯属拍脑袋。2.3 别忽略模型窗口和向量召回预算切片大小不仅由文档结构决定还得考虑实际推理阶段的预算。假设你设置每块800字召回Top 4那一次要给模型喂约3200字原文再加上问题和指令按中文1个汉字约1.2~1.5个token估算光这一段就占了近5000 token。如果模型窗口是8K留给生成的空间就很紧张了。所以切片size要跟两步走先看Embedding模型的适用长度再看LLM上下文窗口的余量。在窗口内我会优先选择能保留完整语义的较大切片因为长窗口是趋势如果窗口紧张就得用小切片配合父文档回填后面章节细讲。这不是一个独立决策而是与模型选型联动的一环。3. 四种常用切分策略源码实现、原理与边界3.1 固定长度滑动窗口最暴力也最稳定固定长度切片不需要任何外部依赖逻辑简单适合快速验证和兜底。核心逻辑是有一个chunk_size和一个overlap按字符数滑动截取。def fixed_size_chunking( text: str, chunk_size: int 500, overlap: int 100 ) - list[str]: if chunk_size - overlap 0: raise ValueError(chunk_size 必须大于 overlap) if len(text) chunk_size: return [text] if text.strip() else [] step chunk_size - overlap chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end].strip()) if end len(text): break start step return chunks这套代码能跑但它有一个致命问题切点对语言结构无感知。中文还好一点英文常把单词和短语拦腰切断。生产环境如果实在要用固定长度切通常会在临近切点的地方往前找最近的句号、分号、问号来落刀尽量减少句子被截断的概率。这种策略适合什么场景文本本身没啥结构、长度又相对均匀的日志、说明片段。它是底线方案能保证每块大小一致但别指望质量多高。3.2 递归字符切分工程界的默认答案如果你去翻LangChain里的TextSplitter文档绝大多数知识库项目最终都会停在RecursiveCharacterTextSplitter上。它背后是很朴素的思路维护一个分隔符优先级列表先尝试用最粗的分隔符把文本切到理想尺寸如果切完的块还太大就降到下一级分隔符继续切。分隔符优先级示例从段落级\n\n到行级\n再到标点级。最后到字符级。这种从粗到细递进的方式基本能保证切出来的每一块都尽量落在自然句子上。from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size800, chunk_overlap120, separators[\n\n, \n, 。, , , , , ], keep_separatorend, ) chunks text_splitter.split_text(long_document)对于代码文件LangChain还内置了按编程语言语法切分的能力用RecursiveCharacterTextSplitter.from_language指定语言后它会优先在函数定义、类定义、导出语句这些边界处切from langchain_text_splitters import ( RecursiveCharacterTextSplitter, Language, ) python_splitter RecursiveCharacterTextSplitter.from_language( languageLanguage.PYTHON, chunk_size800, chunk_overlap100, )这段代码里的keep_separatorend值得考究把分隔符保留在切片末尾而不是开头可以让后一个切片的内容更连贯模型读起来不会明显感觉到跨段。实际效果我测过对生成的连贯性有帮助。3.3 标题感知切分结构化文档的最优解Markdown和HTML这类文档天然带标题层级标题本身就是最佳的语义切分锚点。按标题切本质上就是按章节切返回的每一块都自带章节归属命中之后还能直接告诉用户“答案在第3.2节”体验会好很多。我早期用LangChain的MarkdownHeaderTextSplitter但它对自定义文档结构不够灵活后来干脆自己写了一个轻量版。核心思路维护一个标题栈遇到新标题就开一个新chunk子标题通过元数据继承父标题这样既保住了层级又不会把整个层级全压进正文。import re from dataclasses import dataclass, field dataclass class Chunk: content: str metadata: dict field(default_factorydict) # 正则匹配 Markdown 标题 HEADING_RE re.compile(r^(#{1,6})\s(.*)$) def heading_aware_chunking(md_text: str) - list[Chunk]: chunks [] stack [] # 标题栈保存 (level, title) current_lines [] def flush(): if not current_lines: return meta { path: / .join(title for _, title in stack), headings: {fh{lv}: title for lv, title in stack}, } chunks.append(Chunk(content\n.join(current_lines).strip(), metadatameta)) current_lines.clear() for line in md_text.splitlines(): m HEADING_RE.match(line) if m: flush() level len(m.group(1)) title m.group(2).strip() while stack and stack[-1][0] level: stack.pop() stack.append((level, title)) current_lines.append(line) else: current_lines.append(line) flush() return chunks这段代码生产里可以直接用处理普通Markdown文档足够稳。背后的关键点在于切分不仅产生内容还产生结构化元数据。这些标题层级元数据后续可以注入到向量库的metadata里用于过滤检索范围也可以直接作为回答时的引用溯源。3.4 语义切分效果好但代价不低固定长度和递归切分都是在“文本表面”上做文章语义切分则是往上走一层先算出句子级Embedding再根据相邻句子之间的语义相似度来判定自然边界。原理也不复杂语义越相近的句子余弦相似度越高当相邻句子的相似度突然掉到阈值以下说明话题发生了切换这个地方就是应切分点。import numpy as np from typing import Callable EmbeddingFn Callable[[str], np.ndarray] def semantic_chunking( sentences: list[str], embed_fn: EmbeddingFn, max_chunk_len: int 8, similarity_threshold: float 0.72, ) - list[str]: chunks [] current [sentences[0]] prev_vec embed_fn(sentences[0]).reshape(1, -1) for sentence in sentences[1:]: vec embed_fn(sentence).reshape(1, -1) norm np.linalg.norm(prev_vec) * np.linalg.norm(vec) if norm 0: sim 0.0 else: sim float(np.dot(prev_vec, vec.T) / norm) if sim similarity_threshold or len(current) max_chunk_len: chunks.append( .join(current)) current [] current.append(sentence) prev_vec vec if current: chunks.append( .join(current)) return chunks但注意这段代码对每个句子都要调一次Embedding模型文档一长耗时和费用都会上来。为了控制成本通常会对句子做一次抽稀不是每个句子都算向量而是先按字符长度把文档粗切成小段再在段边界附近做语义判定。语义切分适合什么场景主题跳跃明显的会议纪要、访谈记录、问答对话。这类文本按规律切分效果很差因为前后的句法结构差异巨大但语义上的边界又比较清晰。代价高效果也确实好前提是你的硬件和延迟预算扛得住。4. 调参和善后决定切片最终效果的下半场4.1 用三个指标评估切片质量切完片子不是直接灌库就完了一定要有评估。我在真实项目里最常用三个指标也几乎是业内衡量切片质量的通用维度指标含义推荐取值切片命中率包含标准答案且被Top K召回的比例越高越好目标 85%命中位置答案文本在切片中的相对位置越居中越好避免答案落在边缘被截断答案完整度切片包含答案关键要点的完整比例与切片大小正相关你可以在100条标注好的“问题-标准答案原文”上跑一次全流程评测看看命中的切片是不是恰好覆盖了答案的完整段落。如果命中位置普遍在切片开头或结尾说明chunk_size偏小或overlap不够需要相应调整。4.2 overlap越大越好不是overlap是相邻两个切片之间重复的部分目的是避免答案正好卡在两个切片的缝隙里。很多人以为overlap越大越安全其实这是一个误区。overlap过大带来的直接问题是同一段内容被重复向量化既浪费存储又让相似的向量重复出现在Top K结果里挤压了其他有效切片的名额。检索Top K本来就很珍贵如果前三个都是同一段话的变体真正该进来的内容反而被挡在门外。我自己的经验值是chunk_size在500~800字时overlap取60~150字一般取chunk_size的10%~20%。原则是overlap只负责覆盖边界不负责补救语义丢失。如果答案经常被拦腰切破优先考虑的应该是换更大的chunk而不是盲目靠overlap去救。4.3 元数据注入让切片自带身份信息切片灌进向量库以后如果不带元数据那它就是一个没有来源的浮萍召回之后既无法溯源也没法做过滤。常见的元数据包括文档标题、章节路径、页面数、URL、更新时间、文档类型、权限分级。chunk { text: chunk.content, metadata: { doc_id: manual_2024_01, title: 传感器校准说明, section: 3.2 校准步骤, page: 12, updated_at: 2024-06-01, doc_type: manual, } }这些元数据的价值有三个召回阶段做前置过滤比如只搜索权限范围内的文档、生成阶段引用溯源让模型回答时能指出依据、运营阶段做质量分析哪类文档被命中最频繁。如果你用的是OpenSearch或Elasticsearch这类带过滤能力的向量库建议把doc_type、permission这类字段建立为filter字段检索时直接限定范围既提高准确率又省算力。4.4 先定位再回填父文档检索策略一个工程上非常实用的折中方案是小切片检索 大父块回填这也是LangChain里ParentDocumentRetriever的核心思路。具体做法是对外索引的小切片负责召回向量检索用小块提高定位精度一旦某个小切片命中就根据它记录的parent_id把对应的大父块取出来拼进Prompt。这样做的好处是检索精度和上下文完整性同时兼顾而且不需要重新向量化长文本存储成本增长可控。我在做合同审查类知识库时最常用这个方案切成1000字大块每块再细分为200字小块并保留大块ID。查询时用200字小块做相似度匹配命中后回填1000字大块。实测中答案完整度比直接用小块的方案提高约12个百分点又不会像直接用大块那样出现大量无关内容稀释相似度。5. 最容易翻车的切片场景与排查思路5.1 表格类文档被硬切成碎片表格是切片策略的重灾区。按字符硬切的话表头行、数据行会被拆到不同的切片里检索问“Q2毛利率是多少”命中的切片可能只有一行数字根本不知道这数字对应的是哪个季度模型只能乱猜。修复方案通常是表头注入解析出标题行和列名然后把每一行拼成一句自然语言描述“2024年Q2毛利率为42.3%较上季度增长2.1%”。这一步做完再进入切分流程召回效果立竿见影。如果你用的是Pandas或pdfplumber解析表格把DataFrame转成文本描述这个操作很容易写别省。5.2 代码仓库混进文档库把GitHub仓库直接灌进RAG的场景并不少见。纯文本切分器会把函数签名、注释、字符串常量全拦腰截断向量化出来的语义几乎没法用。修复方案有两个方向一是按语法感知的language splitter切二是对代码文件先做AST解析按函数、类、模块为单元组织切片。后者的质量明显更高但实现成本也高一般建议先用LangChain的from_language跑通扛不住了再上AST方案。5.3 长文档的章节层级在切片后丢失处理上百页的长文档时如果按字符切一个切片里可能同时包含某个大标题下的多级子内容章节归属感丢失。用户在对话里问“第三章结论是什么”模型根本不知道哪块属于第三章。解决办法一是用前面提到的标题感知切分二是给每个切片补上完整路径元数据“第三章 / 3.2 / 3.2.1”并在召回后把路径作为prompt上下文的一部分。做了这一步之后长文档问答的定位能力会显著提升。5.4 指望Rerank救切片是个误区很多人把Rerank重排当成“最后的救世主”觉得只要上了重排模型切片切得再烂也能被纠正回来。这个理解需要修正Rerank解决的是排序问题不是召回缺失问题。如果一个切片的语义已经被切得面目全非它的向量在初筛阶段就已经落选Rerank根本看不到它。我在一个知识库项目里做过对比同样用BGE-reranker重排固定长度硬切的方案召回命中率只有61%改成标题感知切分之后同一套重排模型命中率提升到89%。提升的来源不是重排而是切片给重排提供了更多真正相关的候选。切片是上游重排是下游上游漏水下游再努力也接不住多少。最后分享一个我自己的维护习惯项目上线后每两周抽样跑一次真实问题的召回日志看看Top K命中的切片长什么样。切片策略不是配一次就一劳永逸的东西文档库不断增长新的文档形态随时会突破你原来的切分假设。我建议把chunk_size、overlap这些参数放到配置中心留好监控别把参数焊死在代码里。毕竟对RAG来说切片切得好不好直接影响用户感受到的第一印象这个细节值得反复打磨。本文还有配套的精品资源点击获取