【PaddleOCR】扫描版 PDF 无法复制、RAG 检索为空怎么解决?------OCR 解析与版面还原实战
把 PDF 上传到知识库后,系统没有报错,切分数量却是 0;换一个解析器,终于能提取文字,但双栏论文被拼成"左一行、右一行",表格也变成毫无关系的数字。后面的 embedding、向量库和大模型即使全部正常,检索结果仍然会偏离原文。
这类问题通常不在 RAG 模型,而在文档入口:页面可能只有扫描图像,没有文本层;即使 OCR 识别出字符,也不等于恢复了标题、段落、表格和阅读顺序。本文用 PaddleOCR 的 PP-StructureV3 搭建一条可复现的 PDF→Markdown→语义分块流程,并给出质量门禁与系统排错方法。

教学图:扫描页面经过 OCR、版面分析、结构化输出后,才能成为可检索知识。
1. 为什么普通 PDF 解析器会返回空文本
PDF 是页面描述格式,不等于"带文字的电子文档"。同样看起来是一页中文内容,内部可能有三种形态:
- 文本型 PDF:页面保存字符、字体和坐标,通常可以直接提取。
- 扫描型 PDF:每页只有一张图,肉眼能看见文字,解析器却拿不到字符。
- 混合型 PDF:正文有文本层,印章、截图、公式或部分表格仍是图像。
先用一个低成本探针判断,不要上来就对所有文件运行 OCR。以 PyMuPDF 为例,可以统计前几页的可提取字符数:
python
import fitz
def inspect_pdf(path: str, sample_pages: int = 5) -> dict:
doc = fitz.open(path)
lengths = []
for page in doc[: min(sample_pages, len(doc))]:
lengths.append(len(page.get_text("text").strip()))
return {
"pages": len(doc),
"sampled": len(lengths),
"characters": sum(lengths),
"empty_pages": sum(n == 0 for n in lengths),
}
如果五页合计只有几十个字符,或者大部分页面为空,就应进入 OCR 路径。但"字符多"也不能证明版面正确:多栏论文按坐标粗暴排序后,字符数可能很多,阅读顺序仍然错误。
#mermaid-svg-wUW0FF9ZSCBW1Vaw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .error-icon{fill:#552222;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .marker.cross{stroke:#333333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw p{margin:0;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .cluster-label text{fill:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .cluster-label span{color:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .cluster-label span p{background-color:transparent;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .label text,#mermaid-svg-wUW0FF9ZSCBW1Vaw span{fill:#333;color:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .node rect,#mermaid-svg-wUW0FF9ZSCBW1Vaw .node circle,#mermaid-svg-wUW0FF9ZSCBW1Vaw .node ellipse,#mermaid-svg-wUW0FF9ZSCBW1Vaw .node polygon,#mermaid-svg-wUW0FF9ZSCBW1Vaw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .rough-node .label text,#mermaid-svg-wUW0FF9ZSCBW1Vaw .node .label text,#mermaid-svg-wUW0FF9ZSCBW1Vaw .image-shape .label,#mermaid-svg-wUW0FF9ZSCBW1Vaw .icon-shape .label{text-anchor:middle;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .rough-node .label,#mermaid-svg-wUW0FF9ZSCBW1Vaw .node .label,#mermaid-svg-wUW0FF9ZSCBW1Vaw .image-shape .label,#mermaid-svg-wUW0FF9ZSCBW1Vaw .icon-shape .label{text-align:center;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .node.clickable{cursor:pointer;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .arrowheadPath{fill:#333333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wUW0FF9ZSCBW1Vaw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wUW0FF9ZSCBW1Vaw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wUW0FF9ZSCBW1Vaw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .cluster text{fill:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .cluster span{color:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wUW0FF9ZSCBW1Vaw rect.text{fill:none;stroke-width:0;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .icon-shape,#mermaid-svg-wUW0FF9ZSCBW1Vaw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .icon-shape p,#mermaid-svg-wUW0FF9ZSCBW1Vaw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .icon-shape .label rect,#mermaid-svg-wUW0FF9ZSCBW1Vaw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wUW0FF9ZSCBW1Vaw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wUW0FF9ZSCBW1Vaw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wUW0FF9ZSCBW1Vaw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否
是
PDF 输入
能否提取足够文本?
直接提取文本层
是否包含表格、公式或多栏?
通用 OCR
PP-StructureV3 版面解析
规范化 Markdown
按标题与语义切分
质量门禁后入库
这个路由并非要求先精确知道文档类型,而是通过低成本检测逐步升级:能可靠提取就不做 OCR;只有图像文字时做通用 OCR;遇到表格、公式、多栏和图注时,才启用完整的版面解析。
2. OCR、版面分析和结构化输出分别解决什么
OCR 通常包含文字检测与文字识别。检测模块找到文本框,识别模块把框内像素转换为字符。但一份技术文档还包含更高层的问题:哪个框是标题,哪个是页眉,表格的单元格如何组合,双栏正文先读哪一栏,图片说明属于哪张图。
PP-StructureV3 的官方文档将版面分析描述为对标题、正文、表格、公式、图像、页眉页脚等区域进行识别并输出结构化结果。它支持直接处理 PDF,每页得到 Markdown 信息,也提供多页 Markdown 合并方法。页面方向分类、文档去畸变和文本行方向模块默认可以按需求开启,而不是所有场景无脑打开。

教学图:预处理、OCR/版面检测、结构化 Markdown 和质量门禁各自承担不同职责。
工程上应把这条链路拆成可观测阶段:
- 页面预处理失败,要看旋转、透视和分辨率;
- 检测失败,要看小字、背景、检测阈值;
- 识别失败,要看语言模型与字符置信度;
- 阅读顺序失败,要看版面区域和排序;
- 检索失败,则继续检查切分、元数据和 embedding。
把所有问题都归结为"OCR 不准",会导致参数越调越乱。
3. 使用 PP-StructureV3 生成完整 Markdown
当前官方示例使用 PPStructureV3 创建流水线,通过 predict() 处理图像或 PDF。PDF 会逐页产生结果;如需合并整本文件,应收集每页的 result.markdown,再调用 concatenate_markdown_pages()。下面是可直接改造的核心代码:
python
from pathlib import Path
from paddleocr import PPStructureV3
input_file = Path("manual.pdf")
output_dir = Path("output")
output_dir.mkdir(parents=True, exist_ok=True)
pipeline = PPStructureV3(
device="cpu",
use_doc_orientation_classify=True,
use_doc_unwarping=True,
use_textline_orientation=True,
)
pages = []
page_images = []
for result in pipeline.predict(input=str(input_file)):
pages.append(result.markdown)
page_images.append(result.markdown.get("markdown_images", {}))
markdown = pipeline.concatenate_markdown_pages(pages)
(output_dir / "manual.md").write_text(markdown, encoding="utf-8")
for image_map in page_images:
for relative_path, image in image_map.items():
target = output_dir / relative_path
target.parent.mkdir(parents=True, exist_ok=True)
image.save(target)
安装时要按官方文档选择与操作系统、CPU/GPU 匹配的 PaddlePaddle,再安装 PaddleOCR 文档解析相关依赖。不要照搬旧文章中的 2.x API:PaddleOCR 3.x 的流水线类、参数和结果对象已有变化。生产环境还要固定依赖版本和模型缓存,否则镜像重新构建时可能出现不可预测升级。
完整示例 code/pdf_to_markdown.py 增加了 Markdown 清洗、基于标题的分块、内容哈希和 --self-test。运行方式如下:
bash
python pdf_to_markdown.py manual.pdf --output output --device cpu
python pdf_to_markdown.py ignored.pdf --self-test
自检不加载模型,用于验证清洗和分块逻辑。实际 PDF 推理会首次下载模型,应提前在部署环境准备缓存并评估磁盘、内存与启动时间。
4. 多栏、表格与公式为什么必须恢复阅读顺序
扫描页经过 OCR 后会得到许多坐标框。若简单按 y 坐标再按 x 坐标排序,双栏页面可能读成"左栏第一行→右栏第一行→左栏第二行",两段完整文字被交叉拼接。这样的文本语法看似通顺,语义却已经损坏,尤其难通过字符数量发现。

教学图:先确定版面区域,再在区域内部恢复阅读顺序,避免两栏内容交叉。
表格也不能只保留单元格文字。价格 99 数量 2 如果失去列头与行关系,检索到数字也无法回答"哪个商品的价格是多少"。结构化输出至少应保存表格 Markdown 或 HTML、表格标题、所属章节和页码。公式则要区分普通字符识别与公式识别;下标、积分上下限和矩阵结构被拉平后,文本相似度并不能代表公式含义。
建议抽取三类"金样本页"人工检查:一页纯正文、一页复杂表格、一页多栏或公式。每次升级模型或调整阈值,都重新比较这些页面,而不是只观察单个成功截图。
5. Markdown 清洗与语义切分怎么做
OCR 输出需要清洗,但清洗不能破坏结构。可以删除稳定重复的页眉页脚、孤立页码和扫描水印;不要用一个正则把所有短行删除,因为标题、表头和列表项本来就短。跨页段落也要谨慎合并,尤其是前页句末没有标点、后页开头为小写英文或连续正文时。
分块建议分两层:先根据 H1/H2/H3 切章节,再对超长章节做带重叠窗口。每个块附带 document_id、页码范围、标题路径、块序号、解析器版本和内容哈希。内容哈希既可去重,也能判断文件重跑后哪些块发生变化,避免全量重建向量。

教学图:章节边界优先,字符窗口只处理超长章节,并保留可追溯元数据。
示例代码把中文技术文档的窗口设为 900 字符、重叠 120 字符。这只是可解释的起点,不是通用最优值。若问题通常针对单个参数,块可以更小;若答案依赖完整步骤或表格,块要更大,或使用父子块检索。真正的参数选择应使用一组已知答案的问题,比较召回率、上下文噪声和最终回答正确率。
6. 入向量库之前建立质量门禁
"解析成功"只表示程序没有抛异常。建议至少记录以下指标:
- 每页字符数分布与空页比例;
- 低置信度文本框比例及其所在区域;
- 标题层级跳跃、表格数量与公式数量;
- 页面处理耗时、峰值内存和失败重试次数;
- 用已知问题检索时,正确页是否进入 Top K。

教学图:质量门禁同时覆盖文本、结构和检索结果,不能只看 OCR 平均分。
#mermaid-svg-7m40l5XsKyeHgkvv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7m40l5XsKyeHgkvv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7m40l5XsKyeHgkvv .error-icon{fill:#552222;}#mermaid-svg-7m40l5XsKyeHgkvv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7m40l5XsKyeHgkvv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7m40l5XsKyeHgkvv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7m40l5XsKyeHgkvv .marker.cross{stroke:#333333;}#mermaid-svg-7m40l5XsKyeHgkvv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7m40l5XsKyeHgkvv p{margin:0;}#mermaid-svg-7m40l5XsKyeHgkvv defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-7m40l5XsKyeHgkvv g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-7m40l5XsKyeHgkvv g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-7m40l5XsKyeHgkvv g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-7m40l5XsKyeHgkvv g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-7m40l5XsKyeHgkvv g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-7m40l5XsKyeHgkvv .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-7m40l5XsKyeHgkvv .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-7m40l5XsKyeHgkvv .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-7m40l5XsKyeHgkvv .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7m40l5XsKyeHgkvv .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-7m40l5XsKyeHgkvv .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-7m40l5XsKyeHgkvv .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-7m40l5XsKyeHgkvv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7m40l5XsKyeHgkvv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7m40l5XsKyeHgkvv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7m40l5XsKyeHgkvv .edgeLabel .label text{fill:#333;}#mermaid-svg-7m40l5XsKyeHgkvv .label div .edgeLabel{color:#333;}#mermaid-svg-7m40l5XsKyeHgkvv .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-7m40l5XsKyeHgkvv .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-7m40l5XsKyeHgkvv .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-7m40l5XsKyeHgkvv .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-7m40l5XsKyeHgkvv .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-7m40l5XsKyeHgkvv .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7m40l5XsKyeHgkvv .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7m40l5XsKyeHgkvv #statediagram-barbEnd{fill:#333333;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7m40l5XsKyeHgkvv .cluster-label,#mermaid-svg-7m40l5XsKyeHgkvv .nodeLabel{color:#131300;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-7m40l5XsKyeHgkvv .note-edge{stroke-dasharray:5;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-note text{fill:black;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram-note .nodeLabel{color:black;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagram .edgeLabel{color:red;}#mermaid-svg-7m40l5XsKyeHgkvv #dependencyStart,#mermaid-svg-7m40l5XsKyeHgkvv #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-7m40l5XsKyeHgkvv .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7m40l5XsKyeHgkvv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 低置信度/空页/阅读顺序异常
指标通过
调整参数后重跑
无法自动修复
Parsed
Review
Accepted
Rejected
Indexed
阈值必须来自自己的文档类型。发票、论文、古籍和设备手册的字符密度完全不同,不能虚构一个"低于 0.8 就失败"的统一规则。更稳妥的方法是先人工标注几十页,观察正常值分布,再设告警线与拒绝线。
7. 七个常见失败案例与对应处理
7.1 PDF 可以复制,但抽取内容仍然混乱
原因通常是文本对象保存顺序与视觉阅读顺序不同。改用带版面区域的解析结果,不要认为"有文本层"就一定适合直接进入 RAG。
7.2 识别结果整体旋转 90°
开启文档方向分类或文本行方向识别。若只是少量页面旋转,先做页面级路由,避免为整本文件增加不必要耗时。
7.3 手机拍照文档中间清楚、边缘扭曲
启用文档去畸变,并检查裁边是否截掉页码或表格边框。去畸变会增加计算量,应记录开启前后的错误率,而不是只看视觉更"平"。
7.4 小字号脚注和表格数字大量漏检
先检查 PDF 渲染分辨率,再检查检测模型的边长与阈值。直接降低置信度阈值可能引入大量背景噪声。长边分辨率翻倍会显著增加像素量、显存和耗时,要按页型分级处理。
7.5 表格被识别为连续段落
确认启用了表格识别,检查无线表与跨行合并单元格。对核心业务表格最好同时保存结构化 HTML 和原始区域截图,便于答案引用与人工复核。
7.6 OCR 很准确,RAG 仍答非所问
继续检查分块和检索:标题是否进入 chunk,表格是否被拆开,重复页眉是否占据向量相似度,embedding 是否适合当前语言。不要继续盲调 OCR。
7.7 本地运行正常,容器首次启动极慢或失败
模型可能在运行时下载,网络受限时会失败;CPU 指令集、GPU 运行时与依赖版本也可能不匹配。构建阶段预下载模型,固定版本,启动时输出模型路径、设备和流水线配置。

教学图:按预处理、检测识别、版面恢复和检索逐层定位,避免同时修改多个变量。
8. 性能、隐私与工程参数怎么取舍
文档解析的成本主要与页面像素、启用模块和模型设备有关。先使用小样本基准测试:分别选择纯正文、表格、公式、低清扫描四类页面,记录单页 P50/P95 耗时、峰值内存、字符错误和结构错误。批量任务可按页流式处理,避免整本 PDF 的所有页面同时驻留内存;失败页面单独重试,不要重跑已完成页面。
安全方面,扫描文档常含身份证号、合同、财务数据和内部印章。日志不要打印完整 OCR 文本;临时图片和 Markdown 需要限定目录、设置生命周期并控制访问权限。若使用远程解析服务,应明确文件是否离开本地、保存多久以及如何删除。上传前还要限制 PDF 页数、文件大小和解压后的像素总量,防止恶意文件耗尽资源。
参数选择可以遵循以下顺序:先保证路由正确,再保证阅读顺序,之后调检测识别,最后才调 chunk 与检索。每次只改变一个参数并保存样本结果。只有"正确页进入 Top K,答案能引用到正确段落"才算链路通过。
9. 如何做一组能复现的解析评测
如果只挑一页"看起来识别得不错"的样本,很容易高估系统效果。更可靠的评测集应覆盖真实文档分布,并且把字符正确和结构正确分开计算。可以从业务文件中选取 30~100 页,脱敏后按页面类型分层:纯正文、低清扫描、旋转拍照、双栏论文、有线表格、无线表格、公式和印章。每一类至少保留数页,不要让大量简单正文稀释复杂页面的错误。
字符层可以抽样计算字符错误率,结构层则检查标题类型、阅读顺序、表格行列和图注归属。RAG 层准备一批只能从这些页面回答的问题,记录正确页面进入 Top 1、Top 3、Top 5 的比例。这样可以区分三种情况:字符已经错了;字符正确但结构错;解析全部正确而检索策略错。
建议为每次运行保存如下清单,而不是只保存最终 Markdown:
json
{
"document_id": "manual-v3",
"parser": "PP-StructureV3",
"pipeline_config_hash": "8f0c...",
"page_count": 86,
"failed_pages": [17],
"empty_page_ratio": 0.0116,
"elapsed_seconds": 214.8,
"output_hash": "a932..."
}
这里的数字只是字段示例,不代表本文进行了该文档测试。真正运行时应由程序写入实际值。配置哈希和输出哈希能够回答"同一文件为什么这次结果变了",失败页列表则允许只重跑异常页面。
10. 大批量文档如何避免重复计算
批处理时先对原文件计算内容哈希,并把解析器版本、模型版本、关键参数一起组成任务版本。文件内容与任务版本都没有变化,就复用已有结果;只有参数变化或源文件变化时才重新解析。每一页单独保存状态,进程中断后从未完成页继续,而不是从第一页开始。
predict_iter() 与 predict() 接受相同类型的参数,但迭代接口更适合逐页消费结果、及时写盘和限制峰值内存。写入向量库前采用"两阶段提交"思路:先把新块写入带任务版本的暂存集合,质量检查通过后再切换文档的活动版本。这样解析到一半失败时,线上检索仍然读取旧的完整版本,不会出现半本手册新、半本手册旧的状态。
并发度不要直接等于 CPU 核数。文档解析同时消耗模型内存、图像解码和磁盘带宽。先从单进程测出单页 P95 与峰值内存,再逐级增加 worker;当吞吐不再线性增长或开始频繁换页,就应停止加并发。GPU 环境还要限制单任务页面尺寸,超大扫描图先缩放或分块,防止一页异常输入拖垮整个 worker。
11. 参数调整的顺序与回滚策略
遇到识别错误时,先冻结一份基线输出。第一轮只调整页面方向和去畸变,第二轮再调整文字检测,第三轮才处理表格、公式等可选模块。每轮用同一评测集比较,不要同时更换模型、渲染分辨率和切分参数,否则即使结果变好也无法知道是哪项改变生效。
配置应该进入版本控制,并记录模型文件校验值。升级 PaddleOCR 前在影子目录跑完整评测,不覆盖当前生产结果;如果正确页召回率下降、结构错误增加或资源消耗超出预算,就保留旧版本。模型升级不是单向动作,工程系统必须能够回滚到已验证的解析链路。
12. 发布前检查清单
- 用文本探针区分文本型、扫描型和混合型 PDF;
- 对多栏、表格和公式启用版面解析,而非只做字符 OCR;
- 保存 Markdown、页面图片、页码和标题路径;
- 删除稳定噪声,但不破坏标题、列表和表格结构;
- 先按章节切分,超长章节再使用重叠窗口;
- 建立空页、低置信度、结构完整度和检索抽检指标;
- 固定 PaddleOCR、PaddlePaddle 与模型版本;
- 保护原始文件、识别结果与日志中的敏感信息;
- 用真实问题验证正确页面是否进入 Top K。
如果你的 PDF 已经能识别出文字,却仍然检索不到答案,可以重点观察两项:输出 Markdown 中的阅读顺序,以及正确段落被切到了哪个 chunk。这两处往往比继续更换 embedding 模型更快找到根因。