Spaces:
Runtime error
Runtime error
File size: 8,945 Bytes
4a8fc49 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 | # RAG PDF 提取与切分优化总结
这次优化的目标是提升当前 RAG 系统对金融 PDF,尤其是包含大量数学公式、章节标题和图表内容的 PDF 的解析质量。原始实现能完成基础向量检索,但 PDF 提取、公式保留、chunk 切分和 metadata 管理都比较粗糙,导致检索结果不够稳定。
## 一、整体背景
项目使用 `LlamaIndex + Chroma + HuggingFaceEmbedding` 构建本地知识库,原始 PDF 文档是一本期权/波动率相关书籍。最开始的流程大致是:
```text
pypdf 提取每页文本
-> SentenceSplitter 固定长度切分
-> HuggingFace embedding
-> Chroma 向量库
-> QueryKnowledgeTool 检索返回片段
```
这个流程对普通纯文本还可以,但面对金融教材类 PDF 会遇到很多问题:公式被拆散、章节边界丢失、页眉页脚干扰、图表文字混入正文、数学符号顺序错乱等。
## 二、遇到的主要问题
### 1. PDF 基础文本提取能力弱
最初只使用:
```python
page.extract_text()
```
问题是:
- 页眉、页码、版权信息会混进正文。
- 断行、断词严重,比如单词被 PDF 换行拆开。
- 多栏、图表、公式附近的文本顺序容易错乱。
- 数学公式经常被压成一行,或者符号顺序不对。
解决方法:
- 增加 `pypdf` 的 `layout` 模式作为候选。
- 增加坐标级提取,利用 `visitor_text` 获取文字的 `x/y` 坐标,按视觉行重组。
- 增加文本清洗逻辑:
- 去除空行、页码、重复页眉页脚。
- 修复连字符断词。
- 处理常见 ligature,例如 `fi`、`fl`。
- 保留公式行的换行,不把公式硬合并成普通段落。
### 2. 数学公式提取不理想
金融教材中大量公式包含:
- 希腊字母,如 `𝜎`、`𝜇`、`𝜌`
- 上标、下标
- 分式结构
- 积分、求和、根号
- 公式编号,如 `(21.23)`
普通 PDF 文本提取很难还原这些结构。例如:
```text
d𝜎 = a𝜎 dt + b𝜎 dZ
```
可能会被提取成符号粘连、顺序错乱,或者和前后正文混在一起。
解决方法:
- 先做 `pypdf` 数学感知优化:
- 识别公式行。
- 对短公式行、括号行、根号行保留换行。
- 尝试根据字号和垂直偏移标记上标/下标。
后来发现 `pypdf` 仍然不够,所以进一步接入 `PyMuPDF`。
### 3. PyMuPDF 初次接入后公式误判过多
接入 `PyMuPDF` 后,可以通过:
```python
page.get_text("dict", sort=True)
```
拿到 block、line、span、bbox、font 等信息。这比 `pypdf` 更适合定位公式区域。
但初版公式识别遇到一个问题:误判过多。
例如:
- 版权页中的电话号码。
- 普通正文中的 `Black-Scholes-Merton`。
- 普通段落里出现一个 `𝜎` 或 `F=ma`。
- 图表坐标轴上的数字。
都可能被误识别为公式。
解决方法:
- 从 block 级公式识别改为 line 级公式识别。
- 不再把普通斜体字体当作数学字体。
- 收紧公式触发条件:
- 单独的希腊字母不算公式。
- 普通 `-`、`/` 不作为强数学信号,避免把英文连字符误判为公式。
- 重点识别 `=`、`∫`、`∑`、`√`、`≤`、`≥`、`∕`、公式编号等强信号。
- 增加 `is_useful_formula_text()`,过滤掉太短、太碎、无核心公式结构的片段。
- 对公式续行做合并,避免根号、分母、括号被拆成多个孤立公式 chunk。
最终实现了:
```text
正文 chunk
公式 chunk: content_type=formula
公式位置: formula_bbox
公式编号: formula_id
```
### 4. 章节和标题切分缺失
原始系统只用固定长度切分:
```python
SentenceSplitter(chunk_size=1000, chunk_overlap=150)
```
问题是:
- chunk 可能跨章节。
- 一个小节的标题和正文可能被分开。
- 检索结果不知道来自哪一章、哪一节。
- 回答时引用不够清楚。
解决方法:
在 `SentenceSplitter` 前增加一层章节/标题感知分段:
- 识别 `CHAPTER ...`
- 识别 `APPENDIX ...`
- 识别全大写标题
- 识别标题式大小写小节名
- 过滤图表标题、坐标轴、公式短行、脚注、普通解释句
并写入 metadata:
```python
chapter_title
section_title
section_path
page_number
content_type
formula_id
```
这样检索结果可以返回:
```text
source: The_volatility_Smile_Wiley.pdf
page: 379
section: WITH ZERO CORRELATION
content_type: formula
formula_id: formula-378-3
```
### 5. metadata 过长导致 LlamaIndex 报错
接入公式 bbox 后,最开始把每一行的 bbox 都放进 metadata,导致 metadata 太长。
报错类似:
```text
Metadata length is longer than chunk size.
Consider increasing the chunk size or decreasing metadata size.
```
原因是 `SentenceSplitter` 会把 metadata 长度也计入 chunk 长度。
解决方法:
- 不再存所有行的 bbox。
- 将多个 bbox 合并成一个外接矩形:
```text
x0,y0,x1,y1
```
这样既保留了公式位置,又避免 metadata 过长。
### 6. Hugging Face 模型加载反复联网
本地已经有 embedding 模型缓存,但 `sentence-transformers` 仍尝试访问 Hugging Face 做 HEAD 检查。在网络受限环境下,会反复 retry,导致索引构建卡住。
解决方法:
- 检测本地 snapshot 是否存在。
- 如果存在,直接把本地 snapshot 路径传给 embedding 模型。
- 设置离线环境变量:
```python
HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1
```
这样索引构建可以稳定使用本地缓存。
### 7. 旧索引不会自动更新
PDF 提取逻辑升级后,如果 Chroma 里还是旧版本文本,RAG 实际不会变好。
解决方法:
- 增加 `PDF_EXTRACTION_METHOD` 版本号。
- 当前版本为:
```python
pymupdf_formula_blocks_v5
```
- 启动时检查 Chroma 中 metadata 的 `extraction_method`。
- 如果版本不一致,自动重建索引。
## 三、最终方案
最终 PDF RAG 流程变为:
```text
PyMuPDF 提取 block / line / span / bbox / font
-> 识别公式行
-> 合并公式续行
-> 生成独立公式文档 content_type=formula
-> 正文中保留 [FORMULA id=...] 引用
-> 清洗页眉页脚和噪声
-> 按章节/标题预分段
-> SentenceSplitter 二次切分
-> 写入 Chroma
-> 检索时返回 page / section / content_type / formula_id
```
核心收益:
- 公式可以作为独立检索单元。
- 正文仍保留公式上下文。
- chunk 不再完全依赖固定长度。
- 检索结果能说明来源页码、小节、内容类型。
- 索引版本可控,避免旧数据污染。
## 四、面试中可以怎么回答
可以这样概括:
> 我们一开始的 RAG 只是用 `pypdf` 按页提取文本,然后用固定长度切分。这个方案对普通文档可以,但对金融教材不够,因为里面有大量数学公式、图表和章节结构。主要问题是公式顺序错乱、上下标丢失、页眉页脚混入、chunk 跨章节。
然后讲解决:
> 我先做了基础清洗,包括页眉页脚去重、断词修复、公式行换行保留。后来发现 `pypdf` 对公式区域的定位能力有限,所以接入了 `PyMuPDF`,利用它返回的 block、line、span、bbox 和 font 信息,单独识别公式区域,并把公式作为 `content_type=formula` 的独立 chunk 入库,同时正文里保留 `[FORMULA id=...]`,这样检索公式和检索上下文都可以兼顾。
再讲工程取舍:
> 公式识别不能简单看到希腊字母就判定为公式,否则普通正文会大量误判。所以我把规则收紧到等号、积分、求和、根号、公式编号、比较符等强数学信号,并过滤掉太短的碎片。bbox 也不能直接把所有行都写入 metadata,因为 LlamaIndex 会把 metadata 计入 chunk 长度,所以我把多个 bbox 合并成一个外接矩形。
最后讲效果:
> 优化后索引从原来的纯文本 chunk,变成了正文 chunk 加公式 chunk 的混合结构。每条检索结果都带 page、section、content_type、formula_id 等 metadata,回答时更容易定位来源,也更适合处理“某个公式是什么意思”这类问题。
## 五、后续可继续优化
目前已经接入 PyMuPDF,但还不是完整 OCR/LaTeX 公式识别。后续可以继续做:
1. 对 `formula_bbox` 区域裁图。
2. 接入公式 OCR 模型,例如 LaTeX OCR。
3. 把公式图片转成 LaTeX。
4. metadata 中同时保存:
```python
formula_text_raw
formula_latex
formula_bbox
page_number
section_path
```
5. 检索时对公式 query 单独加权,或者做 hybrid search。
6. 增加 reranker,提高公式相关问题的排序质量。
## 六、一句话总结
这次优化的核心不是简单换一个 PDF parser,而是把 PDF 解析从“按页提取纯文本”升级成“结构化解析正文、章节和公式区域”,让 RAG 的 chunk 更接近人阅读文档时的语义边界。
|