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