ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

RAG开发中chunk切分策略与参数调优实战指南

RAG开发中chunk切分策略与参数调优实战指南 在RAG检索增强生成和AI大模型应用开发里chunk这个词出现的频率极高。很多刚转行做大模型开发的朋友第一次看到代码里满屏的chunk_size、chunk_overlap、text_splitter第一反应是“这不就是把文本切碎吗有什么好讲的”。结果真上手做知识库问答发现检索出来的内容要么答非所问要么关键信息被拦腰截断模型一本正经地胡说八道。问题十有八九就出在chunk这一层。这篇内容我打算把chunk这件事从头到尾讲透它到底是什么、为什么大模型开发绕不开它、切分策略怎么选、参数怎么调、实际项目里踩过哪些坑。不管你是刚接触AI大模型应用开发的新手还是从Java后端转过来做RAG的工程师看完都能直接拿去改自己项目里的切分逻辑。1. chunk到底是什么从大模型的两个硬约束说起1.1 先给一个不绕弯的定义chunk直译就是“块”“片段”。在AI大模型开发语境下它指的是把一份原始文档PDF、Word、网页、数据库记录等按照某种规则切分成的、长度可控的文本片段。每个chunk是后续向量化、存入向量数据库、被检索召回的最小单位。你可以把它理解成图书馆里的索引卡片。一整本书没法直接塞进检索系统但如果把书拆成一页页、一段段每段贴上一个语义标签向量用户提问时就能快速定位到最相关的那几张卡片。chunk就是那张卡片。这里要区分两个容易混淆的概念chunk是文本层面的切分单位token是模型层面的计算单位。一个chunk通常包含几十到几百个token。切分时我们按字符或token来算长度但检索和生成时模型真正“看到”的是token序列。1.2 为什么不能把整篇文档直接丢给模型很多人会问现在大模型上下文窗口都到128K甚至更长了直接把整本书塞进去不就行了还切什么chunk这里有两个硬约束。第一个是上下文窗口的物理限制。即便窗口再大它也是有上限的。一份几百页的产品手册、一整套法律条文、一个大型项目的全部代码token量轻松突破几十万。窗口装不下这是最直接的原因。第二个约束更关键也是很多人忽略的注意力稀释与检索精度。就算窗口装得下把整篇文档一次性喂给模型模型在生成回答时对中间部分的注意力会明显下降业界俗称“lost in the middle”。同时向量检索的本质是相似度匹配如果每个向量代表的是整篇文档那这个向量的语义就被平均化了用户问一个很具体的细节整篇文档的向量根本匹配不准。切分成chunk之后每个向量代表一个聚焦的语义单元检索精度才能上来。所以chunk的存在本质上是在“上下文完整性”和“检索精度”之间找一个平衡点。切得太粗检索不准切得太细语义破碎。这个平衡怎么找就是后面要讲的核心。1.3 chunk在RAG链路里的位置把RAG的完整链路拆开看chunk处在最前端但它的影响贯穿始终文档加载把PDF、HTML、Markdown等解析成纯文本。文本切分chunking按策略切成一个个chunk。这一步就是本文的主角。向量化embedding每个chunk送进embedding模型变成一个高维向量。存储向量连同chunk原文、元数据一起写入向量数据库。检索用户提问向量化后在库里做相似度搜索召回Top-K个chunk。生成把召回的chunk拼进prompt交给大模型生成答案。看出来了吗第5步召回的质量直接由第2步的切分质量决定。切分阶段埋的雷会在检索阶段集中爆发而且很难在后端补救。这也是为什么我一直跟团队说做RAG项目切分策略值得花30%的时间去打磨。2. 切分策略的取舍从固定长度到语义感知2.1 固定长度切分最简单也最容易翻车最朴素的切分方式就是按固定字符数或token数切。比如每500个字符切一刀相邻chunk之间留50个字符的重叠。def fixed_chunk(text, size500, overlap50): chunks [] start 0 while start len(text): end start size chunks.append(text[start:end]) start end - overlap return chunks这种方式的优点是实现简单、速度快、chunk长度均匀对向量数据库很友好。但缺点同样明显它完全不懂语义一刀下去可能正好把一句话、一个表格、一段代码从中间劈开。我见过最离谱的案例一份API文档里有个参数说明“timeout: 请求超时时间单位毫秒默认30000”固定切分正好切在“默认”后面前一个chunk结尾是“默认”后一个chunk开头是“30000”。用户问“timeout默认值是多少”检索召回了后一个chunk模型看到孤零零的“30000”完全不知道这是什么参数的值只能瞎猜。所以固定长度切分只适合一种场景文本本身结构松散、语义边界不明显比如纯聊天记录、日志流。对于结构化文档它是下策。2.2 递归字符切分目前最通用的默认方案递归字符切分RecursiveCharacterTextSplitter是LangChain等框架的默认策略也是我大多数项目的起点。它的思路是准备一组分隔符按优先级从大到小尝试。先按段落分\n\n如果某段还是太长再按换行分\n还长就按句子分。、.、最后才按字符硬切。from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, separators[\n\n, \n, 。, , , ., , ] ) chunks splitter.split_text(document)这个策略的聪明之处在于它优先在语义边界段落、句子处切尽量不破坏完整语义只有在实在没办法时才硬切。对于中文文档把中文标点加进separators很关键否则它会按空格切而中文句子之间往往没有空格效果会大打折扣。实测下来递归切分对80%的文档类型都能给出可接受的结果是性价比最高的默认选择。但它也有盲区它不理解表格、代码块、标题层级这些结构遇到复杂排版还是会出问题。2.3 语义切分效果好但成本高语义切分Semantic Chunking的思路是先用embedding模型把文本切成句子计算相邻句子的向量相似度相似度骤降的地方就是语义边界在那里切。from langchain_experimental.text_splitter import SemanticChunker from langchain_openai import OpenAIEmbeddings splitter SemanticChunker( OpenAIEmbeddings(), breakpoint_threshold_typepercentile ) chunks splitter.split_text(document)这种方式切出来的chunk语义内聚性最好检索精度通常也最高。但代价是要对每个句子做一次embedding计算文档大时耗时和费用都不低。我的经验是对于核心知识库、FAQ、法律条款这类对精度要求极高的场景值得上语义切分对于海量、更新频繁的普通文档递归切分更划算。2.4 按文档结构切分结构化文档的最优解如果文档本身有清晰结构比如Markdown的标题层级、HTML的标签、代码的函数定义那最聪明的做法是顺着结构切。Markdown可以用MarkdownHeaderTextSplitter按#、##、###切每个chunk自带标题路径作为元数据。这样检索时不仅能匹配内容还能匹配标题精度提升明显。from langchain.text_splitter import MarkdownHeaderTextSplitter headers_to_split_on [ (#, h1), (##, h2), (###, h3), ] splitter MarkdownHeaderTextSplitter(headers_to_split_onheaders_to_split_on) chunks splitter.split_text(markdown_doc)代码文档可以按函数、类来切每个函数一个chunk元数据记录文件路径和函数名。表格则最好整表保留不要切开因为表格的语义高度依赖行列关系。这里有个重要原则结构信息要保留在元数据里而不是丢掉。切分时把标题、页码、章节号、来源文件都存进metadata检索时可以按元数据过滤生成时也能给模型提供上下文线索。3. chunk_size和chunk_overlap到底怎么定3.1 chunk_size不是拍脑袋定的chunk_size是最常被随手填的参数很多人直接抄个500或1000就完事。其实它应该由三个因素共同决定。第一embedding模型的最佳输入长度。不同embedding模型对输入长度有偏好超出训练长度会被截断。比如一些模型最佳区间在256到512 token你硬塞1000 token进去后半段信息实际上没被有效编码。选chunk_size前先查清楚你用的embedding模型的max tokens和推荐长度。第二检索粒度需求。用户的问题越具体chunk应该越小。FAQ场景一个问题一个答案chunk_size可以小到200到300字符。而需要综合多段信息才能回答的综述类问题chunk需要大一些500到1000字符。第三生成时的上下文预算。召回Top-K个chunk要拼进prompt如果每个chunk 1000 token召回5个就是5000 token加上系统提示和对话历史很容易逼近窗口上限。chunk_size乘以K要控制在生成模型上下文预算的合理比例内。我通常的做法是先按embedding模型推荐长度定一个基准值再根据实际检索效果微调。下面这张表是我在不同场景下的经验值可以直接参考场景类型推荐chunk_size字符推荐overlap说明FAQ问答200-30020-30一问一答粒度细产品文档400-60050-80段落完整含小标题法律条款300-50050条款边界清晰忌跨条技术书籍600-1000100-150需要上下文连贯代码文件按函数切0-20以函数/类为界聊天记录300-50050按对话轮次切3.2 overlap的作用被很多人低估chunk_overlap是相邻chunk之间重叠的字符数。它的作用是防止关键信息正好落在切分点上被割裂。比如一句话横跨两个chunk有了overlap两个chunk都能包含这句话的完整或大部分内容检索时至少有一个能召回。overlap设多少合适经验法则是chunk_size的10%到20%。太小起不到保护作用太大则冗余严重向量库里存了大量重复内容检索时容易召回一堆高度相似的chunk浪费上下文预算。有个细节要注意overlap不是越大越好。我见过有人把overlap设成chunk_size的一半结果每个chunk有一半内容和邻居重复检索召回5个chunk实际有效信息可能只有2.5个。这是典型的用力过猛。3.3 一个可复现的调参流程参数不能靠猜得靠测。我一般用这套流程准备一批真实用户问题至少20到30个覆盖典型查询。准备对应的标准答案或标准召回文档。用不同参数组合chunk_size × overlap跑切分和检索。计算召回率标准答案所在的chunk有没有被召回进Top-K。选召回率最高、同时chunk数量最少的组合。def evaluate_chunking(docs, questions, ground_truth, size_list, overlap_list): best None for size in size_list: for overlap in overlap_list: splitter RecursiveCharacterTextSplitter( chunk_sizesize, chunk_overlapoverlap ) chunks splitter.split_text(docs) # 建索引、跑检索、算召回率 recall run_retrieval_eval(chunks, questions, ground_truth) if best is None or recall best[recall]: best {size: size, overlap: overlap, recall: recall} return best这套流程跑下来通常一两个小时就能找到适合你数据的最优参数比拍脑袋靠谱得多。4. 那些年我在chunk上踩过的坑4.1 表格被切碎导致数值错乱这是最惨痛的一次。一个财务知识库里面有大量报表。用递归切分时表格被按行切开了表头在一个chunk数据行在另一个chunk。用户问“2023年Q3营收多少”检索召回了数据行chunk但表头chunk没召回模型看到一堆数字不知道哪列是营收直接编了一个数。修复方案解析阶段识别表格整表作为一个chunk不参与常规切分。如果表格太大必须切也要把表头复制到每个子chunk的开头。这个改动之后财务类问题的准确率从六成多提到了九成以上。4.2 代码块被拦腰截断技术文档里的代码示例如果按字符切很容易把一个完整函数切成两半。用户问“这个函数怎么用”召回的是函数后半段缺少定义和参数说明。修复方案用支持代码感知的切分器按语言语法切保证每个chunk是完整的函数或类。LangChain的RecursiveCharacterTextSplitter.from_language就支持按编程语言切。from langchain.text_splitter import RecursiveCharacterTextSplitter, Language python_splitter RecursiveCharacterTextSplitter.from_language( languageLanguage.PYTHON, chunk_size1000, chunk_overlap100 )4.3 中文标点没配好切分全乱套早期用默认separators里面只有英文标点中文文档切出来全是按空格和换行硬切句子被切得七零八落。后来把中文的。都加进separators效果立刻正常。这个坑很隐蔽因为代码不报错只是效果差新手很难定位。4.4 元数据丢失导致无法溯源一开始切分只保留文本把页码、来源、章节都丢了。结果用户问“这个结论出自哪份文件”系统答不上来。后来强制要求每个chunk必须带source、page、section三个元数据字段检索结果可以展示来源用户信任度明显提升。提示元数据不是可选项。做企业知识库没有来源的答案等于没有答案用户不敢用。4.5 过度切分导致语义碎片化有段时间追求“小chunk高精度”把chunk_size压到100字符结果每个chunk只有一两句话语义严重碎片化。用户问一个需要综合多句才能回答的问题召回了一堆零散片段模型拼不出完整答案。后来把chunk_size调回400到600配合overlap效果反而更好。这印证了那句话切分是在完整性和精度之间找平衡不是越小越好。5. 进阶玩法让chunk更聪明的几个思路5.1 父子chunk检索用小的生成用大的这是目前工业界很流行的一种模式。把文档切成两层小的子chunk比如200字符用于向量检索保证精度每个子chunk关联一个大的父chunk比如1000字符用于生成保证上下文完整。检索时命中子chunk但送给模型的是它对应的父chunk。# 伪代码示意 parent_chunks split(doc, size1000) for parent in parent_chunks: children split(parent, size200) for child in children: child.parent_id parent.id vector_store.add(child) # 检索命中child后取child.parent_id对应的parent文本送模型这个思路很好地解决了“检索要小、生成要大”的矛盾实测在长文档问答上提升明显。5.2 给chunk加上下文前缀有个很实用的技巧在每个chunk前面拼接一段上下文说明比如“本文档是XX产品手册当前章节是XX以下内容属于该章节”。这样即使chunk被单独召回模型也能知道它的语境。Anthropic提出的Contextual Retrieval就是这个思路用大模型为每个chunk生成一句上下文摘要拼在前面检索准确率提升显著。5.3 混合检索配合chunk单纯向量检索对精确匹配如产品型号、错误码不敏感。可以配合关键词检索BM25两路召回后融合。这时chunk的切分要兼顾两种检索方式向量检索喜欢语义完整的chunkBM25喜欢包含关键词的chunk。实践中用递归切分加合理overlap通常能同时满足。5.4 动态chunk_size不是所有文档都适合同一个chunk_size。可以根据文档类型动态调整FAQ用小chunk技术书用大chunk代码按函数切。在入库时根据文档元数据选择对应的切分策略比一刀切效果好得多。6. 不同技术栈下的chunk实现差异6.1 LangChain生态LangChain提供了最丰富的切分器RecursiveCharacterTextSplitter、MarkdownHeaderTextSplitter、TokenTextSplitter、SemanticChunker还有按语言切代码的。做原型首选。注意TokenTextSplitter是按token切的需要配合对应模型的tokenizer。6.2 LlamaIndex生态LlamaIndex的SentenceSplitter和SemanticSplitterNodeParser也很有特色它把chunk叫Node天然支持父子节点和元数据继承做层级检索比较顺手。6.3 自研切分如果框架满足不了需求自研也不难。核心就是定义分隔符优先级、实现递归切分、处理overlap、附加元数据。几百行代码能搞定好处是完全可控能针对自己的文档类型做深度优化。我有个项目因为文档格式极其特殊最后就是自研切分器效果比通用方案好不少。6.4 端侧与本地部署场景在本地部署AI大模型或端侧如litert-lm支持的设备端场景下算力和内存受限chunk策略要更保守chunk_size不宜过大overlap要控制避免检索时占用过多内存。同时embedding模型也要选轻量的整体链路要精简。7. 一套可以直接抄的chunk落地清单把上面的经验浓缩成一份可执行清单新项目照着走就行先分析文档类型结构化Markdown/HTML/代码还是非结构化纯文本/聊天记录。结构化优先按结构切。选切分器默认用递归字符切分中文务必配好中文标点separators。定chunk_size参考embedding模型推荐长度结合场景表初定一个值。定overlap取chunk_size的10%到20%。保留元数据source、page、section、doc_type一个都不能少。特殊内容特殊处理表格整表保留代码按语法切公式单独处理。跑评测用真实问题测召回率迭代参数。上线后持续监控收集bad case反推是切分问题还是检索问题。# 一个相对完整的切分配置示例 from langchain.text_splitter import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, length_functionlen, separators[ \n\n, # 段落 \n, # 换行 。, , , , # 中文句末 ., !, ?, ;, # 英文句末 , ,, # 逗号 , , # 兜底 ], keep_separatorTrue, ) chunks splitter.create_documents( texts[raw_text], metadatas[{source: manual.pdf, page: 12, section: 3.2}] )这份配置我用了很久对中英文混合的技术文档兼容性不错。keep_separatorTrue能保留标点避免句子结尾标点丢失影响语义。8. 关于chunk的几个常见误解最后澄清几个高频误解这些是我在带新人和做技术分享时被问得最多的。误解一chunk越大模型看到的上下文越全效果越好。错。chunk越大向量语义越被平均化检索精度越低而且挤占生成上下文预算。大chunk适合生成不适合检索这就是父子chunk存在的意义。误解二overlap越大越安全。错。overlap过大会造成大量冗余检索时召回一堆相似chunk浪费预算还干扰排序。10%到20%足够。误解三切分是一次性工作定好就不用管了。错。文档在更新用户问题在变化切分策略要跟着迭代。我一般每季度会重新跑一次评测看参数是否还合适。误解四所有文档用同一套切分参数。错。FAQ和技术书的最优参数完全不同。有条件就按文档类型分组配置。误解五chunk只是文本切分跟模型无关。错。chunk_size要匹配embedding模型的最佳输入长度overlap要考虑生成模型的上下文预算它跟整条链路都相关。把chunk这件事想清楚RAG项目的下限就稳了。我个人的体会是与其在检索算法上反复折腾不如先把切分这层做扎实很多所谓的“检索不准”问题根子其实在切分。下次你遇到模型答非所问先别急着换embedding模型回头看看你的chunk切得对不对。
返回列表