Spaces:
Runtime error
Runtime error
| # 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 更接近人阅读文档时的语义边界。 | |