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库一般复杂版面偏弱需要手动下载语言包印刷体、英文文档PaddleOCRPython / C / 服务化好支持多语言和方向分类模型文件可离线缓存中文单据、屏幕截图、多语言RapidOCRPython / ONNX好基于PaddleOCR模型无外部框架依赖安装轻量生产环境集成跨语言调用EasyOCRPython / PyTorch中上依赖较重模型体积大快速原型验证商业SDK多种优联网或私有化授权预算充足且不想维护模型Tesseract是老牌选手胜在历史悠久、轻量、跨平台。但说实话中文复杂单据的识别效果PaddleOCR是明显领先的。差距集中体现在中文标点混排、表格线干扰、印章遮挡、斜体字、竖排文字这些场景。PaddleOCR这些坑基本都填过了Tesseract需要你自己花很多精力去做图像预处理。RapidOCR是PaddleOCR模型的ONNX运行时版它不依赖PaddlePaddle框架部署链路短很多非常适合生产环境。如果项目里不打算引入整套Paddle框架只想要一个轻量的OCR能力RapidOCR是个不错的中间选项。我的建议是中文场景、复杂版面优先PaddleOCR如果只识别英文或者印刷清晰的文档Tesseract完全够用如果在意部署体积且不想处理Paddle框架的依赖关系RapidOCR值得试。2.2 离线环境安装以 PaddleOCR 为例在能联网的开发机上安装PaddleOCR并不复杂核心就三步# 1. 安装 PaddlePaddleCPU 版即可满足大多数离线场景 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_clsTrue, langch, 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_gpuFalse, )把模型文件放固定目录、然后在代码里写死这个路径是离线项目里一个非常好的习惯。这样模型文件跟着项目走不会出现“换一台机器就跑不起来”的尴尬。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损失明显约3G4G显存/6G内存只做快速测试Q4_K_M损失较小约4.7G6G显存/8G内存大多数生产场景推荐Q5_K_M损失更小约5.4G8G显存/12G内存对输出质量要求较高Q8_0几乎无损约7.5G10G显存/16G内存硬件条件好的场景我项目里用的是Qwen2.5系列7B模型配Q4_K_M量化。这个组合在绝大多数CPU和GPU机器上都能跑起来输出质量也在可接受范围内。后来在客户那边试了14B的Q5_K_M效果确实更好但显存占用直接翻了倍需要一次性投入更多硬件预算。硬件搭配上我的经验是纯CPU跑7B Q4模型可以做但速度会很折磨人大概每秒四五次token一段话要等十几秒。如果有GPU哪怕是消费级的RTX 3060 12G体验会有质的提升。显存告急的时候可以设置OLLAMA_NUM_PARALLEL1来减少并发资源占用把上下文长度限制在2048以内这些都是调优的常规手段。3.3 离线迁移模型的三个关键动作离线部署大模型最核心的工作就是“把模型从有网机器搬到内网机器”。很多人在这里卡住总以为模型文件复制过去就能用。理论上确实如此但有几个细节必须处理好。第一个动作在能联网的机器上拉取模型。ollama pull qwen2.5:7b-instruct-q4_K_M这里建议明确指定量化标签不要只拉默认版本。默认版本通常是体积更大的不一定是量化过的。第二个动作找到模型目录并完整复制。Ollama的模型文件都存放在模型目录下Linux默认是~/.ollama/modelsWindows是C:\Users\用户名.ollama\models。如果之前设置过OLLAMA_MODELS环境变量以那个路径为准。整个models目录拷到内网机器的同样位置结构保持一致。第三个动作在离线机器上验证。先在命令行执行ollama list看模型是否被正确识别。如果看不到检查一下OLLAMA_MODELS环境变量是否指向了正确的目录。然后真正跑一轮推理确认模型能成功加载并输出内容。提示复制模型文件到内网机器时最好做一次SHA256校验。模型文件几个GU盘或网络传输过程中可能损坏。用哈希比对确认内网机器上的文件和源文件完全一致能避免很多奇怪的问题。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_clsTrue, langch) def image_to_text(image_path): result ocr.ocr(image_path, clsTrue) 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}, timeout120, ) 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_clsTrue不要为了省一点推理时间关掉它。Tesseract的坑主要集中在中文语言包和PSM参数上。PSM默认的自动分页模式对单行文本识别效果很差需要手动指定--psm 7或--psm 6。另外如果图片背景是深色而文字是浅色需要先做颜色反转不然识别率会惨不忍睹。5.2 大模型推理环节的高频坑大模型推理环节的问题多半集中在资源、版本和输出质量这三个方面。显存溢出是最常见的。加载一个7B Q4模型去买之前先用nvidia-smi看看显存剩余。加载模型本身需要接近5G显存再加上上下文窗口和生成时的KV Cache7B模型实际占用可能到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_HOST0.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和大模型落地到离线环境希望这篇文章能帮你少走一些弯路。