Skip to content

Commit 5f2cfb3

Browse files
committed
docs(rag): 修复文档处理篇错误并优化表达
修复内容: - P0: 修正 MDPI 基线准确率为 50%(原文误引为 13%) - P1: 修正 MongoDB 数据归因(非 Databricks 实测) - P1: 补充 Vecta 语义切分阈值的调参背景 - P1: 补充 NVIDIA Page-Level 优势仅 0.3-4.5 个百分点 - P1: 补充固定切分与递归切分差距仅 2% 的数据 - P2: 修复 Word 标题层级代码语法错误 - P2: 补充 PDF 解析工具对比数据(Docling/LlamaParse/Unstructured) 表达优化: - 弱化"XX 研究/基准显示"话术,改为更自然的经验分享风格 - 添加术语约定说明,统一 Chunking/切分 等表述 - 补充流程图脚注说明分层校验策略 - 补充 InMemoryByteStore 生产环境提醒
1 parent 54f2f73 commit 5f2cfb3

2 files changed

Lines changed: 147 additions & 88 deletions

File tree

docs/ai/rag/rag-document-processing.md

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ category: AI 应用开发
55
head:
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

228236
1. **使用 Layout-Aware Parser**。这类解析器会识别文本的物理位置(x、y 坐标)、字体大小、段落间距,从而推断出真实的阅读顺序。LlamaParse、Docling、Marker-PDF 都支持这个能力。
229-
230237
2. **多版本解析对比**。同一个 PDF 用两种解析器跑一遍,检查输出的一致性。如果两份输出差异很大,说明解析结果不可靠,应该降级处理或标记为需要人工审核。
231-
232238
3. **检测表格跨栏**。财务报表里的合并单元格是解析噩梦。跨列的表头、跨行的数值项,如果只按文本流解析,结构会完全乱掉。这类文档建议用专门的表格提取工具(如 Docling 的 TableFormer 模块)处理。
233239

234240
### Word 标题层级
@@ -247,23 +253,34 @@ Word 文档的结构通常靠标题样式体现(Heading 1、Heading 2、正文
247253
# 读取 Word 文档并保留标题层级
248254
from 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

Comments
 (0)