你手上有一批扫描版合同,二十来页。

要做的很简单:把里面的甲乙方、金额、付款节点、违约条款提取出来,做成一张表。听起来是"复制粘贴"的活,实际干下来是这样——打开 PDF,放大,一个字一个字敲,敲到第五页开始怀疑人生,敲到第十二页开始担心自己敲错了哪个数字。

后来我换了条路:把整页图片丢给一个 1.3B 的模型,它直接吐出 Markdown,正文是正文,表格是真的表格(能贴回 Excel),公式是真的 LaTeX。

这篇文章分两半。前半段讲清楚"现在的 OCR 到底卡在哪、大模型为什么能绕过去",不需要任何基础。后半段是这台机器上从零到尾的部署实录,包括我踩的五个坑,照着做能省你半天。

一、先说说 OCR 这事到底难在哪

很多人对 OCR 的印象还停留在"图片转文字"。手机扫一扫、微信提取文字,确实早就能用了。

但如果你真拿它处理过工作文档,就会发现几个绕不过去的问题。

字都认对了,顺序全乱。

双栏排版的论文,机器按"从左到右、从上到下"扫,结果把左栏第一行和右栏第一行拼成一句话。每个字都认识,连起来读不通。

表格变成一坨。

项目  数量  单价  金额  服务器  12  800  9600  ...

看上去信息都在,但你已经不知道哪个数字属于哪一列。想还原成 Excel,还是得人工再敲一遍——绕回原点。

公式直接阵亡。

E = mc² 变成 E = mc2,∑ 变成 Σ 或者干脆消失,上下标全丢。理工科文档基本没法用。

拍歪了、有阴影、隔着玻璃反光,就崩。

扫描件是"干净"的。但现实里更多是手机拍的书页:弯的、斜的、有手指压着的。

最要命的一条:你不知道哪里错了。

传统 OCR 给你一段文字,语气非常自信。但合同里一个金额错了,代价可能就是几千块。它不会告诉你"这个 8 我只有七成把握"。

为什么传统 OCR 扛不住这些?因为它的做法是一堆小模型串起来的流水线:先检测出文字框 → 逐个识别框里的字 → 按坐标排序。它能对付干净扫描件,但它不理解文档的结构——它只知道"这里有字",不知道"这块是标题、这块是表头、这块是脚注"。

顺序、表格、公式,说到底都是结构问题,而它压根没有结构这个概念。

二、换个思路:先分区,再分块问模型

MiniCPM-OCR 是面壁智能(OpenBMB)刚开源的 OCR 专用模型,1.3B 参数——按今天的大模型标准,这是个"小不点",比很多手机上的模型还小。

但它在真实场景文档解析评测 Real5-OmniDocBench 上拿到了 93.68 分,官方说是 SOTA。这个分数覆盖的是扫描、弯曲、拍照、倾斜、翻拍这些"不干净"的场景,不是精心制作的 PDF。

它干活的流程,官方叫 pipeline,实际是一张三段式的流水线:

第一步,版面检测。

这一步用的是另一个小模型(PP-DocLayoutV3,126MB),它的任务很单纯:在这一页上找出所有区块,并且标出每个区块是什么类型——标题、正文段、表格、公式、图片、页眉页脚、印章。

第二步,切图,按类型问不同的问题。

把每个框单独裁下来,然后按类型发给视觉语言模型,问法还不一样:

  • 这块是正文 → "Text Recognition:"
  • 这块是表格 → "Table Recognition:"(输出 OTSL,一种表格的结构化标记语言)
  • 这块是公式 → "Formula Recognition:"

这就是为什么顺序不会乱:先认出结构,再按结构填内容,不靠猜坐标顺序。

第三步,拼装。

表格的 OTSL 转成 HTML <table>,公式转成 LaTeX,正文按层级拼成 Markdown,最后按版面顺序串起来。

关键点在这:它输出的不是文字,是结构。 你给一页正文,它还你带标题层级的 Markdown;你给一张表,它还你带 rowspan / colspan 的真表格;你给一个公式,它还你 $S = \pi r^2$。

这是我用下来最有价值的地方。文字识别准确率早就不是瓶颈了,卡人的一直是结构。

三、为什么我没按官方教程装

这里要说点实话:官方推荐的那条路,在便宜的机器上装不上。

官方教程给的环境要求是:NVIDIA 驱动 ≥ 580(对应 CUDA 13)、显存 ≥ 24 GB,推理用 vLLM,并且开 FLASH_ATTN 注意力后端。

我这台机器是腾讯云上的一台 GPU 服务器:

项实际配置
GPU2 × Tesla T4,各 16 GB(算力 sm_75)
驱动535(对应 CUDA 12.2)
CPU / 内存40 核 / 160 GB

两个硬伤:

一是驱动版本。 官方环境用 CUDA 13,CUDA 13 默认要求驱动 ≥ 580。我这台是 535,差了一大截。

二是算力等级。 T4 是 sm_75,而 FLASH_ATTN 这个加速后端要求 sm_80 以上(A100、A10、4090 那一档)。T4 天生就不支持。

这不是配置错了,是硬件代差。硬装的话,得从源码编译一个改造版 vLLM,还要自己处理 T4 不支持 BF16 的问题——这类活干过就知道,能装上一半靠运气。

官方在发布首日就适配了四个后端:vLLM、SGLang、llama.cpp、Ollama。我选了 llama.cpp,理由很直接:

  • llama.cpp 走 GGUF 格式,量化版本现成,编译只依赖 CMake 和 nvcc,不用跟 Python 生态的版本地狱搏斗
  • 它只要求 CUDA 能跑就行,对驱动版本宽容
  • 模型转成 F16 后只有 1.5 GB,加上视觉投影层 860 MB,总共 2.3 GB

代价也很清楚:没有 DSpark 投机解码(llama.cpp 不支持),单请求延迟会比官方那套高。官方 README 里写的 1.2 pages/s 吞吐,是并发加更好硬件跑出来的;我们这台单请求是 9~24 秒一页。这个差距得认。

但对"一天处理几十页"的用量来说,完全够。

四、部署:五步,能照着敲

下面是我实际跑通的步骤。假设你已经有了一台能跑 CUDA 的 Linux 机器。

第 1 步:拉代码,装依赖

cd ~ && git clone --depth 1 https://github.com/OpenBMB/MiniCPM-OCR.git
git clone --depth 1 https://github.com/ggml-org/llama.cpp
sudo apt-get install -y cmake build-essential libcurl4-openssl-dev pkg-config

第 2 步:编译 llama.cpp(关键在这一行)

cd ~/llama.cpp
export CUDACXX=/usr/local/cuda/bin/nvcc
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=75 \
      -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 40 --target llama-server

CMAKE_CUDA_ARCHITECTURES 一定要对着你自己的卡填。 我这台 T4 是 75;A100 是 80,A10 / 3090 是 86,4090 是 89,H100 是 90。填错了不报错,但编译出来的东西一跑就崩,还挺难查。

核多的话并行编译几分钟就好。

第 3 步:下模型

mkdir -p ~/models && cd ~/models
curl -LO https://huggingface.co/openbmb/MiniCPM-OCR-1.0-gguf/resolve/main/MiniCPM-OCR-1_0-F16.gguf
curl -LO https://huggingface.co/openbmb/MiniCPM-OCR-1.0-gguf/resolve/main/mmproj-model-f16-4x.gguf

两个文件:前面那是语言模型(1.5 GB),后面那 mmproj 是视觉投影层(860 MB)——把图片编码成模型能"读"的向量。少了下不了图。

第 4 步:起服务

~/llama.cpp/build/bin/llama-server \
  --model ~/models/MiniCPM-OCR-1_0-F16.gguf \
  --mmproj ~/models/mmproj-model-f16-4x.gguf \
  --host 0.0.0.0 --port 18499 \
  --ctx-size 16384 --n-gpu-layers 999 \
  --parallel 1 --alias minicpm-ocr --jinja

--alias minicpm-ocr 是给模型起个名字,SDK 里按名字找。--jinja 让它支持 chat template,不然图片消息发不进去。

起来以后 curl http://127.0.0.1:18499/health 应该返回 {"status":"ok"}。

我把它包成了 systemd 服务,开机自启、崩了自动重启,省心:

[Unit]
Description=MiniCPM-OCR (llama.cpp, OpenAI-compatible)
After=network-online.target
Wants=network-online.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/deploy
ExecStart=/home/ubuntu/deploy/start-llamacpp.sh
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

第 5 步:装 SDK,跑第一份文档

单纯调 API 只能识别"一整张图里的字",做不了版面切分。要处理 PDF、要还原表格结构,得用官方 SDK:

cd ~/deploy
python3 -m venv sdk-env
./sdk-env/bin/pip install "paddlepaddle==3.2.1"          # 版面检测用 CPU 版
./sdk-env/bin/pip install -e "~/MiniCPM-OCR/sdk[all]"
./sdk-env/bin/pip install "paddlex==3.7.2"

然后一条命令:

./sdk-env/bin/minicpmvocr parse 合同.pdf -o 合同.md \
  --backend llamacpp --device cpu \
  --server-url http://127.0.0.1:18499 --model minicpm-ocr \
  --workers 1 --set pipeline.queue_mode=serial

第一次跑会下载版面检测模型(126 MB),下完就不用再下了。

五、实测:一组真实的数字

跑完之后我把四种典型输入各测了一遍,用的是页内指标(不含启动开销)和端到端墙钟两种口径:

输入端到端版面检测模型推理输出
1240×900 中文图文(含公式、小表)9.5 s4.6 s2.3 s353 字
A4 正文页(200 dpi)12.8 s4.6 s5.6 s432 字
A4 文本 + 表格 PDF9.5 s4.5 s2.3 s682 字
真实表格图 2000×308323.5 s4.6 s16.2 s3760 字

显存占用 2911 MiB / 16384 MiB,理论上一张卡能塞五个实例。

这张表里有个反直觉的地方:A4 正文页比小截图还慢。

原因是开销结构。版面检测固定 4.6 秒左右(跑在 CPU 上),加上进程启动、加载模型这些一次性开销 4~5 秒,底噪就在 9 秒以上。所以处理一页简单的图,快不到哪去——时间主要花在"准备工作"上,不是花在识别上。

那 A4 正文页的 12.8 秒、复杂表格的 23.5 秒,多出来的部分去哪了?

我拿同样的图片直接打接口量了一下:1240×900 的截图是 1829 个视觉 token,1654×2339 的 A4 页是 2262 个。像素多了三倍,token 只多了两成——因为模型会把大图压进一个固定的 token 预算里,不会因为图大就线性变慢。这算是小模型能跑得快的关键设计。

拉开差距的是后半段:输出多少个字。复杂表格那页要吐 3760 个字,推理 16.2 秒,时间几乎全在"写字"上。

所以时间大致是两段相加:固定的启动 + 版面开销(约 9 秒),加上跟输出长度成正比的推理时间。 想快,先砍固定开销和版面检测,别急着换模型。

识别质量方面,这张 2000×3083 的真实表格,输出的 HTML 里 rowspan 和 colspan 都对:

<tr><td rowspan="4">General</td><td>MMBench</td><td>83.17</td>...</tr>
<tr><td colspan="2">Size</td><td colspan="2">1.3B</td>...</tr>

中英混排、金额、公式都没问题。这类表格最费人工,花 24 秒换 3760 个字,值。

六、我踩的五个坑

这部分是本文最有价值的部分,都是实际踩到的。

坑 1:裸调 API 不带 enable_thinking,输出会跑飞

第一版我直接调 /v1/chat/completions,prompt 写了句"请识别图中文字,表格用 HTML",然后满心期待。

结果它吐了一屏 CSS,还撞满了 max_tokens。

原因:模型默认开思考模式,会先长篇大论推理一遍,再开始输出。要关掉,请求体里加:

"chat_template_kwargs": { "enable_thinking": false }

后来我干脆改用官方 prompt 词,效果更稳:

  • 全文:Text Recognition:
  • 表格:Table Recognition:
  • 公式:Formula Recognition:

坑 2:服务器没有中文字体,我差点误判模型很差

为了验证效果,我用 PIL 造了张中文测试图,跑完一看——全是 ☐☐☐。

第一反应是"这模型中文不行啊"。

其实是服务器上压根没装中文字体,PIL 画出来的图本身就是豆腐块。模型很老实地把豆腐块识别成了豆腐块。

sudo apt-get install fonts-noto-cjk 装完重造,一切正常。

这个坑很典型:测试环境的问题,容易被当成模型的问题。

坑 3:不配 --workers 1 --set pipeline.queue_mode=serial,会排到天荒地老

llama-server 默认单请求串行,一次只处理一个。但 SDK 默认会开多个 worker 并行切块。

结果是一堆请求排在门口,queue_wait 一栏直接飙到 13 秒——比我实际推理时间还长。

两个选择:要么配成串行(--workers 1 + pipeline.queue_mode=serial),要么把模型降到多实例服务池。我选了前者,简单。

坑 4:--device cpu 管的是版面检测,不是模型

看到这个参数我一度以为模型跑在 CPU 上,白担心一场。

实际上 --device cpu 只影响 PP-DocLayoutV3 那个版面检测模型,1.3B 的识别模型老老实实在 GPU 上。想看真实占用,nvidia-smi 一看就明白。

顺带说,版面检测跑 CPU 是我选的(40 核,4.6 秒还行),因为 paddle 的 GPU 轮子是按 cu129 编的,跟 535 驱动不保证兼容。想快可以自己拿显卡跑版面,但得先解决驱动版本。

坑 5:这个服务默认没有鉴权

llama-server 起来的时候没设 API Key,也就是说任何能连上 18499 端口的人,都能白嫖你的 GPU。

自己用、只在内网,问题不大。要对外开放,加一行参数:

--api-key sk-你的密钥

调用时带上 Authorization: Bearer sk-你的密钥。SDK 那边配 --set engine.api_key=...。

顺便一句:这服务是明文 HTTP,公网传输没有 TLS。单人用我更推荐隧道,不占公网端口,也不用操心鉴权。

七、什么时候别用它

夸完了,说点不合适的场景。

要极致速度,别用这套。 9~24 秒一页,批量处理一万页就是两天半。官方那套 vLLM + 投机解码的吞吐高得多,前提是你有 24 GB 显存和 580 以上的驱动。有那硬件就别省这点。

要 100% 确定,别信任何 OCR。 它会错,而且错得很自信。合同金额、身份证号这类字段,做完必须人工抽查。它的价值是把"全手打"变成"抽查 20%",不是替你做决定。

纯电子版 PDF,别折腾。 如果 PDF 里本身就有文字层(能选中文字),直接 pdftotext 就完了,一个字不差,还快一万倍。OCR 是为"图片"准备的。

需要手写体,先测。 官方主打的是印刷体文档,手写内容效果我没测,别拿它当万金油。

八、最后

这套东西有意思的地方,不在"又一个 OCR"上。它把文档解析从"识别文字"变成了"理解结构"。

传统 OCR 关心的是"这里有没有字、是什么字"。它关心的是"这块在文档里扮演什么角色"。差别看着小,落到实际使用上是天壤之别:一个给你一堆需要重新排版的文字,一个给你可以直接贴进知识库的 Markdown。

而做到这件事的模型,只有 1.3B,占 2.9 GB 显存,跑在一张 2018 年发布的 T4 上。

模型变小、部署变简单,带来的变化不是"效果差一点",而是这类能力开始能装进你自己的服务器里了。不用调 API,不用按页付费,不用担心文档里有什么不能外传的内容。

我在它前面加了个小小的网页,左边拖图,右边出 Markdown,粘贴截图也行。现在整理扫描件,从"一天的活"变成了"喝杯咖啡的功夫"。