1. 项目整体设计与技术选型
1.1 为什么非要“全本地”
先交代一下背景。去年我接了一个偏传统的项目:企业内部报销单据自动录入系统。需求看着很常规,图片上传、OCR识别、字段录入、归档。但客户在需求沟通会上补了一句:服务器只能在公司内网,所有单据数据不允许出机房,更不能走任何外部接口。
这句话基本就把方案钉死了。云端OCR不支持,云端大模型API更不可能。要么放弃识别,全部人工录入,要么就在本机或者内网服务器上把识别和智能抽取全部自己搞定。我选择了后者,这也是这篇文章想聊的核心:把OCR和大模型都放回本机的工程做法。
这类需求并不是少数。医疗病历、合同档案、制造业质检单、财务凭证,很多行业的数据敏感度极高,或者网络环境本身就受限。再加上长期调用云端API的成本并不低,按张计费、按token计费,跑一个月的对账下来可能比买一张显卡还贵。离线方案前期会累一点,但长期看,稳定性、成本、数据可控性都是明显占优的。
当然,“全本地”并不是一个技术炫技的决定,而是由需求倒推出来的选择。数据不出内网、识别不能中断、单张图片处理成本要可控,这三条定下来,环境和路线就清晰了。
1.2 离线方案的整体链路:OCR 与大模型的分工
整套系统的链路大概是这样的:图片先进OCR引擎,转成文本,再交给本地大模型做字段抽取和结构化输出。为什么不让大模型直接看图片?因为多模态模型在本地部署的显存开销更大,对硬件要求高,而且可控性不如“先OCR、再LLM”两步走。
OCR负责把图像里的文字“抠”出来,这一步解决的是“有没有这段文字、在哪个位置”的问题。大模型负责理解语义,比如从一段发票文本里抽出“发票号”“开票日期”“价税合计”,这一步解决的是“这些文字表达的是什么业务含义”的问题。两步解耦之后,每一段的模型都可以独立替换、调优,排查问题也方便得多。
我见过不少方案里直接让大模型做OCR,效果其实也能跑,但在工程上并不划算。小模型OCR一秒钟能处理好几张图,纯CPU压力也不大;大模型哪怕只做文本理解,7B参数级别的模型也要吃几个G内存。如果让大模型同时干识图和理解的活,一次请求的响应时间会从两三秒变成十几秒,生产环境很难接受。
1.3 适用场景与选型边界
这套离线方案最适合的场合是“固定机器、固定网络环境、数据敏感、量级可控”的场景。比如企业内部知识库、档案数字化、桌面端小工具、工控机上的信息录入。在这种环境下,本地部署的收益非常明显:数据不出门、响应速度快、没有按次计费。
但也要说清楚边界。如果业务量特别大,比如日均几十万张图片,而且对并发吞吐要求极高,那一张普通显卡撑不起这个量,需要上GPU集群,这时候本机部署的成本优势就不明显了。另外,如果识别场景覆盖超多种语言、超复杂版面,需要持续用大量数据迭代模型,纯离线自维护的负担也会变大。
所以在选型阶段一定要先做一轮真实的样本测试。用客户提供的几十张真实单据跑一遍PaddleOCR和Tesseract,看看准确率差距有多大,再估算一下模型在目标机器上的推理速度。选型这件事,最怕的就是拍脑袋。
2. OCR 引擎本地化部署:选型、安装与调优
2.1 主流离线 OCR 引擎怎么选
离线OCR,说白了市面上就那几个选择。我把它们放在一张表里对比过,各有各的脾气。
| 引擎 | 部署方式 | 中文识别效果 | 离线友好度 | 典型场景 |
|---|---|---|---|---|
| Tesseract | 命令行 / C++库 | 一般,复杂版面偏弱 | 需要手动下载语言包 | 印刷体、英文文档 |
| PaddleOCR | Python / C++ / 服务化 | 好,支持多语言和方向分类 | 模型文件可离线缓存 | 中文单据、屏幕截图、多语言 |
| RapidOCR | Python / ONNX | 好,基于PaddleOCR模型 | 无外部框架依赖,安装轻量 | 生产环境集成,跨语言调用 |
| EasyOCR | Python / PyTorch | 中上 | 依赖较重,模型体积大 | 快速原型验证 |
| 商业SDK | 多种 | 优 | 联网或私有化授权 | 预算充足且不想维护模型 |
Tesseract是老牌选手,胜在历史悠久、轻量、跨平台。但说实话,中文复杂单据的识别效果,PaddleOCR是明显领先的。差距集中体现在:中文标点混排、表格线干扰、印章遮挡、斜体字、竖排文字这些场景。PaddleOCR这些坑基本都填过了,Tesseract需要你自己花很多精力去做图像预处理。
RapidOCR是PaddleOCR模型的ONNX运行时版,它不依赖PaddlePaddle框架,部署链路短很多,非常适合生产环境。如果项目里不打算引入整套Paddle框架,只想要一个轻量的OCR能力,RapidOCR是个不错的中间选项。
我的建议是:中文场景、复杂版面优先PaddleOCR;如果只识别英文或者印刷清晰的文档,Tesseract完全够用;如果在意部署体积且不想处理Paddle框架的依赖关系,RapidOCR值得试。
2.2 离线环境安装:以 PaddleOCR 为例
在能联网的开发机上安装PaddleOCR并不复杂,核心就三步:
# 1. 安装 PaddlePaddle,CPU 版即可满足大多数离线场景 pip install paddlepaddle # 2. 安装 PaddleOCR pip install paddleocr # 3. 验证版本 python -c "from paddleocr import PaddleOCR; print('ok')"但真正麻烦的是完全隔离的内网环境。你不能在目标机器上直接pip install,得提前准备好离线安装包。我当时的做法是:在一台有网络的机器上,用pip download把项目所有依赖全部拉下来,做成一个wheelhouse目录,再整体拷进内网。
pip download paddlepaddle paddleocr -d ./wheelhouse -r requirements.txt然后在内网机器上执行:
pip install --no-index --find-links=./wheelhouse -r requirements.txt这里有个重要细节:别忘了下载PaddleOCR运行时需要的模型文件。PaddleOCR在首次运行时会尝试从网上下载检测模型、识别模型、方向分类器。在离线环境里这个动作一定会失败,所以必须提前把模型下载好,然后通过模型目录参数指定。
from paddleocr import PaddleOCR ocr = PaddleOCR( use_angle_cls=True, lang="ch", det_model_dir="./models/ch_PP-OCRv4_det_infer", rec_model_dir="./models/ch_PP-OCRv4_rec_infer", cls_model_dir="./models/ch_ppocr_mobile_v2.0_cls_infer", use_gpu=False, )把模型文件放固定目录、然后在代码里写死这个路径,是离线项目里一个非常好的习惯。这样模型文件跟着项目走,不会出现“换一台机器就跑不起来”的尴尬。PaddleOCR不同版本API有差异,具体参数名以官方文档为准,但这个思路是通用的。
2.3 别小看的识别质量:预处理、语言包与模型目录
很多人以为OCR识别率低就是模型不行,其实大部分情况下是图像预处理没做好。同一张照片,不处理直接丢给PaddleOCR,和先做灰度化、二值化、矫正倾斜之后再识别,准确率能差出好几个百分点。
我总结了一套比较实用的预处理流程:
- 图像转灰度,去掉色彩干扰,尤其针对彩色背景的票据。
- 自适应阈值二值化,让文字和背景的边界更清晰。
- 检测倾斜角度,用仿射变换把图片摆正。Tesseract命令行里有--psm参数,PaddleOCR有方向分类器。
- 影响识别精度的另一个参数是分辨率。扫描件建议不低于300dpi,手机拍出来的照片如果太模糊,可以先做一些锐化处理。
如果用的是Tesseract,还需要额外注意语言包问题。默认安装只带了英文语言包,识别中文必须下载chi_sim.traineddata,放进tessdata目录,然后指定:
tesseract input.png output -l chi_sim不指定语言包,或者语言包版本和Tesseract版本不匹配,最常见的表现就是输出一堆乱码和空行。这个坑我踩过,Tesseract的tessdata版本一定要和主程序版本对应,否则不会报错,但识别结果完全没法用。
关于模型目录,其实还有一个更深层的使用技巧:如果你发现PaddleOCR自带的通用模型在你这个特定领域里识别率不够,可以通过微调或者添加自定义字典来改善。比如某些医疗单里的药品名称、某些企业内部的英文缩写,不在模型字典里,识别出来就会缺字或错字。在PaddleOCR的rec_model_dir里替换成针对特定领域微调过的识别模型,或者设置自定义词典,都能有效提升准确率。
3. 大模型本地部署:Ollama + GGUF 的工程组合
3.1 为什么是 Ollama 而不是直接裸跑
大模型本地化首先要解决“模型怎么跑起来”的问题。我自己尝试过直接用llama.cpp编译main二进制跑GGUF模型,也试过用transformers库加载HuggingFace格式。说实话,这些方法都能用,但工程化成本很高,依赖管理、内存分配、服务化接口,每一个环节都需要自己手写。
最后我选择了Ollama。原因有三点:
第一,Ollama把GGUF模型的管理做得非常优雅。模型拉取、版本管理、量化格式选择,几乎一条命令搞定。你不用自己纠结下载路径、模型文件命名,Ollama会统一管理。
第二,它自带一个本地HTTP服务。Python通过requests直接调用就行,也可以使用官方Python库,完全不需要自己额外写一套推理服务。对工程集成来说,太省事了。
第三,它在资源占用和并发处理上做了不少优化。模型加载到显存后可以常驻,不会每次请求都重新加载一次。对需要频繁调用的场景,这个优化非常关键。
如果你问能不能直接用llama.cpp裸跑,答案是可以,但你会重新发明一堆轮子。Ollama本质上是把llama.cpp等推理内核封装了一层,你日常关心的是模型调度和接口,而不是内存换入换出的细节。
3.2 模型量化等级选择与硬件匹配
大模型文件动不动就十几G甚至几十G,直接跑原始FP16权重,一张普通显卡根本塞不下。所以本地部署基本都会用GGUF量化格式,你确定要跑7B、14B还是32B模型时,先要理解量化等级。
| 量化等级 | 相对原始精度 | 文件体积(7B模型) | 硬件要求 | 适合场景 |
|---|---|---|---|---|
| Q2_K | 损失明显 | 约3G | 4G显存/6G内存 | 只做快速测试 |
| Q4_K_M | 损失较小 | 约4.7G | 6G显存/8G内存 | 大多数生产场景推荐 |
| Q5_K_M | 损失更小 | 约5.4G | 8G显存/12G内存 | 对输出质量要求较高 |
| Q8_0 | 几乎无损 | 约7.5G | 10G显存/16G内存 | 硬件条件好的场景 |
我项目里用的是Qwen2.5系列7B模型,配Q4_K_M量化。这个组合在绝大多数CPU和GPU机器上都能跑起来,输出质量也在可接受范围内。后来在客户那边试了14B的Q5_K_M,效果确实更好,但显存占用直接翻了倍,需要一次性投入更多硬件预算。
硬件搭配上,我的经验是:纯CPU跑7B Q4模型可以做,但速度会很折磨人,大概每秒四五次token,一段话要等十几秒。如果有GPU,哪怕是消费级的RTX 3060 12G,体验会有质的提升。显存告急的时候,可以设置OLLAMA_NUM_PARALLEL=1来减少并发资源占用,把上下文长度限制在2048以内,这些都是调优的常规手段。
3.3 离线迁移模型的三个关键动作
离线部署大模型,最核心的工作就是“把模型从有网机器搬到内网机器”。很多人在这里卡住,总以为模型文件复制过去就能用。理论上确实如此,但有几个细节必须处理好。
第一个动作,在能联网的机器上拉取模型。
ollama pull qwen2.5:7b-instruct-q4_K_M这里建议明确指定量化标签,不要只拉默认版本。默认版本通常是体积更大的,不一定是量化过的。
第二个动作,找到模型目录并完整复制。Ollama的模型文件都存放在模型目录下,Linux默认是~/.ollama/models,Windows是C:\Users\用户名.ollama\models。如果之前设置过OLLAMA_MODELS环境变量,以那个路径为准。整个models目录拷到内网机器的同样位置,结构保持一致。
第三个动作,在离线机器上验证。先在命令行执行ollama list,看模型是否被正确识别。如果看不到,检查一下OLLAMA_MODELS环境变量是否指向了正确的目录。然后真正跑一轮推理,确认模型能成功加载并输出内容。
提示:复制模型文件到内网机器时,最好做一次SHA256校验。模型文件几个G,U盘或网络传输过程中可能损坏。用哈希比对确认内网机器上的文件和源文件完全一致,能避免很多奇怪的问题。
4. 把 OCR 和 LLM 串成一条完整流水线
4.1 从图片到结构化 JSON 的实现
离线OCR和大模型各自部署好之后,最关键的工程点就是如何把它们串成一条完整的流水线。我不建议把这两步写死在同一个函数里,更合理的方式是拆成独立的服务或独立模块,中间通过文本和JSON传递数据。
我项目的实际流程是这样的:图片通过Web上传或者本地目录监听进入系统,先调用OCR模块输出识别文本。PaddleOCR返回的结果是包含坐标和置信度的,我会先过滤掉置信度低于0.6的识别块,再把每个文本块按位置拼接成段落文本,输入到大模型处理。
下面是简化后的Python代码框架:
import requests import json from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang="ch") def image_to_text(image_path): result = ocr.ocr(image_path, cls=True) lines = [] for page in result: for line in page: text = line[1][0] conf = line[1][1] if conf >= 0.6: lines.append(text) return "\n".join(lines) def text_to_fields(raw_text): prompt = build_prompt(raw_text) resp = requests.post( "http://127.0.0.1:11434/api/generate", json={"model": "qwen2.5:7b-instruct-q4_K_M", "prompt": prompt, "stream": False}, timeout=120, ) data = resp.json() return json.loads(data["response"])OCR结果先拼成文本,再交给大模型。这样做的最大好处是,你可以单独测试OCR模块和大模型模块,出问题时不用两头排查。生产环境里第一步OCR的每一步都要记录日志,包括图片文件名、识别时长、识别文本内容。这样如果某个字段抽错了,你可以回溯是OCR认错了字,还是大模型理解错了。
4.2 Prompt 设计:让 LLM 稳定输出目标字段
大模型在离线环境里同样存在“提示词敏感”的特点,同一个模型,prompt写得好不好,输出结果的稳定性差别很大。我最早直接用“请提取以下文本中的发票号、日期、金额”这样简单的指令,结果模型经常输出一长段解释,或者字段名不一致。
后来我改成了“任务说明+输出格式+示例+输入内容”的结构化提示词,稳定了很多:
你是财务单据信息抽取助手。请从下面的OCR文本中提取以下字段:发票号、开票日期、金额、销售方名称、购买方名称。 只输出JSON对象,不要输出任何解释或多余文字。字段名严格使用英文键: {"invoice_no": "", "invoice_date": "", "amount": "", "seller": "", "buyer": ""} 如果某个字段无法确定,填null。 OCR文本如下: {ocr_text}这样写之后,模型基本能稳定输出合法JSON。再配合Ollama接口里的format参数,可以进一步约束输出格式:
resp = requests.post( "http://127.0.0.1:11434/api/generate", json={ "model": "qwen2.5:7b-instruct-q4_K_M", "prompt": prompt, "stream": False, "format": "json", "options": {"temperature": 0.1, "num_predict": 512}, }, )Ollama支持JSON mode,会让模型尽量生成合法的JSON结构。再加上把temperature调低,比如0.1,生成结果就很少飘了。我在实测里,结构化抽取字段的稳定率能从最初的70%提升到接近95%。
4.3 性能预算与并发控制
整套流水线跑通之后,还有一个绕不开的问题:性能。OCR一张普通A4扫描件,CPU模式下大概需要1到3秒。大模型生成一段100字的JSON输出,在消费级GPU上大概需要2到5秒。如果前端同步等待完整个流程,用户点击一次“识别”按钮,要等将近10秒才能看到结果,体验非常差。
我当时的做法是引入一个任务队列。图片上传后立刻返回一个任务ID,后端用队列异步处理,前端轮询任务状态。这样即使识别流程需要十几秒,用户也不会觉得系统卡死。如果业务场景允许批量处理,比如每天晚上批量扫描归档一批旧档案,那可以把这张流水线封装成一个批量任务,定时执行。
并发控制也要提前设计。Ollama默认会尝试调度模型,但不代表你可以无限并发。在一张显存有限的GPU上,并发请求过多会导致显存溢出或者任务排队时间暴涨。我通过OLLAMA_NUM_PARALLEL参数控制并发请求数量,并预留一个信号量,让OCR任务和大模型任务相互不挤占资源。
5. 常见问题与排坑实录
5.1 OCR 环节的高频坑
OCR这块我在实际项目中踩了不少坑,挑几个最典型的说说。
PaddleOCR首次运行自动下载模型失败,这是离线环境里出现频率最高的报错。解决办法就是提前下载模型文件,在代码里显式指定模型目录。前面已经给过代码示例,这里不再重复。
中文标点丢失和数字混淆是另一个常见问题。尤其是金额里的“1”和“7”、“0”和“8”,在低分辨率图片里很容易认错。我的经验是,涉及关键字段的数据,尽量让图片保持300dpi以上的清晰度,同时在预处理阶段用图像增强让字符边缘更锐利。如果模型输出置信度低于0.8的字段,建议人工复核一道。
还有一类问题是方向分类器带来的“上下颠倒”。有些手机拍的图片带了EXIF旋转信息,PaddleOCR方向分类器如果没启用,文字就会被倒着识别。解决办法是保持use_angle_cls=True,不要为了省一点推理时间关掉它。
Tesseract的坑主要集中在中文语言包和PSM参数上。PSM默认的自动分页模式对单行文本识别效果很差,需要手动指定--psm 7或--psm 6。另外,如果图片背景是深色而文字是浅色,需要先做颜色反转,不然识别率会惨不忍睹。
5.2 大模型推理环节的高频坑
大模型推理环节的问题,多半集中在资源、版本和输出质量这三个方面。
显存溢出是最常见的。加载一个7B Q4模型去买之前,先用nvidia-smi看看显存剩余。加载模型本身需要接近5G显存,再加上上下文窗口和生成时的KV Cache,7B模型实际占用可能到6到7G。显存不够时,要么缩小上下文长度,要么换更低的量化等级,要么干脆用CPU推理。
模型加载特别慢的问题,通常出在磁盘IO上。如果你把模型文件放在机械硬盘里,加载一个5G模型可能需要一分钟以上,而放在SSD上只需要十几秒。所以尽量把模型目录放在SSD上,这是成本最低的性能优化手段。
输出格式不稳定的问题,我在4.2节已经提过解法:用结构化提示词、JSON mode、低温采样。如果模型还是偶尔输出非JSON内容,可以在代码里加上JSON解析容错,解析失败时重新请求一次。实测中,重试一次的成功率已经非常高。
5.3 工程集成与离线依赖问题
工程集成阶段最烦人的是离线依赖安装。除了Python的wheel包,可能还要处理Node.js离线安装、Gradle离线依赖、系统级的安装包等。核心思路是相通的:提前在有网环境下把依赖全部拉取下来,做成一个离线资源包,再拷贝进内网。
Python用pip download加--no-index的方式,Node.js用npm pack离线缓存,Gradle用offline模式加预置依赖仓库。这些做法的本质都一样:把“联网解析依赖”改成“本地从缓存中解析”。
Windows服务自启动和Linux systemd服务也是容易忽视的点。Ollama在Linux上可以配置systemd服务,开机自启;在Windows上可以通过计划任务或NSSM把ollama serve注册为系统服务。否则每次服务器重启,你都得手动去启动模型服务,很容易被遗忘。
还有一个非常容易踩的坑是服务端口占用。Ollama默认端口是11434,如果这台机器还跑着其他Web服务占用了这个端口,Ollama起不来。启动前先用netstat检查端口占用,或者在配置里改OLLAMA_HOST指定其他端口。
export OLLAMA_HOST=0.0.0.0:11435改完端口后,所有调用代码里的地址都要同步改,这个不难,但要记得统一维护一个配置文件,不要散落在各处。
6. 个人落地经验与补充建议
6.1 离线项目一定要学会“预缓存”
做离线项目,最大的敌人就是“在线依赖”。模型文件、安装包、Python依赖、jar包、npm包,这些东西如果在部署现场才去下载,任何一个环节出问题都会卡住整个上线进度。我在这个项目里养成一个习惯:准备一个专门的“离线资源包”,把所有需要的东西提前下载好,并且登记SHA256校验值,内网部署时逐步校验。
资源包的目录结构大概是这样的:
offline-package/ ├── models/ │ └── ocr/ # PaddleOCR 模型文件 ├── llm/ │ └── ollama-models/ # GGUF 模型文件 ├── wheels/ # Python 依赖包 ├── installers/ # 系统级安装包 └── checksums.txt # 所有文件的 SHA256 校验值这样组织的好处是,每次在新环境部署,只需要按清单执行安装步骤,遇到问题也能快速定位是哪个文件缺失或损坏。这个习惯后来帮我解决了至少三次部署事故。
6.2 我的硬件配置与最终效果
我自己主要用来开发和压测的机器配置是:CPU是i5-12400,内存32G,显卡是RTX 3060 12G。这套配置跑Qwen2.5 7B Q4量化模型,配合PaddleOCR做完整流水线,单张图片从上传到输出结构化JSON,大约需要6到8秒,其中OCR占1到2秒,大模型占4到5秒。
如果你手里没有独立显卡,其实也能跑,只是速度会慢不少。纯CPU推理7B模型,大概每秒只能生成3到5个token,完整抽取一段100字的字段,可能要等二三十秒。但如果是内部工具、批次处理,这个速度也不是完全不能接受。
如果预算允许,我更推荐一步到位上16G显存的显卡,比如RTX 4070 Ti Super或者RTX 4080。16G显存可以轻松跑14B模型,还能留出更多KV Cache空间,长文本处理能力强一大截。对于字段抽取这种场景,14B模型在复杂单据上的理解能力提升是肉眼可见的。
6.3 一点真心建议
最后想分享一个我自己的感受。离线OCR加上本地大模型这套方案,技术门槛并没有想象中那么高,真正难的是把各个环节的工程细节打磨好。模型版本锁定、依赖离线化、日志可回溯、失败可重试,这些才是生产环境和demo之间的本质区别。
不要一上来就追求大模型效果完美,先把小模型、简单模型跑通全链路,再去替换更好的模型。这样每一步的变量都少,出了问题好排查。我在项目里就是把整个流水线搭好之后,才把OCR模型从mobile版本换成了server版本,把大模型从7B换到14B去对比效果。每一步都留下性能记录,效率会高很多。
这套方案目前已经稳定跑了几个月,客户那边也没再提过“要不要联网识别”这类问题。数据始终留在自己机器上,这本身就是一种竞争力。如果你也在纠结怎么把OCR和大模型落地到离线环境,希望这篇文章能帮你少走一些弯路。