犀牛鸟 2026 研究笔记 estelledc.github.io

案例 #1808:PDF 强制扫描件 OCR

PR:#1808 | 14 文件 +210/-6 | 2026-06-25 合入 | 栈:Python docreader + Go knowledge_process + 上传 UI

结论先行

部分 PDF(网页打印、扫描件、图片型页面)带有 低质量或误导性 text layer — 自动 router 把它们当「文本页」,只抽出少量乱码,跳过 scanned-page OCR/VLM 路径。结果:ingest 成功、chunk 极少或为空、检索 永远空。本 PR 增加 force-scanned 解析路径:全局 env DOCREADER_PDF_FORCE_SCANNED + 单次上传 parser_engine_overrides.pdf_force_scanned + 上传 UI 勾选,强制走 scanned-page OCR,并与自动扫描路由 对齐 metadatascanned_page_count 等)。

适合谁读:改 docreader PDF router、ingest 配置透传、上传 UI 的全栈贡献者。

Ch39 解析天花板 · Ch28 docreader · 对比 case-1785(噪声过滤)与 case-1743(跨语言管线)。


1. 问题现象(What)

用户故事

  1. 用户上传 扫描 PDF浏览器「打印为 PDF」 的文档
  2. 控制台显示解析 完成,无报错
  3. 知识库 chunk 数 ≈0 或只有封面乱码
  4. 任意 QA:检索不到内容

修复前 / 修复后

观测 修复前 修复后
ingest 状态 成功 成功
chunk 数 ≈0 / 乱码 OCR 后正常文本
用户操作 无杠杆 UI 勾选 / env / override

为何「成功但空」

自动 PDF router 看到「存在 text layer」→ 分类为 text page → 走 text extract → 实际可用字符极少 → 不触发 OCR/VLM。用户感知是 ingest 假成功

类比

一本书表面有目录(text layer),但正文页是照片 — 自动流程只读目录就当读完了全书,没有翻开照片页做 OCR


定位步骤

rg "force_scanned|FORCE_SCANNED|pdf_force_scanned" docreader/ internal/
rg "UploadConfirmDialog|parser_engine_overrides" frontend/

阅读顺序pdf_parser.py router 分支 → Go knowledge_process_config.go override 合并 → UploadConfirmDialog.vue 勾选透传。


2. 根因分析(Why)

三栈配置透传

flowchart TD
    A[UploadConfirmDialog.vue<br/>勾选「扫描件 OCR」] --> B[上传请求携带<br/>parser_engine_overrides]
    B --> C[Go knowledge_process<br/>types + service 合并 override]
    C --> D[tenant 级 override 之上叠加 upload 级]
    D --> E[docreader pdf_parser.py<br/>force_scanned 分支]
    E --> F[scanned-page OCR/VLM]
    F --> G[chunk + embed + 检索]

    H[DOCREADER_PDF_FORCE_SCANNED<br/>全局 env] --> E

与自动路由的分歧点

路径 触发条件 结果
自动 text text layer 非空(哪怕垃圾) 少量乱码 chunk
自动 scanned router 判 scanned OCR → 正常文本
force-scanned(本 PR) 用户/运维 显式指定 跳过误导性 text layer,走 OCR

关键文件

文件 职责
UI frontend/.../UploadConfirmDialog.vue + i18n 仅当批次含 PDF 时显示 toggle
Go types internal/types/knowledge_process.go parser_engine_overrides 结构
Go service knowledge_process_config.go upload override 覆盖 tenant override
Python docreader/parser/pdf_parser.py force 分支进 scanned 解析
测试 test_pdf_router.py、Go override 序列化测 text PDF vs force-scanned

PR 要求 force 路径输出 metadata 与自动 scanned 对齐

便于下游监控与 UI 展示一致。


3. 解决方案(How)

PR 四段式

What Why How Test
扫描 PDF 空 chunk text layer 误导 router force-scanned 三栈 + env pytest + Go 测 + UI

两级开关

  1. 运维全局DOCREADER_PDF_FORCE_SCANNED=true — 适合扫描件为主的部署
  2. 用户单次上传parser_engine_overrides.pdf_force_scanned: true — 默认自动,必要时手动

合并规则:upload 级叠在 tenant 级之上 — 避免租户默认误伤所有 PDF,又允许单次补救。

为何不做「自动检测 text layer 质量」?

Python 测试要点

test_pdf_router.py


4. 与 #1785 / #1725 对比

案例 问题类型
case-1785 Python parser 噪声 进 chunk
case-1725 Go docparser 格式 不满足 chunker 保护
case-1808 Py+Go+UI 策略路由 选错解析路径

三者合成 ingest checklist:选对路径 → 清噪声 → 归一化格式 → 再分块


5. 排查 playbook:「ingest 成功但检索空」

  1. 看 chunk 数 / 平均长度 — 是否 ≈0
  2. 看 knowledge 解析 metadata — text_page_count vs scanned_page_count
  3. 若是扫描 PDF — 重传并开 force-scanned
  4. 对比 lab D 组 query 命中数

6. 常见误区

错误认知:有 text layer 就是文本 PDF。
正确理解:web-print / OCR 残留层可能是 误导信号

错误认知:解析失败才会空 chunk。
正确理解:策略误判 也会产出「合法空结果」— 更隐蔽。

错误认知:只改 Python pdf_parser 就够。
正确理解:无 UI/types 透传,用户 无法 在正确入口触发 — 14 文件是 完整产品闭环

错误认知:ingest 报错才需要排查,成功就说明解析没问题。
正确理解:策略误判 会产出「合法空 chunk」— 必须看 text_page_count / scanned_page_count metadata,必要时开 force-scanned 重传。


7. 思考点

  1. force-scanned 的 成本(时延、GPU)如何在 UI 上告知用户?
  2. 全局 env 与 per-upload override 冲突时,应以谁为准?(PR:upload 叠 tenant)
  3. 若 OCR 后仍空,下一步应查 VLM 配额 还是 PDF 加密
  4. 设计 A/B:自动 text quality 分数 vs 纯手动 toggle,各适合什么租户?

章末自检


延伸阅读