起点:不是追求最强指标,而是先把“能稳定跑起来”变成第一目标
这个项目的起点很现实:没有 GPU,内存只有 4GB。在这种约束下,OCR 方案的目标不能定义成“跑最强模型”,而应该先定义成三件事:
- 进程能否稳定启动
- 单次推理能否在可接受的时间内完成
- 结果能否足够可控,且不暴露敏感识别内容
如果忽略这些约束,很多看起来很漂亮的方案都会在部署阶段直接失败。大模型 OCR 不是不能做,而是需要把“模型精度、显存/内存占用、推理时延、输入分辨率、上下文保持、失败兜底”放在同一张表里一起看。
所以这篇日志记录的不是一个“炫技型 OCR 方案”,而是一个资源受限场景下的工程化落地过程。
1. 先定约束,再选模型
为什么不能先看榜单再决定
OCR 模型的评测榜单通常强调准确率、复杂版面理解、图文混排、多语言能力,但这些指标默认的前提通常是:
- 有较充足的显存或内存
- 有 GPU 或高性能加速
- 推理引擎和模型格式完全匹配
而当前环境不满足这些假设。于是选型时真正要看的不是“谁分数最高”,而是:
- 模型是否能在 CPU 环境下跑
- 模型是否容易量化
- 输入图像是否能被压到一个合理的 token / pixel 预算
- 推理引擎是否能接受模型格式和上下文长度
- 出错时能否快速降级
选型原则
在这种场景里,模型选型的顺序应该是:
- 先确认任务边界:只做 OCR,还是还要做版面理解、字段抽取、结构化输出
- 再确认资源边界:内存上限、是否允许常驻模型、并发上限
- 再确认推理边界:能否量化、能否换引擎、是否支持流式输出或分段处理
- 最后才是准确率和结果美观度
这一套顺序很重要,因为它会直接影响后面的工程设计。在资源受限场景里,模型不是“选出来”的,而是“被约束筛出来”的。
2. 量化版本不是降级,而是让模型具备可部署性
为什么最终走向 Q4_K_M
在 4GB RAM 的前提下,未经量化的大模型 OCR 基本没有现实部署空间。即使模型本身参数量不夸张,推理时还会有:
- 权重加载内存
- KV cache
- 图像编码中间态
- tokenizer 与 runtime 额外开销
这些叠加起来,常常会把实际占用推到一个远高于预估的水平。
Q4_K_M 的意义不只是“压缩模型体积”,而是让模型进入一个可以被 CPU 进程稳定托管的状态。对我来说,量化不是妥协,而是把模型从实验室条件拉回到部署条件。
量化带来的工程收益
量化后,至少能获得三个直接收益:
- 常驻内存明显下降,进程更容易启动
- 推理时峰值压力下降,减少系统被 OOM 杀掉的概率
- 在长时间运行中更容易保持稳定,不必把全部希望押在某一次“刚好没撞内存”的启动上
但量化也不是白送的,它会带来:
- 个别字符、数字、标点的识别置信度波动
- 表格、密集文本、细小字号上的误差上升
- 对输入质量更加敏感
所以后续的像素、DPI 和兜底策略,实际上都是在给量化带来的误差补偿。
3. 推理引擎从官方方案切到 llama-server
官方推理链路的问题不在“能不能跑”,而在“是否适合当前环境”
官方指定的百度飞桨方案通常更贴近原始模型设计,也更容易在标准环境里得到更接近论文或官方演示的结果。但这类方案在实际工程里会遇到两个问题:
- 运行栈更重,依赖更多,内存更难压
- 某些部署环境里,启动、封装、适配和调试成本会明显上升
在“无 GPU + 4GB RAM”的约束下,我最后选择把推理引擎切到 llama-server,核心原因不是它“更强”,而是它更符合下面几个工程目标:
- 进程结构更轻
- 更容易做常驻服务
- 更容易控制上下文和请求边界
- 更容易通过统一服务层做超时、降级、重试
换句话说,这次替换不是从“官方路径”换到“非官方路径”,而是从模型绑定更强的路径换到工程控制力更强的路径。
为什么这一步是关键转折
一旦推理引擎切换,整个系统就不再围绕某个特定框架组织,而是围绕一个标准服务接口组织:
- 输入是一张经过规范化处理的图
- 输出是结构化结果或中间文本
- 上层服务不关心底层到底是 Paddle、llama.cpp,还是别的 runtime
这让后续的像素配置、预处理裁剪、失败回退、记录脱敏都可以独立演进,而不会和推理框架死绑定。
4. 输入不是“直接喂图”,而是先做分辨率预算
OCR 场景里,输入大小就是成本
很多人做 OCR 时会本能地想“尽量保留高清原图”。但在大模型推理里,这个直觉不总是对的。原因很简单:
- 分辨率越高,视觉 token 越多
- token 越多,内存和时延越高
- 输入越大,边界细节未必更好,反而可能放大噪声
所以这个项目里,像素和 DPI 不是一个单纯的图像美化参数,而是一个推理预算参数。
DPI 和像素策略的设计思路
我把输入处理拆成两层:
- 几何层:控制长边、短边、缩放比例,确保输入落在可预测的分辨率区间
- 版面层:根据图像类型决定是否保留整图、是否裁切区域、是否先做轻量增强
这里的关键不是把图像一味压小,而是找到一个“足够清晰、但不会让推理成本失控”的平衡点。
实际策略
策略上更接近这样一套思路:
- 小图:尽量避免过度下采样,防止字符被压糊
- 大图:优先按长边限制缩放,控制总像素
- 低 DPI 扫描图:适度提升有效对比度,但不做过度锐化
- 版面复杂图:优先切分为更小的推理单元,而不是把整页塞给模型
这类设计背后的原则是:对 OCR 来说,输入质量并不是越高越好,而是越稳定越好。
5. 推理逻辑不是一把梭,而是分层执行
为什么不直接“一次推完”
如果把所有图片都当成同一种输入,系统会很快遇到两个问题:
- 简单图被过度处理,浪费资源
- 复杂图处理不足,结果不稳定
因此推理逻辑更适合做成分层流程。
一套更稳的执行顺序
我采用的逻辑是:
- 先做输入预检查,判断图像是否可读、尺寸是否越界、是否有明显压缩噪声
- 再做轻量标准化,包括方向、缩放、像素预算控制
- 然后走主推理链路,尽量拿到结构化结果
- 如果主链路失败,再进入降级链路
- 如果降级链路仍失败,返回可解释的错误,而不是沉默失败
这种设计比“一个请求里硬凑完所有逻辑”更稳,因为它让失败点变得可观测。
6. 兜底机制比“识别成功率”更重要
为什么 OCR 项目必须要有兜底
在真实业务里,失败不是异常,而是常态的一部分。失败来源可能是:
- 图片太糊
- 图片太大
- DPI 不适合
- 模型置信度不足
- 推理服务临时不可用
- 内存压力太高导致服务被重启
如果没有兜底,系统会在这些普通失败上表现得像“完全不可用”。
兜底层次
这个项目里,兜底不是单一动作,而是分层的:
- 输入兜底:先把图像标准化到可推理边界内
- 引擎兜底:主引擎失败时,返回明确错误,不让调用方误以为已完成
- 结果兜底:当模型只能给出部分信息时,只输出置信度足够高的字段
- 服务兜底:服务不可用时返回重试建议或降级提示
最重要的一点是:兜底不是为了“假装成功”,而是为了让失败可控。
7. 隐私边界:只输出必要结果,不保留识别细节
为什么要特别处理隐私
OCR 天然会接触图片中的文本内容,而这类内容很容易包含:
- 个人信息
- 编号
- 联系方式
- 地址
- 内部业务字段
所以这个项目从设计上就应该遵守一个原则:
模型可以看见输入,但系统层不要多记录无关内容。
我实际采用的边界
在工程实现上,隐私边界可以做成几条硬规则:
- 不在日志里打印原图内容
- 不把完整识别文本写入调试日志
- 只记录必要的请求标识、耗时、状态码和错误类型
- 对输出内容做字段级最小化,只返回业务需要的部分
- 测试与排障时使用脱敏样本或结构化占位数据
这样做的意义不是“看起来更谨慎”,而是让这个系统即使被长期运行,日志和历史记录也不会无意间积累过多敏感信息。
8. 资源约束下的工程取舍
4GB RAM 场景的底层现实
在这个内存预算里,很多东西都不能随便加:
- 不能无限开并发
- 不能长时间缓存过多图片
- 不能把所有中间态都保留
- 不能把每个失败都重试很多次
所以工程取舍必须明确:
- 用更轻的量化模型
- 用更轻的推理引擎
- 用更严格的输入预算
- 用更少的中间缓存
- 用更明确的失败返回
这类系统真正追求的不是“极限精度”
在资源受限条件下,系统目标更像是:
- 可启动
- 可重复
- 可恢复
- 可解释
- 可控风险
这也是为什么这篇日志不把重点放在“某一条识别结果多漂亮”,而是放在整个推理链路为什么能稳定成立。
9. 结论:把 OCR 做成一个工程系统,而不是一个演示脚本
这次落地最重要的经验其实很简单:
- 模型选型要先服从资源约束
- 量化不是最后的补丁,而是部署前提
- 推理引擎要服务于工程控制,而不只是“官方推荐”
- 像素和 DPI 是预算问题,不只是画质问题
- 兜底机制决定系统能不能长期跑
- 隐私边界必须在日志和输出层先设计好
如果把这些点串起来,PaddleOCR-VL 1.5 的价值就不只是“识别文本”,而是让它在一个没有 GPU、只有 4GB 内存的环境里,仍然能以可控、可解释、可收敛的方式工作。
这才是这次开发日志真正想记录的部分。