背景

RAG 项目验证期需要把 PDF、Excel 等文档入库。最初的 parse_pdf_v2.py 是为 KOKOPELLI 财报手写的规则:识别表格行、拼表头、映射列名(FY2025=440, FY2026=481)。问题是每换一份 PDF 就要调一遍规则,工作量不可持续。

于是试了几种方案,对比结果如下。

方案一:手搓规则(parse_pdf_v2.py 初版)

做法:pypdf 提取文本 → 正则识别表头行、数据行 → 拼成 Net Sales (FY2025=440, FY2026=481) → 单独 chunk 入库。

优点:

  • 在 KOKOPELLI 财报上语义映射精确,FY2025=440 直接对上。
  • chunk 短,检索准,「Net Sales 是多少」稳定命中 p27。

缺点:

  • 每份新 PDF 都要调规则。guess_columns、is_table_header、is_table_row 是针对 KOKOPELLI 写的,换一家公司财报,表头格式、列名、语言全变,规则失效。
  • 两层表头、表头被截断、FY 2025 带空格,都要单独处理,正则越写越多。
  • 维护成本:接 50 份 PDF 就是 25 小时调规则。

结论:验证期只接 1–2 份格式固定的文档时可用,但不可持续。

方案二:MarkItDown(微软)

做法:pip install markitdown[pdf],MarkItDown().convert(pdf) 转 Markdown。

优点:

  • 安装轻,不加载大模型,内存占用低。
  • 支持 PDF、Word、PPT、Excel、网页,统一转 Markdown。
  • 不需要调规则。

缺点:

  • 对财报表格反而更差。内部用 pdfplumber,但转 Markdown 时没有把表格还原成 | 管道表,只是把每个单元格的文本按顺序拼成一句。KOKOPELLI p27 输出的是 Provided Services Financial SMEs Institutions Management Support... 这种混在一起的文本,比 pypdf 裸提取还糟。
  • 适合普通文档(Word、网页),不适合表格密集的 PDF。

结论:不适用于财报表格。可以作为 Word/PPT 的解析器,但 PDF 表格不行。

方案三:Docling(IBM)

做法:pip install docling,DocumentConverter().convert(pdf) 输出结构化 Markdown,表格是 Markdown 管道表。

优点:

  • 表格提取质量好,Thoughtworks 技术雷达评价「生产级提取工作负载中表现良好」,官方 TEDS 分数简单表 95.4、复杂表 90.1。
  • 支持 PDF、DOCX、PPTX 等,自动识别格式。
  • 输出 Markdown 表格,表头和数据行按原始布局排列,LLM 能直接读。

缺点:

  • 内存占用高,首次加载模型约 1.5–2GB。5.8G VPS 上和 bge-m3(1.5–2GB)不能同时跑,必须串行。
  • 已知坑:孤立表格(周围没文字)会被误判成图片(<!-- image -->),单元格全丢;跨页表格不支持重建。
  • 没在 KOKOPELLI PDF 上实际跑过(内存不够,swapoff 后 available 117Mi,装不上)。

结论:表格质量最好,但内存约束下验证期跑不动。如果以后升级 VPS 内存,或接更多格式的文档,Docling 是首选。

方案四:整页 chunk(零规则)

做法:pypdf 提取每页文本 → clean_text(去页眉页脚、合并字距)→ 整页作为一个 chunk 入库,不做表格行识别、不做列名映射。

优点:

  • 零规则。新 PDF 进来,clean_text 是通用清洗,直接入库,不调任何规则。
  • 内存低,pypdf 纯文本提取,不加载模型。
  • 表头和数据行在同一个 chunk 里,LLM 自己读对应关系。

缺点:

  • 检索精度略降。整页几千字符,Net Sales 440/481 只占一小部分,embedding 被页面整体主题稀释。实测 Net Sales 的 top-1 是 p50 的 Growth Targets(sim=0.5449),p27 排第二(0.5239)。
  • 需要 top_k=5 才能让 p27 进候选,top_k=3 时可能漏掉。

实测结果(top_k=5):

问题 结果
Net Sales 是多少 ✅ 481(FY2026 Q1),区分 actual/forecast
EBITDA 是多少 ✅ 58 百万日元
营业利润是多少 ✅ 8 百万日元(FY2026 Q1)
第2天下午去哪 ✅ 祇园 + 伏见稻荷

4/4 通过。模型从整页原文里读出表头 FY2025 Q1 Results (a) | FY2026 Q1 Results Total (b) 和数据行 Net Sales 440 481 481,自己对应 481 是 FY2026 Q1。

结论:验证期最优。零规则、内存低、4/4 通过。代价是检索精度略降,靠 top_k=5 和 LLM 的表格阅读能力弥补。

方案五:agent 语义映射(未实施)

做法:解析器只做结构还原,agent 判断「表头在哪、列名是什么」,输出结构化 chunk。

优点:最灵活,能处理任意表头格式。

缺点:每页表格一次 LLM 调用,入库慢、成本累积,agent 也会错。验证期不需要。

对比总结

方案 表格质量 需调规则 内存 验证期可用
手搓规则 精确(单份) 每份调 低 ✅ 但不可持续
MarkItDown 差(表格丢失) 不需要 低 ❌ 财报表格不行
Docling 最好 不需要 高(1.5–2GB) ❌ 内存不够
整页 chunk 够用(LLM 读) 零规则 低 ✅ 4/4 通过
agent 映射 灵活 不需要 中 验证期不需要

最终选择:整页 chunk

理由:

  1. 零规则,新 PDF 进来不调任何 guess_columns、is_table_header。
  2. 内存低,pypdf 纯文本,和 bge-m3 共存压力小。
  3. 4/4 通过,LLM 自己读表头对应列名,不需要你预先映射。

代价:

  • 检索精度略降,top_k 从 3 提到 5。
  • 整页 chunk 较长,max_length 从 8192 降到 2048 避免 OOM。
  • BATCH 从 10 降到 3,入库慢一些。

以后如果要接更多格式(Word/PPT),可以:

  • Word/PPT 用 MarkItDown(它对非表格文档质量好)。
  • 如果 VPS 升级内存,PDF 换 Docling,表格质量提升。
  • 如果接几十份不同公司财报,考虑 agent 映射。

一个诚实的结论

手搓规则在单份文档上确实比通用工具精确,但不可持续。企业级方案的核心不是「写更聪明的规则」,而是「接受工具的输出质量,把语义理解交给 LLM」。整页 chunk 就是这个思路的验证:不追求 chunk 自包含,让 LLM 读原始表格,4/4 通过。

验证期的目标是「RAG 能不能支撑股票分析场景」,不是「表格解析完美无缺」。整页 chunk 证明了前者成立,后者可以延后。

文件路径

  • /opt/rag-new/parse_pdf_v2.py(整页 chunk)
  • /opt/rag-new/bge_m3.py(max_length=2048)
  • /opt/rag-new/ingest.py(BATCH=3)
  • /opt/rag-new/config.py(TOP_K=5)

下一步:nginx + HTTPS + WordPress 对接。