数据分块(Chunking)策略
摘要:在检索增强生成(RAG)、向量数据库建设以及企业级知识库大模型落地的工程实践中,数据分块(Chunking)往往被误认为只是简单的字符串切片操作(如
text[i:i+500])。然而在生产环境中,80% 的检索质量瓶颈与上下文断裂问题,根源都在于数据分块策略的不合理。本文将系统性拆解数据分块的底层逻辑与表征机理,深入分析固定大小分块、递归字符分块、语义分块、结构感知分块、父子切片(Small-to-Big)以及大模型驱动分块(Agentic Chunking)等 6 大核心策略;针对 PDF、复杂表格、源代码、多轮对话等非结构化数据给出具体的实践方案,并提供一份生产级 Python 代码框架与调优评估方法论。
前言:被低估的数据分块工程
在大语言模型(LLM)应用开发中,著名的“Garbage in, Garbage out”(垃圾进,垃圾出) 定律依然适用。如果说 Embedding 模型是将自然语言映射到高维几何空间的“翻译官”,那么Chunking(数据分块)就是为这位翻译官提供输入素材的“排版师”。
很多开发者在搭建 RAG 系统时,往往直接使用框架默认的配置(例如 LangChain 的chunk_size=500, chunk_overlap=50)。但在实际业务落地中,经常会遇到以下痛点:
检索出的段落“有头无尾”:核心结论或关键条件恰好被切断在两个 Chunk 之间,导致大模型生成答案时产生“幻觉”或直接回答“无法获取相关信息”。
检索噪音过多,向量被“稀释”:切块过大,包含了几千字跨越多个主题的内容,导致向量表征模糊,在计算相似度时排名偏后。
结构化信息损毁:Markdown 表格、JSON 格式或代码块被强行切割,破坏了语法闭合,使得大模型完全无法解析。
因此,数据分块绝非简单的文本分割,而是一场关于“语义连贯性”与“检索精准度”的博弈与平衡。
一、 为什么数据分块如此关键?(底层逻辑与技术挑战)
1.1 Embedding 模型与向量空间的物理约束
每一个 Embedding 模型(如text-embedding-3-small、bge-m3、bge-large-zh)都有其固有的最大 Token 输入限制(如 512、8192),并且其背后的 Transformer 架构通常采用 Pooling(如 Mean Pooling、CLS Pooling)将整段文本压缩为一个固定维度的向量(如 1024 维、1536 维)。
向量稀释(Vector Dilution):当输入给 Embedding 模型的文本过长(例如 2000 字)且包含了多个不同的主题时,Pooling 机制会将这些主题的语义“平均化”。其结果是:该向量在向量空间中处于一个“平庸的中间位置”,对任何单一特定问题的匹配度都会大幅下降。
信息丢失(Information Loss):文本越长,其中包含的局部细节(如特定的产品型号、日期、金额)在最终向量中的权重就越低。
[原始长文档 (多个主题混合)] ── Embedding ──> [稀释的向量 (语义模糊,匹配度低)] [精细分块 (单一明确主题)] ── Embedding ──> [精准的向量 (语义聚焦,匹配度极高)]1.2 LLM 的上下文窗口与注意力衰减
虽然当前大模型的上下文窗口(Context Window)已经扩展到 128K、1M 甚至更高,但将过长、过多的非相关 Chunk 直接塞给 LLM 仍会带来三大问题:
计算成本(Cost)与延迟(Latency)增加:输入 Token 数量与 LLM 推理成本及首字延迟(TTFT)成正比。
中间丢失现象(Lost in the Middle):大模型的注意力机制对 Prompt 的头部和尾部最敏感,放在上下文中间位置的 Chunk 容易被“无视”。
注意力干扰(Attention Noise):无关或半相关的 Chunk 会分散模型的注意力,增加生成幻觉的风险。
1.3 Chunk Size 与 Overlap 的权衡博弈
在数据分块中,有两个最基础的超参数:Chunk Size(块大小)和Chunk Overlap(块重叠度)。
Chunk 1: [----------------------] Chunk 2: [----------------------] <--------> Chunk OverlapChunk Size 越小:
优势:向量语义更聚焦,检索准确率(Precision)高,噪声低。
劣势:缺少足够的上下文,可能丢失完整的因果关系或完整背景,损害大模型的理解力。
Chunk Size 越大:
优势:包含丰富的上下文信息,大模型读取后更容易生成全面答案。
劣势:向量表征容易被稀释,检索召回率(Recall)下降,增加 Token 消费。
Chunk Overlap 的作用:
在相邻的两个 Chunk 之间保留一部分重叠文本(通常为 Chunk Size 的 10%~20%),目的是减缓边界处的语义断裂,防止某个关键句子恰好被从中间切断。
| 分块维度 | 粒度过细 (如 100~200 Tokens) | 粒度适中 (如 300~800 Tokens) | 粒度过粗 (如 1500+ Tokens) |
| 向量精确度 | 极高 | 高 | 低(语义稀释) |
| 上下文完整性 | 差(容易断章取义) | 良好 | 极佳 |
| 检索噪声率 | 低 | 较低 | 高(夹带无关文本) |
| 适合场景 | 事实核查、FAQ、精准属性提取 | 通用问答、文档分析 | 摘要生成、长文本总结 |
二、 主流数据分块(Chunking)策略深度拆解
为了应对不同的文档类型与业务场景,业界演进出了多种分块策略。下面由浅入深进行拆解。
┌──────────────────────────────────────────┐ │ 数据分块策略演进路线 │ └────────────────────┬─────────────────────┘ │ ┌──────────────────────────────────┼──────────────────────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 规则与字符 │ │ 语义驱动 │ │ 结构与感知 │ │ Fixed-size │ │ Semantic │ │ Hierarchy │ │ Recursive │ │ Parent-Child │ │ AST / Layout │ └──────────────┘ └──────────────┘ └──────────────┘2.1 固定大小分块(Fixed-size / Character-based Chunking)
原理机制
按照固定的字符数(Character Count)或 Token 数(如tiktoken统计)强行切分文本,每次移动固定步长。
缺陷分析
这种方式完全忽略了自然语言的语法结构。它可能会将一个单词、一个完整的句子、甚至一个数字从中间砍断(例如将100000切成100和000),在现代生产环境中极不推荐单独使用。
代码实现(Python)
def fixed_size_chunking(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]: """固定字符大小分块示例""" chunks = [] start = 0 text_len = len(text) while start < text_len: end = min(start + chunk_size, text_len) chunk = text[start:end] chunks.append(chunk) if end == text_len: break start += (chunk_size - overlap) return chunks # 验证 sample_text = "数据分块是RAG系统的核心步骤。合理的切分能提升检索精准度。固定切分可能会切断句子。" print(fixed_size_chunking(sample_text, chunk_size=20, overlap=5))2.2 递归字符分块(Recursive Character Chunking)
原理机制
这是目前 LangChain、LlamaIndex 等框架中最常用的默认通用切片算法(如RecursiveCharacterTextSplitter)。
它引入了一组具备优先级梯度的层级分隔符列表:
["\n\n", "\n", "。", "!", "?", ";", " ", ""]
算法运行逻辑:
优先尝试按照段落分隔符(
\n\n)将文本切分成块。如果切分后的某一部分依然超过指定的
chunk_size,则递归地降级到下一个分隔符(如换行符\n)继续切分。依次类推,直到切分出的每一个 Chunk 长度都符合
chunk_size要求。
优势
能够最大程度地保持段落和句子的自然结构边界,避免将同一句话切割开。
生产级代码实现
import re class RecursiveChunker: def __init__(self, chunk_size: int = 500, overlap: int = 50, separators: list[str] = None): self.chunk_size = chunk_size self.overlap = overlap self.separators = separators or ["\n\n", "\n", "。", "!", "?", ";", " ", ""] def split_text(self, text: str) -> list[str]: final_chunks = [] # 寻找当前适用的最高优先级分隔符 separator = self.separators[-1] for s in self.separators: if s == "": separator = "" break if s in text: separator = s break # 根据选定的分隔符拆分 if separator != "": splits = text.split(separator) else: splits = list(text) # 退化为单字符拆分 # 重新组合碎片,确保不超过 chunk_size good_splits = [] for s in splits: if len(s) < self.chunk_size: good_splits.append(s) else: # 如果某个碎片仍然超长,递归降级调用 if good_splits: merged = self._merge_splits(good_splits, separator) final_chunks.extend(merged) good_splits = [] # 寻找更细粒度的分隔符继续切 sub_chunker = RecursiveChunker( self.chunk_size, self.overlap, self.separators[self.separators.index(separator)+1:] ) final_chunks.extend(sub_chunker.split_text(s)) if good_splits: merged = self._merge_splits(good_splits, separator) final_chunks.extend(merged) return final_chunks def _merge_splits(self, splits: list[str], separator: str) -> list[str]: """合并零碎文本,保持Overlap""" chunks = [] current_chunk = [] current_len = 0 for s in splits: s_len = len(s) + len(separator) if current_len + s_len > self.chunk_size: if current_chunk: doc = separator.join(current_chunk) chunks.append(doc) # 保留重叠部分 while current_len > self.overlap and current_chunk: removed = current_chunk.pop(0) current_len -= (len(removed) + len(separator)) current_chunk.append(s) current_len += s_len if current_chunk: chunks.append(separator.join(current_chunk)) return chunks # 使用测试 chunker = RecursiveChunker(chunk_size=100, overlap=20) text = "在大模型RAG架构中,数据分块是决定检索质量的关键阶段。\n\n优秀的切分策略能够完美保留文本的上下文语义。\n如果分块不合理,会导致严重的检索噪音以及模型回答幻觉。" print(f"切分块数: {len(chunker.split_text(text))}")2.3 基于语义的分块(Semantic Chunking)
原理机制
无论是固定大小还是递归切片,本质上都是基于“字符规则”而非“文本含义”。语义分块(Semantic Chunking)则是利用 Embedding 模型主动感知语义变迁的动态切片技术。
核心计算步骤:
句子拆分:将原始文本按照句号、问号等分割为独立的句子列表 $S = [s_1, s_2, \dots, s_n]$。
组合窗口(Buffer Window):为了捕获局部语义,将相邻的几句话(例如前 1 句、当前句、后 1 句)拼接成一个临时组合块。
计算向量:对每个组合块调用 Embedding 模型生成语义向量 $V_i$。
计算相邻语义距离:计算相邻向量之间的余弦距离或欧氏距离:
$$\text{Distance}(i) = 1 - \text{Cosine\_Similarity}(V_i, V_{i+1})$$
阈值切分(Thresholding):寻找语义距离的“突变峰值”(如设置百分位数 95% 或固定阈值)。当距离超过阈值时,说明文本主题在此处发生了转换,触发切分。
语义距离 (Distance) ▲ │ ▲ (语义突变点 -> 触发切分 Chunk 边界) │ ╱ ╲ │ ───╱───┼───阈值 (Threshold)──────────────── │ ╱ ╲ ╱ ╲ │ ╱ ╲_╱ ╲ └──────────────────────────► 句子序号 (Sentence Index)完整 Python 实现代码
import numpy as np from sklearn.metrics.pairwise import cosine_similarity class SemanticChunker: def __init__(self, embedding_fn, breakpoint_percentile_threshold: float = 95.0): self.embedding_fn = embedding_fn # 传入可调用Embedding函数 self.threshold = breakpoint_percentile_threshold def _split_into_sentences(self, text: str) -> list[str]: # 简单句号切分正则,生产环境推荐使用 nltk 或 spacy sentences = re.split(r'(?<=[。!?\n])', text) return [s.strip() for s in sentences if s.strip()] def split_text(self, text: str) -> list[str]: sentences = self._split_into_sentences(text) if len(sentences) <= 1: return sentences # 1. 构造滑窗文本 (Sentence Window) combined_sentences = [] for i in range(len(sentences)): combined = "" # 取前一句、当前句、后一句组合 if i > 0: combined += sentences[i-1] + " " combined += sentences[i] if i < len(sentences) - 1: combined += " " + sentences[i+1] combined_sentences.append(combined) # 2. 批量计算向量 embeddings = self.embedding_fn(combined_sentences) # 3. 计算相邻句子间的语义距离 distances = [] for i in range(len(embeddings) - 1): similarity = cosine_similarity([embeddings[i]], [embeddings[i+1]])[0][0] distance = 1 - similarity distances.append(distance) # 4. 根据百分位数计算动态阈值 breakpoint_threshold = np.percentile(distances, self.threshold) # 5. 根据突变点分组聚类 chunks = [] current_chunk = [sentences[0]] for i, dist in enumerate(distances): if dist > breakpoint_threshold: # 语义发生显著突变,划定新块 chunks.append("".join(current_chunk)) current_chunk = [sentences[i + 1]] else: current_chunk.append(sentences[i + 1]) if current_chunk: chunks.append("".join(current_chunk)) return chunks2.4 高级结构感知分块(Document Structure-Aware Chunking)
对于拥有天然排版结构的文档(Markdown、HTML、LaTeX、API 文档),最优雅的切分方式是遵循文档本身的抽象语法树(AST)。
Markdown 结构化切分
利用标题层级(# H1,## H2,### H3)进行逻辑分块,并在切出来的每一个子 Chunk 中保留父级标题的路径信息(Breadcrumb 元数据)。
没有 Breadcrumb 的普通 Chunk:
“本节要求的初始资金为 50 万元。”(不知道属于哪家公司、哪个项目)
带结构元数据的 Chunk:
[路径: 华东项目组 -> 财务预算 -> 初始资金]
本节要求的初始资金为 50 万元。
代码实战(Markdown 层级切分与元数据注入)
import re class MarkdownStructureChunker: def __init__(self, max_chunk_size: int = 800): self.max_chunk_size = max_chunk_size def parse(self, markdown_text: str) -> list[dict]: lines = markdown_text.split('\n') chunks = [] headers_stack = [] # 记录标题栈 [H1, H2, H3] current_content = [] current_len = 0 for line in lines: header_match = re.match(r'^(#{1 Bast|#{1,6})\s+(.*)', line) if header_match: # 保存之前累积的文本 if current_content: breadcrumb = " -> ".join([h[1] for h in headers_stack]) chunks.append({ "breadcrumb": breadcrumb, "content": "\n".join(current_content) }) current_content = [] current_len = 0 # 更新标题栈 level = len(header_match.group(1)) title = header_match.group(2).strip() while headers_stack and headers_stack[-1][0] >= level: headers_stack.pop() headers_stack.append((level, title)) else: current_content.append(line) current_len += len(line) if current_content: breadcrumb = " -> ".join([h[1] for h in headers_stack]) chunks.append({ "breadcrumb": breadcrumb, "content": "\n".join(current_content) }) return chunks # 测试 md_doc = """ # 腾讯云计算服务条款 ## 1. 账号注册与安全 用户注册时需提供真实身份信息。 ## 2. 计费与退款政策 ### 2.1 包年包月服务 包年包月服务在购买后 7 天内支持无理由退款。 """ chunker = MarkdownStructureChunker() print(chunker.parse(md_doc))2.5 父子文档与小到大切片(Parent-Child / Small-to-Big Chunking)
在检索阶段,我们常陷入两难境地:
用小 Chunk(如 100 Tokens)算向量:表达明确、精准命中;但送给 LLM 时上下文严重缺失。
用大 Chunk(如 2000 Tokens)算向量:上下文丰富,但向量语义稀释,匹配度低。
解法:父子切片架构(Small-to-Big Chunking)
┌──────────────────────────────────────────────────────────┐ │ 父文档 (Parent Chunk): 包含 1500 Tokens 的完整段落/章节 │ │ ┌────────────────────┬────────────────────┐ │ │ │ 子 Chunk 1 (200T) │ 子 Chunk 2 (200T) │ ... │ │ └─────────┬──────────┴─────────┬──────────┘ │ └────────────┼────────────────────┼────────────────────────┘ │ │ 存入向量库 存入向量库 │ ▼ [向量检索匹配命中] │ └──────► 最终映射并调取【父文档】发送给 LLM向量库中存储:粒度极小的子 Chunk 向量(Child Chunks)。
内存/文档库中存储:大范围的父 Chunk 或完整段落(Parent Chunks)。
工作流:用户 Query 在向量库中精准检索并命中子 Chunk 后,系统依据子 Chunk 绑定的
parent_id自动向上检索提取出对应的父 Chunk,将整段父文本拼接入 Prompt。
2.6 大模型驱动与命题分块(Agentic & Propositional Chunking)
来自陈丹琦团队与 Stanford 提出的Dense X / Propositional Chunking代表了当前数据分块的最前沿探索。
核心思想
利用小参数量 LLM 将自然语言段落解构分解为一系列独立的“命题(Propositions)”:
每个命题表达一个独立的原子事实。
命题具备完全自包含性(Self-contained):自动补全指代消解(将“他/该公司”替换为具体的人名/公司名)。
[原始文本]: “马斯克生于南非,后来移居加拿大。他是特斯拉的CEO。” [解构后的命题 Chunk 列表]: Chunk 1: 马斯克出生于南非。 Chunk 2: 马斯克从南非移居到了加拿大。 Chunk 3: 马斯克是特斯拉公司的 CEO。适用场景
对事实精准度要求极高的场景(如医疗诊断手册、法律条款判例、金融审计数据)。
三、 复杂非结构化数据分块实战
在实际的企业知识库建设中,最头疼的往往不是纯文本,而是 PDF 排版、复杂表格、代码仓库和多轮对话。
3.1 PDF 文档分块实战
PDF 是一种以版面印刷坐标为核心的格式,天然没有段落和语义概念。
常见坑点
页眉/页脚/页码断打:每一页顶部的“某某公司机密文件”被切进文本,破坏语义。
双栏/多栏排版混读:按行读取会导致左栏第一行与右栏第一行拼接在一起。
解决方案
版面分析(Layout Analysis):借助
pdfplumber、Unstructured或 OCR 模型(如 LayoutLM、PaddleOCR)识别页眉页脚并主动过滤。跨页拼接逻辑:检查页面末尾的最后一个字符,如果是未闭合的标点或连字符(
-),不要强行切块,将其与下一页的开篇合并。
3.2 复杂表格(Table)分块策略
如果直接将表格按字符切割,会导致表头(Header)与数据行(Row)分离,产生完全无意义的“文本碎片”。
[错误分块案例]: Chunk 1: | 姓名 | 年龄 | 部门 | Chunk 2: | 张三 | 28 | 技术部 | (失去了表头,在向量空间中成为纯噪音)生产推荐的三种表格分块方案:
Markdown 表格渲染法:将表格转换为带分隔符的 Markdown 表格文本。要求每一个切切出来的 Chunk 内部必须重复打入相同的表头。
HTML 表格模式:保持
<table><tr><td>结构,大模型对 HTML 标签的结构感知能力极强。行转文本序列化(Row-by-Row Serialization):将表格的每一行转化为独立的KV对描述句(推荐用于精准问答):
[行序列化示例]: "数据记录: 姓名=张三; 年龄=28; 部门=技术部; 绩效=A" "数据记录: 姓名=李四; 年龄=35; 部门=市场部; 绩效=B"3.3 源代码(Code Repository)分块策略
源代码切片切忌使用字符切片,必须使用基于抽象语法树(AST)的语法感知器(如Tree-sitter)。
┌───────────────────────────────┐ │ 源代码文件 (.py / .java) │ └───────────────┬───────────────┘ │ Tree-sitter AST 解析 ▼ ┌─────────────────────────────────┐ │ AST 语法节点 (AST Nodes) │ └────────┬───────────────┬────────┘ │ │ ▼ ▼ ┌────────────────┐ ┌──────────────┐ │ Class 节点 │ │ Function 节点│ └────────────────┘ └──────────────┘粒度规范:以完整的类(Class)或函数/方法(Function/Method)为最小不可分割单位。
签名与文档保留:每个函数 Chunk 必须包含顶部的类继承声明、函数签名与 Docstring 注释。
四、 生产级 Chunking 系统设计与代码实现
为了在生产环境中支持高效、高可扩展的数据分块,我们需要设计一个包含解析、切割、元数据绑定、哈希去重的标准组件。
生产级 Python 数据分块框架设计
import hashlib import uuid from typing import List, Dict, Any, Optional from dataclasses import dataclass, field @dataclass class ChunkRecord: chunk_id: str doc_id: str content: str metadata: Dict[str, Any] = field(default_factory=dict) char_count: int = 0 token_count_est: int = 0 content_hash: str = "" def __post_init__(self): self.char_count = len(self.content) # 粗略估计 Token 数 (中文约 1.5 chars/token, 英文约 4 chars/token) self.token_count_est = int(self.char_count * 0.7) if not self.content_hash: self.content_hash = hashlib.md5(self.content.encode('utf-8')).hexdigest() class ProductionChunkingPipeline: def __init__(self, chunker_strategy, min_chunk_len: int = 20): self.chunker_strategy = chunker_strategy self.min_chunk_len = min_chunk_len def process_document( self, doc_id: str, raw_text: str, global_metadata: Optional[Dict[str, Any]] = None ) -> List[ChunkRecord]: """ 全流程处理:切分、过滤短文本、元数据增强、生成ID """ if not raw_text.strip(): return [] global_metadata = global_metadata or {} # 1. 执行具体的切分算法 raw_chunks = self.chunker_strategy.split_text(raw_text) processed_chunks: List[ChunkRecord] = [] for idx, chunk_text in enumerate(raw_chunks): cleaned_text = chunk_text.strip() # 2. 过滤无意义极短碎片 if len(cleaned_text) < self.min_chunk_len: continue # 3. 构造子 Metadata chunk_meta = global_metadata.copy() chunk_meta.update({ "chunk_index": idx, "total_chunks": len(raw_chunks), "is_first_chunk": idx == 0, "is_last_chunk": idx == len(raw_chunks) - 1 }) # 4. 生成唯一并可追溯的 Chunk ID chunk_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"{doc_id}_chunk_{idx}")) record = ChunkRecord( chunk_id=chunk_uuid, doc_id=doc_id, content=cleaned_text, metadata=chunk_meta ) processed_chunks.append(record) return processed_chunks # ==================== 运行验证 ==================== if __name__ == "__main__": # 使用上文定义的 RecursiveChunker 作为底层策略 base_chunker = RecursiveChunker(chunk_size=150, overlap=30) pipeline = ProductionChunkingPipeline(chunker_strategy=base_chunker) doc_id = "DOC_20260731_001" raw_doc = """ 【系统升级公告】 尊敬的用户: 为了提供更稳定的服务,我们将于 2026 年 8 月 1 日 00:00 - 04:00 进行系统例行维护。 维护期间,API 调用可能会出现短暂中断。请妥善安排您的业务计划。 如有紧急问题,请联系技术支持热线:400-800-1234。 """ metadata = {"category": "SystemNotice", "author": "DevOps Team"} results = pipeline.process_document(doc_id=doc_id, raw_text=raw_doc, global_metadata=metadata) print(f"成功生成 Chunk 数量: {len(results)}\n") for chunk in results: print(f"ID: {chunk.chunk_id}") print(f"Metadata: {chunk.metadata}") print(f"Content: {chunk.content}") print(f"Hash: {chunk.content_hash}") print("-" * 50)五、 Chunking 效果评估与调优方法论
没有量化指标的调优等同于瞎摸象。如何确定当前的分块策略是最佳的?
5.1 核心量化评估指标
我们可以通过RAGAS或TruLens等离线评估工具,对不同 Chunk 策略的输出进行打分:
检索命中率 (Hit Rate @ K):测试集中的标准答案是否出现在检索出的前 K 个 Chunk 中。
平均倒数排名 (MRR @ K):相关文档出现在检索结果列表中的位置排名倒数均值。
上下文召回率 (Context Recall):检索出来的 Chunk 包含回答该问题所需全部信息的比例。
上下文精确度 (Context Precision):检索出来的 Chunk 中,有用信息与噪声文本的比例(噪声越少得分越高)。
5.2 分块策略选型决策矩阵
[文档类型与场景需求] │ ┌──────────────────────────┼──────────────────────────┐ ▼ ▼ ▼ 【强结构化文档】 【通用长文本/知识库】 【超高精度问答】 (Markdown, HTML, Code) (图书, 新闻, 行业报告) (医疗, 法律, 规章条款) │ │ │ ▼ ▼ ▼ - 结构感知切分(AST) - 递归字符切分(Recursive) - 语义切分(Semantic) - 保留 Breadcrumbs - 父子切分(Small-to-Big) - 命题分解(Propositional)| 业务场景 | 建议 Chunk Size | 建议 Overlap | 推荐策略 |
| 客服 FAQ / 短问答 | 100 ~ 250 Tokens | 10% ~ 15% | 递归字符切分 / 语义切分 |
| 企业规章制度 / PDF 知识库 | 400 ~ 800 Tokens | 15% ~ 20% | 父子文档切片 / 结构感知切分 |
| 技术 API 文档 / 源码 | 函数 / 类级(完整性优先) | N/A | AST 节点结构化切分 |
| 财务报表 / 统计表格 | 按表格单元/行重新组装 | N/A | Markdown 表格 / 行序列化 |
| 学术论文 / 深度研报 | 800 ~ 1200 Tokens | 20% | 递归切分 + 混合检索 (Dense + BM25) |
六、 总结与未来演进趋势
数据分块(Chunking)是大模型 RAG 系统设计中成本最低、效果提升最显著的优化点之一。
回顾数据分块的技术演进逻辑:
第一代(规则驱动):固定大小与递归字符切分,以文本长度和符号为界限。
第二代(语义与结构驱动):基于 Embedding 距离突变感知语义边界,基于 AST/Markdown 解析文档骨架。
第三代(动态与上下文重构):父子映射(Small-to-Big)、大模型命题解构(Propositional Chunking)以及结合 Late Chunking 技术的上下文增强。
未来趋势:Late Chunking(长上下文向量化后再切片)
传统的步骤是“先切片 ➔ 再计算 Embedding”,这会导致每个切片缺失整体上下文。
目前以 Jina AI 为代表的Late Chunking提出了一种新范式:先将整篇长文档输入能够支持长上下文的模型(如 8K/32K Transformer),在 Transformer 的最后一个隐层(Hidden State)获取包含了全局相互注意力的词向量,然后再在此隐层矩阵上进行分块 Pooling。这种方式既保留了全局上下文信息,又获得了小 Chunk 的精准向量定位。
对于工业级开发者而言,始终保持“数据清洗 ➔ 版面识别 ➔ 结构化分块 ➔ 评估验证”的严谨工程闭环,才能让大模型在复杂的业务场景中展现出极致的检索与生成性能。
