Agent 光能打字不够,很多时候它得看得懂屏幕上的字——按钮文案、价格、报错信息、验证码提示。云端 OCR 有隐私和网络依赖,本地 OCR 才是 Agent 的长期姿势。这篇把本机 PaddleOCR 从装环境到调优再到服务化的完整实测记录下来,全部数据来自 CPU 6 核无独显机器的实跑。
环境:Windows / Python 3.13 venv / paddleocr 3.7.0 + paddlex 3.7.2 + paddlepaddle 3.2.0 / CPU 6 核无独显。2026-09 实测。
第一个坑:paddlepaddle 必须锁 3.2.0
系统只有 Python 3.13,Paddle 需要 3.x 轮子,所以建了独立 venv。装完 3.3.1 跑 PP-OCRv6 直接崩:NotImplementedError: ConvertPirAttribute2RuntimeAttribute not support ...(oneDNN 指令路径 bug)。降到 3.2.0 一切正常。而且脚本最顶部必须显式关 oneDNN,否则部分算子仍走那条路径:
python -m venv C:\paddleocr\venv
C:\paddleocr\venv\Scripts\pip.exe install paddleocr
C:\paddleocr\venv\Scripts\pip.exe install paddlepaddle==3.2.0 # 必须锁 3.2.0
import paddle
paddle.set_flags({'FLAGS_use_onednn': 0})
from paddleocr import PaddleOCR
ocr = PaddleOCR(lang='ch', use_doc_orientation_classify=False,
use_doc_unwarping=False, use_textline_orientation=False)
顺带两个接口变化:3.x 用 ocr.predict(img)(旧 ocr.ocr() 已废弃),返回 dict 含 rec_texts(文字)、dt_boxes(每行 4 点检测框,像素坐标)、rec_scores(置信度)。Windows 控制台是 GBK,识别结果带特殊字符会 UnicodeEncodeError,脚本里 sys.stdout.reconfigure(encoding='utf-8') 解决。
真实截图速度表:7 张图一次跑完
| 图片 | 分辨率 | 大小 | 文字数 | 字符数 | 耗时 |
|---|---|---|---|---|---|
| cloak_login | 1280×800 | 171KB | 17 | 94 | 3.98s |
| step1 | 1280×800 | 171KB | 18 | 95 | 4.07s |
| home_login | 1440×1000 | 321KB | 129 | 758 | 15.25s |
| rank_cover | 900×1883 | 221KB | 100 | 1043 | 13.17s |
| step_now | 1280×800 | 764KB | 191 | 1425 | 20.38s |
| GPU 天梯图 | 2160×3382 | 746KB | 75 | 1400 | 24.29s |
| detail_full (12MB) | 1265×20529 | 12446KB | 64 | 438 | 19.74s |
中英文混合识别很准(京东、M.2固态硬盘、大模型显卡 AI 算力天梯 都正确)。两个规律:
- 速度主要由像素量和文字框数决定,与文件体积弱相关——12MB 长图 19.7s,比 746KB 的天梯图(24.3s)还快
- 默认 max_side_limit=4000:超长边自动缩小,所以超大文件不会按比例爆炸变慢
- 图太小会漏检:同图缩到 0.25×(320×200)只检出 2 个文字 vs 原图 191 个
两个线性规律:像素量 & 文字量
控制变量跑了两组,用来预测耗时很实用。先看同一张图缩放(文字密度不变):
| 缩放 | 分辨率 | 百万像素 | 文字数 | 耗时 |
|---|---|---|---|---|
| 0.25× | 320×200 | 0.06 | 2 | 1.62s |
| 0.5× | 640×400 | 0.26 | 90 | 9.67s |
| 1.0× | 1280×800 | 1.02 | 191 | 20.63s |
| 2.0× | 2560×1600 | 4.10 | 206 | 28.89s |
耗时与百万像素近似线性(0.06→4.10MP,耗时 1.6s→28.9s),但过小会丢字。再看固定 1200×1600 改变文字行数:
| 行数 | 识别文字 | 耗时 |
|---|---|---|
| 5 | 5 | 4.46s |
| 20 | 20 | 7.26s |
| 50 | 38 | 10.86s |
| 100 | 38 | 11.40s |
识别阶段耗时随文字框数近似线性增加;50 行后漏检(38 个),说明文字太密时检测环节开始丢框——Agent 读图前先把图放大一点更稳妥。
白赚 10~15%:关掉 3 个辅助模型
文档方向分类、UVDoc 弯曲矫正、文本行方向分类这 3 个辅助模型只对旋转/扫描件有用,普通截图(竖直、无弯曲)关掉零影响,反而更快、召回更好:
| 配置 | 3 张图总耗时 | 初始化 | 识别文字数 |
|---|---|---|---|
| 原版(全开) | 48.8s | 1.94s | 191/129/100 |
| 关掉 3 个辅助模型 | 44.0s | 1.31s | 202/152/100 |
快约 10–15%,识别文字反而更多(191→202、129→152,召回更好)。推荐配置见上面的初始化代码块。
试过但没用的开关
- 批处理
predict([img1,img2,...]):CPU 单流,无加速(44.2s ≈ 分别跑) - text_det_limit_side_len 调小:只对超巨图有效,普通截图帮助极小还可能漏小字(实测 960 → 45.2s)
- 线程数:默认已用满 6 核,再调无意义
真正的提速途径
- NVIDIA GPU + paddlepaddle-gpu:可快 5–20 倍。本机只有 AMD 核显,Paddle 在 Windows 不支持,故不可用
- 更轻的 mobile 模型:约再降一半,但有轻微精度折损;3.x 封装没暴露模型名参数,暂未采用
给 Agent 用:常驻服务化(9.9s → 3.0s)
Agent 要反复读图,每次都新开进程 + 重新加载模型(~6.5s)根本不行。改成模型常驻内存的 HTTP 服务,加载只付一次,之后每次只付推理:
| 场景 | 改造前(每次新进程) | 改造后(常驻服务) |
|---|---|---|
| 小图 1920×1080 单次识别 | ~9.9s(含 6.5s 模型加载) | ~3.0–3.4s(纯推理) |
| 12MB 大图 连续 3 次 | 每次均含 6.5s 加载 | 9.51s / 9.23s / 9.24s(无加载尖峰) |
# 启动常驻 OCR 服务(模型加载约 2.6s,之后常驻内存)
$env:FLAGS_use_onednn = "0"
C:\paddleocr\venv\Scripts\python.exe C:\mcp-server\engines\ocr_service.py
# 健康检查
Invoke-RestMethod http://127.0.0.1:8200/health
更进一步,这个服务通过 MCP 暴露给本机 opencode 和局域网其他机器:ocr_extract_text(提取)、ocr_locate_text(定位关键词)、ocr_visualize(画框)、ocr_smart_region(先定位再裁局部)。
region 局部识别:再快 5.8 倍
推理耗时 = 像素量 + 文字框数,只识别局部区域就能大幅砍掉两者。实测(模型已热):
| 图片 | 全图 | 局部 region | 提速 |
|---|---|---|---|
| 大图 12.7MB (1265×20529) | 10.2s | [0,0,900,700] → 4.9s | ~2× |
| 小图 1920×1080 | 3.7s | [0,0,400,300] → 0.6s | ~5.8× |
# 方式 A:直接给 region(原图像素坐标)
ocr_extract_text(image_path="x.png", region=[x, y, w, h])
# 方式 B:给关键词,工具自动 locate 并裁出匹配框周围 margin 区域再识别
ocr_smart_region(image_path="x.png", keyword="价格", margin=20)
典型用法:先全图 locate 找到大致区域,再只裁小图反复识别某一块(价格、品牌、按钮),既快又省。region 坐标基于原图,无需考虑缩放。
跨机调用:base64 免共享文件系统
OCR 服务只能读本机文件路径。局域网里的其他机器(如 NAS)传路径会找不到文件——把图片 base64 编码传过去,服务端解码成临时 PNG 再识别:
import base64, json, urllib.request
img_b64 = base64.b64encode(open("/vol2/.../some.png", "rb").read()).decode()
req = urllib.request.Request("http://<LAN-IP>:8200/extract",
data=json.dumps({"image_base64": img_b64}).encode(),
headers={"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req, timeout=60)))
注意:base64 体积约增 33%,超大图(>~15MB 原图)要留意 HTTP 请求体上限;MCP 端点无认证,仅适合可信内网。
资源占用:模型常驻的代价
| 服务 | 工作集(物理内存) | 提交内存 | 说明 |
|---|---|---|---|
| local-tools-mcp (:8190) | ~86 MB | ~72 MB | 很轻 |
| ocr-service (:8200) | ~616 MB | ~1.83 GB | 模型常驻代价 |
任务管理器默认「内存」列显示工作集 ≈616MB;「提交大小」列才看得到 ~1.8GB。工作集会随系统内存压力被 Windows 动态裁剪,但模型并未卸载。两个服务都用 nssm 装了开机自启。
总结
- 装环境就一个坑:paddlepaddle 锁 3.2.0 + 脚本顶部关 oneDNN
- 速度可预测:像素量 + 文字框数 ≈ 线性;Agent 读图前放大一点能防漏检
- 白赚 10~15%:关掉 3 个辅助模型,召回反而更好
- Agent 正确姿势:模型常驻服务 + region 局部识别 + base64 跨机,单次可到亚秒级
