上个月主力机重装,我为了把一本三百多页的扫描版技术手册变成可检索的电子文档,前后换了三套OCR方案。Tesseract识别纯正文还行,一旦碰到表格和双栏版式就完全露怯;PaddleOCR要单独搭版面分析还有一堆依赖;最后让我真正把整条链路跑通的是本地的Ollama,配合视觉语言模型一起把PDF逐页渲染、OCR智能解析和富格式导出全流程做完了。这篇复盘不是从零开始的教程,而是把选型逻辑、部署细节、踩坑记录和导出方案完整串起来讲一遍,给同样被扫描件折磨的人一个可以直接参考的落地路径。
这套系统本质上是用本地运行的多模态大模型替代传统OCR引擎,解决扫描版PDF、复杂版面、表格和公式的识别问题。它不需要联网、不需要付费API,把PDF丢进去,输出的是带格式的Markdown、Word或HTML。适合经常处理扫描件、想把纸质资料数字化、或者想在自己机器上折腾视觉大模型的开发者参考。下面按我当时从选型到落地的顺序来复盘。
1. 为什么我会把OCR押注在Ollama上
先说结论:传统OCR引擎和我们用的视觉语言模型(VLM)解决的不是同一个问题。传统OCR做的是“把图像里的字符抠出来”,而VLM做的是“像人一样读懂这一页,再把读懂的内容按原有结构组织出来”。这个差异决定了上限。
1.1 传统OCR方案在我项目里的三个失分项
我实际测试了几套主流方案,各有各的问题。
Tesseract版本更新到5.x之后,纯文字识别准确率其实不错,工程上也很轻量,pip装个pytesseract就能用。但它对版面几乎零理解:PDF手册里的双栏排版,识别结果会从左栏中间直接跳到右栏开头;表格的边框线、合并单元格处理得一塌糊涂;遇到代码块里的缩进和特殊符号更是直接乱套。它适合“整页都是纯文字”的理想场景,实际文档很少有这种好事。
PaddleOCR是几个开源方案里综合能力最强的,自带版面分析模型、表格结构识别模型,还支持方向分类。但它的问题是链路太重:跑通版面分析至少要拉三四个模型文件,不同模型之间的坐标对齐、置信度阈值调参很费事。而且表格识别模型对复杂表头、跨页大表的还原依然有限,识别出来是纯文本还是结构化数据,取决于具体版本和训练数据。对我这种想快速处理一批杂乱的PDF手册的人来说,成本太高。
闭源API方案我也试过,准确率和版面理解都不错,但我这本手册涉及内部技术参数,不方便传上去识别,而且几百页的文档按API调用量算也是一笔开销。我需要一个纯粹本地、可重复批量跑的方案。
1.2 视觉语言模型做OCR的本质变化
VLM做OCR的思路完全不同。它把整个页面当作一张图片“读”进去,而不是逐字切割识别。这意味着它能理解版面的层级关系:看到加粗大字知道是标题,看到等宽字体知道是代码,看到网格线知道是表格。对于“这段话是正文还是注释”这种版面语义问题,传统OCR引擎很难回答,但VLM可以。
更关键的是,VLM可以直接输出Markdown格式的结构化文本。模型在训练时见过大量Markdown语料,能自然地把“标题、列表、表格、代码块”这些格式标记还原出来。这一特性直接决定了富格式导出的可行性——如果OCR输出只有纯文本,后面再怎么转换也补不回结构与格式信息。
当然,VLM也不是万能的。它对小字号、低对比度、密集公式的识别精度可能不如专门的OCR引擎。但在我这个场景里,优先级是“还原结构与可读性 > 逐字逐句零误差”,VLM明显更合适。
1.3 这套方案适合谁、不适合谁
如果你需要处理的是扫描版技术手册、论文PDF、纸质单据、混合排版的文档,而且希望导出后还能继续编辑、检索、复制内容,那这套方案很值得试。数据全部留在本机,不用上传,隐私也说得过去。
反过来,如果你做的是生产级批量识别,每天几千页票据、证照,或者对单字识别精度有硬指标(比如身份证号、银行卡号必须100%正确),目前的VLM方案还不适合直接上生产。这种场景应该用传统OCR加规则校验,或者用VLM做粗排、再用专业OCR做精校。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模型选型与部署:Ollama跑视觉模型的取舍与加速
Ollama在模型分发和运行管理上做了大量封装,让我不用关心模型量化的底层细节,一条命令就能把模型拉下来用。但“拉下来”这一步在实操中比想象中更容易卡住。
2.1 几款视觉模型的横向对比
我实测过三款Ollama可以直接跑的视觉模型,结论如下。
| 模型 | 参数量 | 量化后显存占用 | 中文识别 | 版面理解 | 我的评价 |
|---|---|---|---|---|---|
| qwen2.5vl:7b | 7B | 约5.5GB | 很好 | 强 | 中文场景首选,表格和代码还原都稳 |
| llama3.2-vision:11b | 11B | 约8GB | 一般 | 中等 | 英文文档不错,中文掺英文会飘 |
| minicpm-v:8b | 8B | 约6GB | 较好 | 中等 | 端侧模型代表,显存紧张时可用 |
如果你主要处理中文技术文档,直接选qwen2.5vl:7b,别犹豫。它的中文语料覆盖好,对Markdown表格、代码块的输出结构也更规整。我这次手册里大量混合了中文正文和英文API签名,qwen2.5vl基本没有串行乱序的问题。
2.2 部署环节的三个必要动作
安装Ollama本身不复杂,Windows直接下安装包,Linux一条curl脚本。但有几个细节不处理会拖慢整个项目。
第一是安装包和模型下载速度。官方通道下载一个7B的视觉模型,在公司普通宽带环境下实测经常要等一个下午。我是把模型下载地址配置成国内可正常访问的镜像仓库地址来加速的,设置好之后一个7B模型几分钟就能拉完。Ollama的模型存储位置默认在系统盘,我先把OLLAMA_MODELS环境变量指向了独立数据盘,避免C盘爆掉,再配合镜像仓库的地址配置,下载速度和磁盘空间两个问题一起解决。
第二是确认模型真的跑在了GPU上。很多时候模型能跑,但走的是CPU推理,慢到怀疑人生。装好模型后先执行ollama ps查看当前加载状态,如果显示的是CPU而不是GPU,需要检查显卡驱动和Ollama版本。Ollama新版对AMD GPU也有ROCm支持,我手头一台AMD核显笔记本实测能跑,但显存识别和驱动匹配对得上版本才稳。如果是NVIDIA显卡,CUDA驱动装好基本就自动调用了。
第三是保持服务常驻。Ollama默认监听localhost:11434,进程退出模型会被卸载,下次调用重新加载模型要等十几秒。我的处理是把这个Ollama服务注册成系统服务,开机自动启动,配合OLLAMA_KEEP_ALIVE参数让模型驻留内存,避免频繁冷启动。
2.3 提示词模板与关键参数
视觉模型做OCR,提示词直接决定输出质量。我最终稳定使用的模板是:
text复制请识别这张图片中的全部文字内容,并保持原有的标题层级、段落、列表和表格结构。
- 标题使用Markdown的#、##、###标记
- 表格使用Markdown表格语法
- 代码块使用```标记,并保留原始缩进
- 不要输出任何额外解释,只输出识别结果
调用时注意两个关键参数。temperature一定要调到0.1左右,默认值偏高会让模型“发挥”出根本不存在的文字,尤其在表格内容上,幻觉问题会明显放大。num_predict设置成4096以上,因为一整页识别结果很容易超过模型的默认输出上限,截断后表格后半段就丢了。这两个参数单独调一个都不行,必须同时配合。
3. PDF智能解析链路:从页面渲染到识别结果
Ollama的视觉模型只接受图片输入,不接受PDF直接作为输入。所以PDF解析的第一步是把每一页渲染成图片,这一步做不好,后面模型再强也白搭。
3.1 为什么要先渲染成图片,而不是直接传PDF
有朋友问我,Ollama不支持PDF,那能不能先把PDF转成一张长图再丢给模型?可以,但不好。几百页的PDF合成长图,哪怕模型能接收,上下文和显存也扛不住,识别精度还会因为长图压缩而下降。最稳妥的粒度是“一页一图”,每张图片独立识别,再按页码顺序拼接结果。
我推荐用PyMuPDF(fitz)来做渲染,速度比pdf2image快很多,而且不需要系统预装poppler。核心代码很短:
python复制import fitz
doc = fitz.open("manual.pdf")
for i, page in enumerate(doc):
pix = page.get_pixmap(dpi=200)
pix.save(f"page_{i+1:03d}.png")
dpi参数是这里的关键。我最初用dpi=300渲染,单页图片接近4MB,不仅占用大量显存,推理耗时也翻倍。后来降到200,识别准确率几乎没有变化,速度和磁盘占用都舒服了。如果你的PDF里全是小字号注释,可以局部放大后再识别,而不是全局拉高dpi。
另外要先判断PDF是扫描版还是文字版。文字版PDF有文本层,直接用page.get_text()提取就是准确的,完全不需要OCR绕一圈。只有get_text()结果为空,或者提取内容乱码、缺字时,才走渲染和识别的链路。这个前置判断能帮你省掉一大半工作量。
3.2 多页PDF的分页与并发控制
单页识别没什么难度,真正容易翻车的是批量处理。
我第一版脚本是逐页串行调Ollama的API,一张图识别15到20秒,300页手册就是两个小时起步,完全不可接受。后来改成并发调用,但第一次把并发数开到8,6GB显存的卡直接OOM,Ollama服务当场挂掉。把并发降到2之后稳定运行。
这里我给一个可复用的并发拉取思路:
python复制from concurrent.futures import ThreadPoolExecutor
def process_page(img_path):
result = ocr_image(img_path, PROMPT)
return result
with ThreadPoolExecutor(max_workers=2) as pool:
results = list(pool.map(process_page, image_paths))
并发数不要拍脑袋定。先看单页识别时显存占用多少,再用“可用显存 / 单页显存占用”估算安全并发上限。我实测6GB显卡跑qwen2.5vl:7b,并发2就是极限,4就会在复杂页面触发OOM。
还有一点:Ollama的API调用要设置足够长的timeout。视觉模型单页推理很容易超过30秒,如果客户端默认超时时间短,会出现服务端还在算、客户端已经报错的情况。我用的是300秒,复杂表格页偶尔会跑到两三分钟,但不会再出现中途断连。
3.3 识别结果的后处理:从“能看”到“合规”
模型输出的Markdown不是拿来就能用的,有几个高频问题需要在代码层面修复。
表格语法是最容易出错的。模型偶尔会把表格行写成“|名称| 类型|”这种缺列数不一致的格式,导致后面转换Word时表格显示错位。我写了一个正则检查每个表格块的列分隔符数量,不一致的自动补列或丢弃坏行。
代码块缩进是另一个坑。PDF里的代码缩进到了模型手里有时会变成全角空格,或者在Markdown代码块内被错误包裹。我的处理是识别完成后单独抽离代码块,用原始等宽字符去重校对一遍,再拼回完整结果。
双栏版式偶尔会乱序。模型大部分情况下能按阅读顺序输出,但遇到文中间插图片、跨页表格这种复杂版式,会把右上栏的内容先输出,再回到左下栏。我的方案是把输出结果与书签目录对比,如果标题层级顺序明显异常,就把该页重新识别一次,第二次错误率会大大降低。
4. 富格式导出:从Markdown到可编辑文档
整套链路里,真正让这份成果“可用”的一步是导出。OCR识别结果如果不能变成可编辑的Word或HTML,那它和一堆无格式文本没有本质区别。
4.1 为什么中间格式一定选Markdown
在和模型对话之前我就确定了输出格式必须用Markdown。原因有三个。
第一,VLM输出Markdown最自然。模型训练语料里就有海量Markdown,让它直接输出其他格式(比如LaTeX、纯HTML)反而容易出错。
第二,Markdown是纯文本,方便做后处理和拼接。每一页识别完成后把结果字符串按序拼进一个大Markdown文件,中途断点续跑也方便,不需要复杂的数据结构。
第三,Markdown到docx、html、pdf都有成熟的转换工具,尤其是pandoc,转换效果稳定,样式控制能力也够。中间格式选得好,导出环节就是几条命令的事。
4.2 pandoc导出Word的完整流程
安装好pandoc之后,从Markdown转Word只需要一条命令:
bash复制pandoc output.md -o output.docx --toc --toc-depth=2
但直接用默认模板生成的Word丑到不想打开:中文字体不对,标题没有层次感,表格线很细。解决办法是准备一个参考docx模板。
bash复制# 先生成默认模板
pandoc -o template.docx --print-default-data-file reference.docx > custom-reference.docx
# 在Word里调整好标题字体、正文字号、表格样式后,再用它做参考
pandoc output.md -o output.docx --reference-doc=custom-reference.docx --toc
我在custom-reference.docx里把中文正文字体设成了宋体,标题设成黑体,表格加上边框,导出效果基本接近直接排版的文档。这份模板一次调好,之后所有文档都能复用。
4.3 HTML与PDF导出,以及图片资源处理
导出的HTML在Web页面里打印成PDF,往往比直接转PDF更可控。HTML的CSS能精确控制分页、页边距、字号,浏览器打印功能也比pandoc默认的PDF引擎灵活。
bash复制pandoc output.md -o output.html -s --metadata title="手册"
如果你在识别结果里嵌入了图片,pandoc转HTML时图片默认是相对路径引用。我当时用Markdown引用了图片文件,导出时要把图片文件和HTML放在同一目录,或者用--embed-resources参数把所有图片base64内嵌进HTML,这样单文件分发很方便。
bash复制pandoc output.md -o output.html -s --embed-resources --standalone
4.4 导出效果复盘
最终导出的Word里,标题层级、目录、表格都能正常编辑,代码块保留了等宽字体和缩进。我特意对比了一页包含表格、代码、多级标题的PDF,导出后除了表格列宽需要手动微调,其他内容结构和原PDF几乎一一对应。
有一类问题需要留意:跨页表格在Markdown中如果被拆成多个独立表格块,pandoc转换后会生成多个表格而不是合并成一个。我目前的处理是在导出前按表格标题和上下文做合并判断,涉及跨页时手动指定页码范围重新识别。这个问题自动化起来比较麻烦,算是当前链路的一个已知短板。
5. 踩坑记录:显存、超时、中文路径,逐一定位
这章记录几个我在实跑中真正花时间排查过的问题。每个问题我都按“现象、根因、解决”的顺序复盘。
5.1 显存不足与OOM的排查链路
现象是并发识别到第五页时,Ollama服务直接失去响应,ollama ps显示进程还在但显存被吃满,后续请求全部超时。
排查过程先看是不是量化不够低。qwen2.5vl:7b我用的默认量化,显存占用5.5GB。如果显卡只有4GB,就想办法用更低精度的量化版本,或者换更小的模型。我这边不是量化问题,把并发从4降到2之后,显存占用稳定在4GB左右,问题解决。
另一个容易忽略的点:Ollama的KEEP_ALIVE机制会让模型常驻显存,即使当前没有请求,显存也不会释放。我一度以为是内存泄漏,后来发现是OLLAMA_KEEP_ALIVE设得太长。如果同一台机器要跑多个不同模型,建议设置OLLAMA_KEEP_ALIVE为较短时长,避免多个模型同时在显存里占地。
5.2 引入Dify后遇到的请求超时问题
我把识别好的文本接进Dify做后续的知识库切片和问答时,遇到一个典型问题:Dify调用Ollama模型处理大页面,经常报“model request timeout”错误。
根因是Dify默认请求超时时间对视觉模型来说太短。识别一张复杂页面要一两分钟,Dify等不到99秒就中断了。解决办法是进入Dify的模型供应商设置,把Ollama的连接超时和读取超时都调大,我在实际项目里设成了600秒才完全稳定。另外,生产环境中尽量不要让Dify的工作流直接触发大模型的冷启动,先在外部脚本里预热模型,或者配置OLLAMA_KEEP_ALIVE避免模型频繁卸载重载,超时问题会明显减少。
5.3 中文文件名与路径编码问题
Windows下批量处理PDF时,如果输入目录或文件名包含中文,Python的open、fitz.open在个别环境里会报编码错误。
我最终的写法是不管当前系统默认编码,全部用pathlib.Path构造路径,同时给PDF和图片路径统一传UTF-8编码的字符串。核心逻辑很简单:路径用Path对象进行处理,不要再做手动字符串拼接。像这样:
python复制from pathlib import Path
pdf_path = Path("C:/Work/技术手册/新版手册.pdf")
out_dir = Path("C:/Work/output")
out_dir.mkdir(parents=True, exist_ok=True)
img_path = out_dir / f"page_{i:03d}.png"
这个习惯帮我避开了后续大量文件操作的兼容性问题。
5.4 模型输出不稳定时的兜底策略
即使参数调稳,模型偶尔还是会输出一段不相关的文字,比如识别到页眉时突然冒出一句“对不起,我无法识别该图片”。这通常是页面包含水印、印章或者模糊缩略图导致的。
我的兜底策略是给识别函数加一个简单校验:如果输出文本明显短于图片应有内容量(比如一页只有几十个字符),或者包含模型拒绝回答的固定句式,就把图片重新用更高dpi渲染一次,配合更高的局部裁剪再识别一遍。如果两次结果都不理想,就标记为“需要人工处理”,最后统一查看。这一步不能完全自动化,但能把人工复核量缩到很小。
6. 这套系统的边界与我的后续优化方向
项目跑通之后,我重新审视了一下这套方案的适用边界。它解决了一个真实痛点,但不是万能钥匙。
6.1 当前还搞不定的情况
手写体文档的识别效果依然不稳定。视觉模型对手写中文的识别受笔迹风格影响很大,同一个模型换个人写字,准确率可能从80%掉到50%。如果你需要处理大量手写单据,传统OCR加专门的手写识别模型仍然是更可靠的选择。
超复杂表格(比如多层合并表头、斜线表头、跨多栏的财务表格)虽然能识别,但导出的Word表格经常需要手动调整列宽和合并关系。VLM输出的Markdown表格是平面结构,原生的复杂表格层级关系在转换中会丢失。
超大PDF的批量处理时间成本依然偏高。一本300页的手册,在6GB显卡上全量处理大约需要两小时。如果只是偶尔处理几本,这个时间可以接受;但如果要搭建处理上千本PDF的生产服务,还是需要考虑更高显存的GPU和更精细的分页调度。
6.2 已排上日程的扩展
接下来我打算把识别结果接入RAG知识库,让这套OCR系统不只是“把扫描件变成文档”,而是进一步变成“可对话的领域知识库”。
另一个方向是收集模型识别失败或低置信度的页面,用标注工具对结果进行人工校正,形成一批高质量的OCR数据标注集。虽然目前VLM开箱即用的效果已经很能打,但遇到特定类型的扫描件时,用标注数据做一次微调,识别准确率还能再往上走一截。
最后想分享一个实际体会:这套系统最值钱的部分不是某个模型或某段代码,而是数据流。PDF渲染、页面调度、提示词设计、Markdown后处理、pandoc转换,每段都有很多看似不起眼但决定成败的小细节。把这些细节沉淀成脚本和模板,以后任何扫描版文档进来,只需要一个命令就能得到可编辑、可检索、可复用的电子版,这套东西的长期价值远远超过单次识别本身。
