
背景
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
理由:
- 零规则,新 PDF 进来不调任何
guess_columns、is_table_header。 - 内存低,pypdf 纯文本,和 bge-m3 共存压力小。
- 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 对接。


讨论
订阅评论 RSS加入讨论
分享你的想法与经验,也可以登录后参与讨论。
使用 WordPress 登录