
在实际项目中从“无法复制的文档”中提取文本这件事经常比训练一个模型还让人头疼。用户可能遇到扫描版 PDF、微信聊天截图、银行回单、电子书页面、会议白板照片这些内容能看不能选不能直接 CtrlC更不能直接粘贴给大语言模型使用。OCROptical Character Recognition光学字符识别就是解决这个问题的关键环节。OCR 把图片中的文字区域转换成人可以编辑、程序可以检索、大语言模型可以理解的纯文本让 LLM 不再面对一张模糊图片而是面对一段干净上下文。这篇文章围绕一条完整链路展开从一张不可复制的截图或扫描件开始经过图像预处理、OCR 识别、结果清洗最后把文本交给本地或云端的大语言模型。无论你想做合同信息抽取、表单录入、知识库构建还是让 LLM Agent 自动读取截图后执行任务底层都需要这套能力。我会给出可以运行的最小工程示例、关键参数说明、常见报错排查方法以及生产环境落地时的补充建议。代码统一使用 Python便于在 Windows、Linux 和 RK3588 等 ARM 开发板上复用。1. 为什么“无法复制”的文档会成为 LLM 应用的输入瓶颈1.1 “无法复制”并不是同一种问题很多用户以为“无法复制”等于“文件加密”实际上工作中遇到的情况远不止这一种。我先把常见的几种场景分类因为不同类型的文档OCR 的处理策略完全不同。场景文档形态无法复制的真实原因OCR 目标扫描版 PDF图片型 PDFPDF 中只有扫描图像没有文字层逐页识别文字手机拍照件JPG / PNG光线、角度、阴影干扰文字边缘先预处理再识别网银或内部系统页面屏幕截图页面禁止选择或前端渲染组件限制复制从截图区域还原文字电子书阅读器页面排版图片客户端限制复制或阅读器按图片渲染按章节识别片段手写表单图片没有电子字段信息只存在于笔迹中尽量识别或先分块再人工核对这些场景有一个共同点信息被封闭在像素里时LLM 无法直接推理。大语言模型的输入是 token 序列不是图片。要让 LLM 理解合同里的付款方、发票里的金额、截图里的报错信息第一步必须做 OCR 文本化。1.2 OCR 在 LLM 工作流中的位置一个典型的 OCR LLM 链路可以这样表达截图/扫描件 - 图像预处理 - OCR 引擎 - 文本清洗 - 构造 Prompt - LLM - JSON/摘要/回答每个环节都有自己的职责不能互相替代预处理不是为了仪式感而是让 OCR 引擎看到更清晰的笔画。分辨率过低、对比度不足、背景有噪点都会直接降低识别准确率。OCR 引擎本身不是 NLP 模型它只负责把字符识别出来不理解语义。它可能把“订金”识别成“定金”也可能把“10000”识别成“1oooo”。文本清洗负责处理 OCR 噪声比如合并断行、去掉多余空格、修正全角半角字符。LLM 负责语义理解、信息抽取和结构化输出。它可以修正一部分 OCR 错误但前提是文本主体足够干净。这套链路里的关键判断是OCR 产生的错误会向后传递。如果图片阶段就没有处理好后续 LLM 再怎么优化提示词也很难拿到稳定的结构化结果。1.3 不经过 OCR 的替代方案有哪些并不是所有需求都必须先 OCR再喂 LLM。可以根据场景选择不同方案。方案优点缺点适合场景多模态大模型直接识图能理解版式、表格、图表上下文token 消耗高处理速度慢成本不低小批量、版面复杂、单张图片OCR 普通 LLM本地可运行批量成本低隐私可控OCR 错误会传给 LLM需要清洗批量合同、发票、日志截图纯手工录入准确率高慢无法应对批量场景只有几页关键文档如果你的场景是一次性处理 10 张复杂表格直接用多模态大模型看图会更省事。如果是每天处理上万张截图或扫描件OCR 普通 LLM 是更可行、更可控的路线。2. 先选好 OCR 引擎再决定部署形态2.1 开源 OCR 引擎横向对比开源 OCR 引擎里目前使用最广的是 Tesseract、PaddleOCR 和 EasyOCR。三者都有自己的适用边界。Tesseract 是老牌 OCR 引擎很多 PDF 工具内部都使用它。它的优点是轻量、跨平台、支持多语言安装后可以直接命令行调用。缺点是中文识别效果需要语言包配合复杂版面需要调 psm 参数。PaddleOCR 是百度开源的中文 OCR 工具链按“检测 方向分类 识别”三个模块工作。它在中文长文本、表格、自然场景文字上的表现通常比 Tesseract 好但依赖体积更大。EasyOCR 基于 PyTorch使用简单安装后调用 API 即可。它对中英文都有不错的支持但推理性能开销大在低算力 ARM 设备上跑起来会比较吃力。引擎开发语言中文支持模型体积推理性能适合场景TesseractC需要安装 chi_sim 语言包小几十到一百多 MB快CPU 即可印刷体、统一版式、服务端轻量部署PaddleOCRPython C 推理中文效果好较大几百 MB 到 1GB 以上CPU 可跑性能适中中文截图、表格、复杂版面EasyOCRPython PyTorch支持中英文较大较慢快速验证、小样本实验建议如果你的业务主要是中文截图和文稿优先尝试 PaddleOCR。如果只是想在 RK3588 开发板上低成本跑一个 OCR 服务Tesseract 往往更省资源。2.2 云端 OCR 接口的适用场景有些团队不想维护模型的安装和更新会选择云服务 OCR 接口。以百度智能云提供的 OCR API 为例使用方式一般是注册服务、申请 API Key、按接口文档发送图片请求返回识别文本和位置信息。这种方式的好处是部署简单、识别效果有服务方持续优化缺点是图片需要上传到云端对数据敏感的合同、身份证、财务单据场景要谨慎。在选型时可以用这张表做判断决策因素云端 OCR本地开源 OCR部署成本低注册账号申请密钥即可需要安装引擎、语言包调整依赖数据隐私图片传到云端需要评估合规风险数据不出本机中文识别效果通常较好服务方持续迭代取决于引擎、模型和调参离线场景不支持可以批量成本按调用量计费主要消耗本机 CPU 和内存我的建议是生产环境如果图片数量不多、对隐私要求不严云端接口能省很多事一旦达到一定量级或者业务必须离线运行就切换到本地 OCR。2.3 在 RK3588 等 ARM 设备上运行 OCR 的注意点RK3588 是瑞芯微的一款 ARM SoC常见于 Orange Pi 5、Rock 5B 等开发板。这类设备的算力比普通 PC 弱但跑 OCR 足够。需要注意几个问题操作系统通常是 Debian/Ubuntu 的 ARM64 版本。普通pip install不一定每个包都有 aarch64 轮子安装失败时要先确认包是否支持 ARM64。Tesseract 可以直接用系统包管理器安装。以 Debian/Ubuntu 为例sudo apt update sudo apt install -y tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-simPaddleOCR 在 ARM 上可以运行但依赖较多。安装前先确认 Python 版本、ONNX Runtime 或 PaddlePaddle 是否有对应的 ARM64 版本。如果使用云端 OCR 接口RK3588 只负责发 HTTP 请求Python SDK 只要能安装即可。但某些 C SDK 可能只提供 x86_64 版本直接在 ARM 上运行会失败落地前要确认官方是否提供 ARM64 版本。3. 搭建一个最小可复现的 OCR It 管线3.1 环境准备与依赖安装要实现 OCR LLM 的最小闭环不需要很重的框架。只需要 Python、Tesseract 引擎再加几个图像处理和 HTTP 请求库。先安装 Tesseract 本体。Windows 用户需要从 Tesseract OCR 项目下载 64 位安装包安装中文语言包并记住安装目录。安装后如果命令行找不到tesseract说明没有加入 PATHPython 代码里需要手动指定可执行文件路径。Linux 用户直接使用包管理器# Debian / Ubuntu sudo apt install -y tesseract-ocr tesseract-ocr-eng tesseract-ocr-chi-sim安装完成后创建虚拟环境并安装 Python 依赖python3 -m venv venv source venv/bin/activate pip install pytesseract opencv-python pillow requests openai这里说明一下每个依赖的作用依赖作用pytesseractPython 调用 Tesseract 的封装opencv-python图像读取、灰度化、二值化、旋转校正pillow图像打开与格式处理requests调用本地或云端 LLM 接口openai调用 OpenAI 兼容接口环境准备完成后用两条命令检查是否可用tesseract --version tesseract --list-langs如果--list-langs没有输出chi_sim说明中文语言包没有安装。Linux 下重新安装tesseract-ocr-chi-simWindows 下在安装界面勾选 Chinese Simplified。3.2 图像读取与预处理OCR 识别的成败一半在识别前。很多“为什么识别不出来”的问题并不是引擎能力差而是图片太暗、文字太小、背景有纹理。下面是一个最小预处理函数按顺序完成灰度化、放大、去噪和二值化import cv2 def preprocess_image(image_path: str): img cv2.imread(image_path) if img is None: raise FileNotFoundError(f无法读取图片: {image_path}) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 对低分辨率图片适当放大提升小字号字符的识别率 gray cv2.resize( gray, None, fx1.5, fy1.5, interpolationcv2.INTER_CUBIC ) # 去噪减少背景颗粒对二值化的干扰 gray cv2.fastNlMeansDenoising(gray, None, 10, 7, 21) # 使用 Otsu 自适应阈值二值化适合前景背景对比度明显的文档 _, binary cv2.threshold( gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU ) cv2.imwrite(preprocessed.png, binary) return binary这段代码里有两个容易被忽略的参数。fx1.5表示把图片放大 1.5 倍。手机截图如果文字本来就清晰可以不放大因为放大后计算量会变大。扫描件或者小字号截图适当放大能让 LSTM 引擎更容易区分字符边界。THRESH_OTSU会基于图像灰度分布自动计算阈值适合背景比较统一的情况。如果图片背景有渐变比如拍摄角度导致一半亮一半暗全局二值化会失效后面会讲到自适应阈值的替代方案。3.3 调用 Tesseract 识别文本预处理完成之后用 pytesseract 调用识别接口import pytesseract from PIL import Image def ocr_image(image_path: str, lang: str chi_simeng) - str: # Windows 如果没有把 tesseract.exe 加入 PATH需要手动指定 # pytesseract.pytesseract.tesseract_cmd rC:\Program Files\Tesseract-OCR\tesseract.exe text pytesseract.image_to_string( Image.open(image_path), langlang, config--oem 3 --psm 6 ) return text.strip() if __name__ __main__: text ocr_image(preprocessed.png) print(text)这段代码里最重要的部分是config--oem 3 --psm 6。--oem 3表示让 Tesseract 自动选择 OCR 引擎模式一般场景默认即可。--psm 6表示把整张图片当作一个均匀的文字块。它适合文章截图、纯文本页面。如果识别结果出现大量异常分段可以换成--psm 3让 Tesseract 自动布局分析。3.4 把 OCR 结果输出成结构化的 JSON为了让后续 LLM 调用更方便可以把原始识别结果整理成统一结构import json def save_ocr_result(source_path: str, text: str, output_path: str): result { source: source_path, engine: tesseract, lang: chi_simeng, text: text, char_count: len(text) } with open(output_path, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2)这里把source、engine、lang都记录下来是必要的。生产环境里如果用户反馈“识别错了”你需要知道这张图是用哪个引擎、哪个语言包、哪个参数识别的否则无法复现问题。4. 把 OCR 结果喂给 LLM提示词、结构化与 RAG4.1 大语言模型为什么需要干净文本OCR 输出的原始文本往往带有噪声常见问题包括英文单词之间多出空格比如hello world变成h e l l o w o r l d。中文标点被识别成英文标点。表格内容丢失列对齐关系。扫描件把O和0混淆。LLM 不是万能的。给 LLM 一段充满 OCR 噪声的文本它可能仍然能读懂大概意思但在严格字段抽取时会把噪声一起写进结果。所以文本清洗要放到 LLM 之前。一个简单但有效的清洗函数import re def clean_ocr_text(text: str) - str: # 全角空格统一成普通空格 text text.replace(\u3000, ) # 合并多个连续空格 text re.sub(r[ \t], , text) # 超过两个换行时压缩为两个换行保留段落结构 text re.sub(r\n{3,}, \n\n, text) # 去掉行尾残留空格 text \n.join(line.rstrip() for line in text.split(\n)) return text.strip()清洗规则不建议做得太重。过度清洗可能导致表格列信息丢失比如把制表符全部删掉后LLM 无法判断哪一列是哪一列。清洗的目标是去噪不是重排文档。4.2 用提示词把 OCR 结果转成 JSONOCR 拿到纯文本后最常用的 LLM 任务是字段抽取。以银行回单识别为例提示词可以这样构造请从下面的 OCR 文本中提取字段只输出 JSON。 JSON 格式要求 { 交易日期: YYYY-MM-DD, 收款方: , 付款方: , 金额: , 交易备注: } OCR 文本 2024年05月20日 收款方 北京某某科技有限公司 付款方 某某商贸有限公司 金额 12,500.00 备注 货款在真实代码里可以这样组织def build_extract_prompt(ocr_text: str) - str: schema { 交易日期: YYYY-MM-DD, 收款方: , 付款方: , 金额: , 交易备注: } prompt f请从下面的 OCR 文本中提取字段只输出 JSON。 JSON 格式要求 {schema} OCR 文本 {ocr_text} return prompt这里要注意两点。第一JSON schema 一定要写清楚字段名和示例格式LLM 才能稳定输出。第二OCR 文本可能很长而 LLM 有上下文窗口限制。首次接入时可以先处理一小段验证效果后再分批。4.3 接入本地 LLM 的最简单方式本地 LLM 是当前很常见的部署方式。Ollama 是其中一种易于安装的本地模型运行工具它提供 OpenAI 兼容接口。下面代码演示了如何直接请求本地的 Ollama 服务import requests def ask_local_llm(prompt: str, model: str qwen2.5:7b) - str: resp requests.post( http://localhost:11434/api/generate, json{ model: model, prompt: prompt, stream: False }, timeout120 ) resp.raise_for_status() return resp.json().get(response, )如果你的项目里使用的是 OpenAI SDK也可以把base_url指向本地兼容接口from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1, api_keyollama ) resp client.chat.completions.create( modelqwen2.5:7b, messages[ {role: user, content: 用一句话总结这段 OCR 文本的内容。\n\n ocr_text} ] ) print(resp.choices[0].message.content)这里再补一句关于模型精度的说明。本地部署 LLM 时模型文件常见有 fp16、bf16、fp32 等格式。fp32 精度最高但占空间最大fp16 和 bf16 是实际部署中更常见的权衡方案。如果你只是做文本抽取不需要自己训练模型优先使用官方推荐的部署格式即可不需要强行对比每一种精度。4.4 从 OCR 到 RAG 的扩展当 OCR 文本量变大后你不可能把整本手册都塞进一个 Prompt。RAGRetrieval-Augmented Generation是把 OCR 文本切分成小段先检索相关片段再把片段拼进 Prompt 的技术。接入 RAG 之前至少需要做两件事分块和向量化。分块函数可以这样写def chunk_text(text: str, chunk_size: int 500, overlap: int 50) - list[str]: if not text: return [] chunks [] for i in range(0, len(text), chunk_size - overlap): chunk text[i:i chunk_size] if chunk.strip(): chunks.append(chunk) if i chunk_size len(text): break return chunks分块里设置overlap是为了避免一句话被从中间切断导致语义不完整。文本向量化需要调用 Embedding 服务。如果项目报“文本向量 API 未配置”先检查向量服务地址、模型名和密钥是否配置正确问题通常出在这三个环节。5. 参数调优与运行验证5.1 Tesseract 参数速查表Tesseract 的--psm参数直接决定识别策略。我把常见模式整理成一张速查表参数值含义适用场景--psm 3完全自动自动页面分割默认选择适合混合版面--psm 6单一均匀文本块文章截图、纯文本页面--psm 7单行文本标题、单行数据--psm 11稀疏文本表格、发票、不规则排版--psm 12稀疏文本并保留方向多方向文本--oem参数用来选择 OCR 引擎模式参数值含义--oem 0仅使用传统引擎--oem 1仅使用 LSTM 引擎--oem 2传统引擎 LSTM 引擎--oem 3自动选择默认如果你的环境安装了多个语言包--psm 6在单栏文档上通常比--psm 3更稳定因为它不会错误地把正文拆成多个块。5.2 预处理参数怎么调预处理没有万能参数但有一条稳定的调优顺序先看文字大小。如果识别结果大量漏字尝试把图片放大到 1.5 倍到 2 倍。再看对比度。如果背景是灰色使用 Otsu 二值化如果背景有渐变使用自适应阈值。自适应阈值的 OpenCV 写法adaptive cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blockSize31, C10 )blockSize表示局部区域大小一般建议是奇数常用 31 或 51。C是一个常量偏移量值越大最终二值化结果越容易把文字判定为背景。扫描阴影严重的文档时可以优先尝试自适应阈值。5.3 验证识别正确率的方法不要用“看着还行”判断 OCR 效果。准备一份标准答案文本用相似度算法量化比较from difflib import SequenceMatcher expected 实际应识别出的标准文本 recognized ocr_image(preprocessed.png) similarity SequenceMatcher(None, expected, recognized).ratio() print(f识别相似度: {similarity:.2%})对于字段抽取类任务更实用的验证方式是人工检查字段级准确率。比如 100 张发票抽出 100 个发票号对比标准答案统计正确数量。字段级准确率比整段文字相似度更能反映业务可用性。6. 常见问题排查6.1 识别结果乱码或全空遇到识别结果全空或者乱码先按下面的顺序检查问题现象常见原因检查方式处理建议结果全空语言包没安装tesseract --list-langs安装对应语言包结果全空图片是白字黑底查看图片颜色分布二值化前先取反色结果乱码图片分辨率过低放大图片看文字是否清晰提高采样率或放大图片结果乱码tesseract_cmd没配置打印异常信息手动指定可执行文件路径白字黑底的场景经常被忽略。Tesseract 默认识别黑字白底遇到浅色文字时可以先用 OpenCV 反转inverted cv2.bitwise_not(binary)6.2 中英文混排识别很差中英文混排的主要问题是语言切换和空格。现象中文识别成乱码英文单词连在一起。原因语言包选择不正确或者--psm模式把中文和英文字符混合成不合理的序列。检查方式先对单一语言样本测试确认中文语言包可用再测试chi_simeng混合模式。处理建议语言参数写成langchi_simeng注意这里是加号不是逗号。如果混排仍然不理想考虑换 PaddleOCR。在 LLM 提示词里增加一条约束“注意修正 OCR 中英文之间缺失的空格”。6.3 在 RK3588 等 ARM 设备上安装失败现象pip install报错找不到匹配的 Python wheel。原因某些包的 PyPI 源没有aarch64预编译包默认尝试源码编译而源码编译需要完整工具链。检查方式查看报错日志中是否出现No matching distribution或Failed to build。处理建议优先安装系统包管理器提供的版本比如python3-opencv。使用 Tesseract 代替 PyTorch 系 OCR 引擎。如果坚持使用 PaddleOCR先确认对应 PaddlePaddle 版本是否提供 ARM64 支持。6.4 图片倾斜和透视畸变现象文字识别率低且识别出的行顺序错乱。原因拍摄角度不正文字行不是水平方向。简单倾斜校正可以用旋转。透视畸变则需要四点变换。下面是一个最小倾斜校正示例import cv2 import numpy as np def rotate_if_needed(image: np.ndarray, angle: float) - np.ndarray: h, w image.shape[:2] center (w // 2, h // 2) matrix cv2.getRotationMatrix2D(center, angle, 1.0) return cv2.warpAffine(image, matrix, (w, h), borderValue(255, 255, 255))判断倾斜角度的常见做法是检测文本行的边缘线段计算平均角度。这个逻辑需要不少调试建议先用手工旋转测试确认 OCR 准确率随角度的变化趋势再决定是否要自动化。6.5 大尺寸图片内存占用过高现象脚本处理大扫描图时卡死或内存溢出。原因图片分辨率过大且fx1.5继续放大了尺寸。处理建议识别之前先判断图片宽度超过 3000 像素时先等比缩小。大图片可以切片识别再把结果按位置拼接。部署为服务时给图片处理接口设置最大输入尺寸和超时时间。7. 生产环境落地的补充建议7.1 从脚本到服务的改造学习环境里OCR 脚本可以从命令行运行。生产环境要把它改成可重用的服务核心改造点包括用 FastAPI 或 Flask 封装 HTTP 接口接收图片上传返回 JSON。图片处理建议放到异步任务队列避免长耗时阻塞接口。增加图片格式校验、大小限制和内容类型白名单。记录每次请求的图片哈希、OCR 版本、识别耗时和返回结果。这里提供一个最小接口思路from fastapi import FastAPI, UploadFile import hashlib app FastAPI() app.post(/ocr) async def ocr_api(file: UploadFile): content await file.read() image_hash hashlib.sha256(content).hexdigest() # 实际项目中这里应调用 ocr_image() 函数 text 识别后的文本结果 return { image_hash: image_hash, text: text, engine: tesseract }7.2 日志、缓存、脱敏与回滚生产环境要保留可追溯性。建议每次 OCR 都保存三样东西原始图片、预处理后图片、识别文本和参数。这样遇到识别纠纷时可以还原当时的处理过程。缓存也是必须的。同一张图片可能被重复上传按图片哈希做缓存可以显著减少重复计算cache_key image_hash _ lang _ psm如果使用云服务 OCR缓存能同时降低费用。如果使用本地 OCR缓存能降低 CPU 压力。脱敏是容易被忽略的一环。识别后的文本可能包含身份证号、银行卡号、手机号等敏感信息。入库前要设计脱敏规则日志里也不要打印完整明细。这不是“多做一步”而是合规底线。7.3 下一步从 OCR 到文档智能解析OCR 只是文字提取不等于文档理解。真实文档里有标题、段落、表格、页眉页脚、印章、手写批注这些结构信息对 LLM 的理解很重要。进阶方向有三个版面分析。使用目标检测模型先区分标题、正文、表格、图片再分别做 OCR。表格识别。普通 OCR 会把表格转成混乱的文本流表格结构还原需要专门的表格识别模型。多模态大模型。对复杂版面多模态 LLM 可以直接理解图像整体但成本和速度需要评估。对新手来说建议不要一开始就搭建完整系统。先把“截图 - OCR - 文本 - LLM - JSON”这条最小链路跑通用 20 张真实样本验证准确率再根据失败样本决定引入版面分析还是表格识别。技术方案越复杂维护成本越高业务收益才是最终衡量标准。7.4 一套可复用的发布前检查清单最后给出一套可以直接用于项目验收的检查清单[ ] Tesseract 安装路径正确tesseract --version可执行。[ ]tesseract --list-langs含chi_sim和eng。[ ] 图片预处理在 20 张真实样本上验证过不是只在理想样本上验证。[ ]--psm参数与目标版面匹配表格和文章截图分别测试过。[ ] 文本清洗函数去除了 OCR 噪声没有破坏表格结构。[ ] LLM 提示词在至少 20 条真实 OCR 结果上测试过字段抽取稳定。[ ] 接口层有超时、重试和错误码不会因为单张图片失败而中断整个任务。[ ] 敏感图片和敏感文本已做脱敏日志中不打印完整信息。[ ] 保存了原始图片哈希、OCR 版本和识别参数可以复现问题。[ ] 图片和结果有缓存策略重复请求不会重复计算。这套清单在个人项目里可能显得繁琐但它能帮你提前避免大部分线上问题。OCR 和 LLM 结合的难点不在单个环节而在整条链路的稳定性。先把图像处理和文本清洗做到位LLM 的输出质量自然会提高。