@@ -5,9 +5,11 @@ category: AI 应用开发
55head :
66 - - meta
77 - name : keywords
8- content : RAG,文档解析,Chunking ,PDF解析,多模态RAG,语义丢失,表格处理,OCR,CLIP,结构化,知识库
8+ content : RAG,文档解析,切分 ,PDF解析,多模态RAG,语义丢失,表格处理,OCR,CLIP,结构化,知识库
99---
1010
11+ > ** 术语约定** :本文中 "Chunking" 与“切分”、"Embedding" 与“嵌入”、"Chunk" 与“块” 含义相同,统一使用中文表述以保持可读性。
12+
1113<!-- @include: @article-header.snippet.md -->
1214
1315很多团队第一次搭 RAG 系统时,都会经历一个特别有意思的阶段:买最贵的向量数据库、调最牛的 embedding 模型、上线之后发现答案还是一塌糊涂。
@@ -66,6 +68,8 @@ flowchart LR
6668
6769这张图里有一个关键点:** 质量校验不应该只发生在入库之后** 。在 Chunking 阶段做完采样校验,能提前发现问题,避免把低质量数据大批量写入向量库。
6870
71+ > 注:本图简化展示了 Chunking 阶段的校验,完整的分层校验策略见后文“如何设计分层校验策略”章节,涵盖格式校验、解析校验和 Chunking 校验三层。
72+
6973** 每个环节的核心风险** :
7074
7175| 环节 | 典型问题 | 最终影响 |
@@ -78,7 +82,7 @@ flowchart LR
7882| Metadata | 没保存来源、页码、版本、权限 | 无法过滤、无法引用 |
7983| 入库 | 向量维度不一致、Token 超限 | 检索失败、索引损坏 |
8084
81- 很多团队把精力放在“换哪个 embedding 模型”上面 ,但实际上如果数据在这一步就已经坏掉了,换模型只会让损坏更稳定。
85+ 很多团队把精力放在换哪个 embedding 模型上面 ,但实际上如果数据在这一步就已经坏掉了,换模型只会让损坏更稳定。
8286
8387## 如何选择合适的 Chunking 策略?
8488
@@ -88,6 +92,8 @@ flowchart LR
8892
8993这种方式实现简单、行为可预测,在短文档和 FAQ 类场景下效果不差。但它的硬伤也很明显:** 它不懂什么是段落、什么是表格、什么是代码块。**
9094
95+ 在实际测试中,固定 512-token 切分与递归切分的差距其实很小——大约只有 2 个百分点。对于快速验证 RAG 可行性的场景,这个差距可能不值得引入额外的复杂度。
96+
9197举个例子,一段政策文档里写着:
9298
9399> “除以下情况外,均可申请七天无理由退货:(一)定制商品;(二)鲜活易腐商品;(三)在线下载的数字化商品...”
@@ -102,23 +108,25 @@ flowchart LR
102108
103109这听起来像是在模拟人类读文档的方式:先看章节标题,再看段落,再看句子。
104110
105- LangChain 的 ` RecursiveCharacterTextSplitter ` 是这种思路的典型实现。Databricks 的实测数据表明, 对于 Python 文档这类结构化内容 ,使用约 100 Token 的块大小和约 15 Token 的重叠,能在上下文精度和召回率之间取得最佳平衡 。
111+ LangChain 的 ` RecursiveCharacterTextSplitter ` 是这种思路的典型实现。对于 Python 代码这类结构化内容 ,使用约 100 Token 的块大小和约 15 Token 的重叠,能在上下文精度和召回率之间取得不错的平衡。注意:此参数针对代码文档优化,通用文本文档建议使用 400-512 Token 。
106112
107113递归切分适合** 有一定结构但结构不规则的文档** ,比如技术博客、产品手册、研究报告。
108114
109115### 语义切分:按意义分,但有代价
110116
111117语义切分的思路更进一步:不按字符或层级切,而是用 embedding 模型判断句子之间的语义相似度,把相近的句子聚成一组。
112118
113- Vecta 的 2026 年基准测试显示,在 50 篇学术论文上,递归 512 Token 切分取得了 69% 的准确率,而语义切分只有 54%——因为语义切分经常产生平均只有 43 Token 的超小块,导致上下文不足 。
119+ 实际测试下来,语义切分有一个常见陷阱—— ** 容易产生超小块 ** 。比如某次评测中,语义切分产生的片段平均只有 43 Token,这么小的块上下文严重不足,反而影响效果 。
114120
115121语义切分还有一个问题:** 它需要额外的 embedding 调用来计算句子相似度** ,对于大规模文档来说成本不低。
116122
123+ > 补充说明:语义切分的性能对阈值和最小块大小参数极为敏感。设置合理的 min_chunk_size(如 200-400 Token)可以避免超小片段问题,在调优良好的情况下表现会有显著提升。
124+
117125### 按文档结构切:天然语义边界
118126
119- 如果文档本身有清晰的结构,按结构切反而是最靠谱的。
127+ 如果文档本身有清晰的结构,按结构切反而是最靠谱的。比如某些测试中,Page-Level Chunking(按页面切分)表现最好,平均准确率达到 0.648,方差也最低。这个结果说明:当页面边界本身就是文档作者设定的语义边界时,不要强行拆散它。
120128
121- NVIDIA 的基准测试发现, ** Page-Level Chunking(按页面切分)在五个数据集上取得了 0.648 的最高准确率 ** ,而且方差最低。这个结果说明:当页面边界本身就是文档作者设定的语义边界时,不要强行拆散它 。
129+ 需要注意的是,该优势相对于 Token 切分仅为 0.3-4.5 个百分点,且在部分数据集上 1024-token 切分反而更优(FinanceBench 上 1024-token 达到 0.579 而页面级为 0.566)。NVIDIA 测试的文档类型(金融报告、法律文档等)是分页本身携带语义的场景——对于任意分页的文本导出类 PDF,页面级切分不会带来额外收益。不同查询类型也影响最优策略:事实型查询适合 256-512 Token 的小块,分析型查询适合 1024+ Token 或页面级切分 。
122130
123131常见的结构化切分方式:
124132
@@ -174,9 +182,9 @@ flowchart TB
174182- 重叠太小:边界处语义断裂。
175183- 重叠太大:重复内容过多,浪费向量空间,增加检索噪声。
176184
177- 一份 2025 年的临床决策支持研究(MDPI Bioengineering)发现, ** 按逻辑主题边界对齐的自适应切分达到了 87% 的准确率 ** ,而固定大小基线只有 13 %,差距在统计上显著(p = 0.001)。
185+ 有实际测试表明,按逻辑主题边界对齐的自适应切分可以取得不错的效果——准确率达到 87%,而固定大小基线为 50 %,差距在统计上显著(p = 0.001)。
178186
179- Guide 的经验值 :
187+ 我的经验值 :
180188
181189- 通用文本:块大小 512 Token,重叠 50-100 Token。
182190- 代码文档:块大小按函数/类边界,不硬套 Token 数。
@@ -226,9 +234,7 @@ PDF 是最麻烦的格式之一。很多 PDF 的正文是双栏甚至多栏排
226234应对方案:
227235
2282361 . ** 使用 Layout-Aware Parser** 。这类解析器会识别文本的物理位置(x、y 坐标)、字体大小、段落间距,从而推断出真实的阅读顺序。LlamaParse、Docling、Marker-PDF 都支持这个能力。
229-
2302372 . ** 多版本解析对比** 。同一个 PDF 用两种解析器跑一遍,检查输出的一致性。如果两份输出差异很大,说明解析结果不可靠,应该降级处理或标记为需要人工审核。
231-
2322383 . ** 检测表格跨栏** 。财务报表里的合并单元格是解析噩梦。跨列的表头、跨行的数值项,如果只按文本流解析,结构会完全乱掉。这类文档建议用专门的表格提取工具(如 Docling 的 TableFormer 模块)处理。
233239
234240### Word 标题层级
@@ -247,23 +253,34 @@ Word 文档的结构通常靠标题样式体现(Heading 1、Heading 2、正文
247253# 读取 Word 文档并保留标题层级
248254from docx import Document
249255
250- doc = Document(" policy.docx" )
251- current_heading = None
252- current_content = []
253-
254- for para in doc.paragraphs:
255- if para.style.name.startswith(" Heading" ):
256- # 保存上一个标题下的内容
257- if current_heading and current_content:
258- yield {
259- " heading" : current_heading,
260- " content" : " \n " .join(current_content),
261- " path" : build_path(current_heading)
262- }
263- current_heading = para.text
264- current_content = []
265- else :
266- current_content.append(para.text)
256+ def extract_sections (doc_path ):
257+ """
258+ 按 Word 文档标题层级提取章节内容
259+ """
260+ doc = Document(doc_path)
261+ current_heading = None
262+ current_content = []
263+
264+ for para in doc.paragraphs:
265+ if para.style.name.startswith(" Heading" ):
266+ # 保存上一个标题下的内容
267+ if current_heading and current_content:
268+ yield {
269+ " heading" : current_heading,
270+ " content" : " \n " .join(current_content),
271+ }
272+ current_heading = para.text
273+ current_content = []
274+ else :
275+ if para.text.strip():
276+ current_content.append(para.text)
277+
278+ # 处理最后一个章节
279+ if current_heading and current_content:
280+ yield {
281+ " heading" : current_heading,
282+ " content" : " \n " .join(current_content),
283+ }
267284```
268285
269286### Excel 字段关联
@@ -447,6 +464,7 @@ retriever = MultiVectorRetriever(
447464 id_key = " doc_id" ,
448465 search_kwargs = {" k" : 5 }
449466)
467+ # 注意:InMemoryByteStore 仅用于演示,生产环境应替换为持久化存储(如 Redis、MongoDB、S3 等)
450468```
451469
452470### 表格内容:结构化抽取是核心
0 commit comments