想做真正能落地的 AI 应用,绕不开一个听起来不起眼、但真上手会让人头大的环节——从图片里把文字抠出来。这活儿看着简单,实际做起来全是细节:图片清晰度不够、中英混排、表格错位、文字顺序乱,每一处都能让你改到怀疑人生。这篇文章我会直接用langchain框架 + EasyOCR搭一套完整的图片文字提取链路,重点不只是“识别出文字”,而是让大模型真正把文字用起来——比如一张报销单图片传进去,直接抽取出日期、金额、收款方,输出成结构化的 JSON。
内容适合正在做 Agent 应用开发的朋友、想在本地跑一套离线 OCR 工具链的工程师、以及做文档办公自动化场景、苦于多类型图片文本处理的小团队参考。我尽量把每一步的“为什么这样做”都讲清楚,看起来像是从一个真实项目复盘里整理出来的经验,而不是一份枯燥的接口文档。
1. 为什么是 EasyOCR + LangChain:选型思路拆解
1.1 OCR 工具横评:EasyOCR 凭什么入选
做 OCR 选型,市面上大致有四类路线:传统开源 OCR、国产深度 OCR 方案、云识别服务、以及 EasyOCR 这类轻量级本地库。我先用一张表把它们的差异拉出来看:
| 方案类型 | 安装体验 | 中文识别效果 | 硬件要求 | 适用场景 |
|---|---|---|---|---|
| 传统开源 OCR | 依赖系统库,配置繁琐 | 一般,旧引擎对复杂版面力不从心 | CPU 可跑 | 印刷体、简单单据 |
| 国产深度 OCR 方案 | 中等,文档全面但依赖较重 | 较强,尤其在中文垂类场景有优势 | CPU/GPU 均可 | 复杂版面、中英混排、表格 |
| 云识别服务 | 免安装,开箱即用 | 强,大厂模型调优到位 | 无硬件要求 | 在线场景,但图片要上传第三方 |
| EasyOCR | 一条 pip 命令搞定 | 强,内置多语言模型 | CPU 能跑,GPU 可加速 | 本地化场景、快速接入 |
我之前做某个模拟项目时,一开始图省事,直接调了云识别服务,效果确实不错。但一聊到图片要上传、按调用量计费、部分业务环境又不允许图片出内网,这套方案立刻就卡住了。后来换成本地方案,对比了一圈,EasyOCR 的优势非常明确:
- 安装体验极其友好,
pip install easyocr一行,不需要折腾系统级的图像库依赖; - 多语言内置,
['ch_sim', 'en']直接中英混排识别,不需要自己去训练模型; - 支持 GPU 加速,也支持纯 CPU 跑,部署下限很低;
- 模型文件本地存放,图片完全不出主机,隐私这块天然占优。
当然它也有短板,比如对极复杂的表格结构还原不如专门做表格解析的方案,遇到手写体也偏弱。但如果不是要处理天生模糊的手写病历或复杂扫描表格,EasyOCR 在“快速、够用、可控”这个三角上,平衡得相当好。
1.2 LangChain 在这里到底有什么用
很多人看到标题会问:OCR 是 OCR,LangChain 是 LangChain,我直接调 EasyOCR 拿文本不就行了,为什么要扯上大模型框架?
这里的关键在于:OCR 只是把图片翻译成文字,真正的业务问题往往出在“识别完之后怎么处理”。一张报销单,OCR 能吐出“日期:2025-03-12”“金额:1280.00”“收款方:某供应商”,但如果你要做流程自动化,需要的是结构化 JSON,甚至要结合上下文判断哪些字段属于哪个行项目。这件事属于大模型的理解范畴。
LangChain 在这里扮演的角色,是把 EasyOCR 包装成 Agent 可调用的工具,然后让大模型在对话中按需调用它。你可以理解成:EasyOCR 是眼睛,负责把图像内容变成文字;大模型是大脑,负责理解意图、抽取字段;LangChain 是神经回路,负责让两者顺畅协作。它提供的@tool装饰器,本质上会把 Python 函数转成一个带 schema 描述的“工具协议”,大模型根据函数名和描述判断要不要调用、传什么参数。
我自己的体会是,这种组合的场景非常广。比如用户上传一张截图,问“这里面的重点是什么”;比如 RAG 项目里用户传的是图片型 PDF,得先把图片转成文本才能走后续的向量化;再比如客服系统收到一张快递单照片,需要自动把单号提取出来填单。只要输入不是纯文本,OCR 工具链几乎都是躲不掉的第一环。
2. 环境搭建与依赖安装:最容易被绕晕的一步
2.1 环境要求与依赖清单
先交代环境。我建议 Python 版本至少在 3.8 以上,3.10 或 3.11 更舒服,很多依赖对解释器版本有要求。Windows、Linux、macOS 都能跑,差别主要在于 GPU 支持和模型存储路径。
依赖安装我用这一条命令:
pip install easyocr langchain langchain-openai opencv-python pillow逐个说下用途:
easyocr:OCR 引擎本体,依赖torch,安装时会顺带拉进 CPU 版 PyTorch;langchain:核心框架,用来写工具链和 Agent 编排逻辑;langchain-openai:LangChain 的 OpenAI 兼容 SDK,实际使用中也可以换成你正在用的其他模型 SDK,接口思路一致;opencv-python:图片读取、缩放、灰度化、二值化这些预处理操作都靠它;pillow:处理图片格式转换和基础操作,opencv 对某些格式支持不全时能补位。
有一个版本坑我必须提前说:langchain从 0.1 到 0.2 再到 0.3,API 变动不算小,网上很多教程是旧版本写法。我自己使用时是锁版本装的,比如:
pip install "langchain>=0.2,<0.4" "langchain-core>=0.2,<0.4"避免装到某个中间版本后@tool的导入路径变了导致一堆报错。如果你有现成项目依赖,建议先建一个干净的虚拟环境再装,不要直接在系统环境里混着搞。
安装完成后,可以用一段非常简单的代码验证环境是否正常:
import torch print("CUDA available:", torch.cuda.is_available())如果输出False,也不用慌,EasyOCR 在 CPU 上照样能跑,只是速度慢一些,后面我会讲 GPU 折腾和降级方案。
2.2 首次模型下载与目录结构
第一次执行easyocr.Reader(['ch_sim', 'en'])时,它会自动下载检测模型和识别模型。这一步很多新手心态会崩,因为看起来像是卡死了,一动不动,其实是模型在后台下载。
模型默认放在用户目录下的.EasyOCR/model里:
C:/Users/你的用户名/.EasyOCR/model/ # Windows ~/.EasyOCR/model/ # Linux/macOS下载完成后,里面一般会有这几个关键文件:
| 文件 | 作用 |
|---|---|
craft_mlt_25k.pth | 文本检测模型,负责从图片里找到文字区域 |
zh_sim_g2.pth | 中文字符识别模型 |
english_g2.pth | 英文识别模型 |
其他语言对应的.pth | 按你传入的语言列表决定 |
知道这个目录结构很重要。如果新的机器上有内网限制,无法直接下载模型,你可以把模型文件从别人机器上拷贝过去,放到同样的路径下,然后初始化Reader时设置download_enabled=False,就可以完全离线运行。具体代码我在第 5 章的排查部分会贴出来。
3. 从“能用”到“好用”:三版本代码拆解
3.1 入门版:三行代码跑通 OCR
先把最简单的代码跑通,建立信心:
import easyocr reader = easyocr.Reader(['ch_sim', 'en'], gpu=False) result = reader.readtext('sample_invoice.jpg', detail=1, paragraph=False)readtext返回的是一个列表,每个元素长这样:
[ ( [[x1, y1], [x2, y2], [x3, y3], [x4, y4]], # 四个角坐标 '识别的文字内容', 0.982712 # 置信度,0~1 之间 ), ... ]拿一张含“2025年3月12日”的测试票据运行,输出大致是:
[([[8, 16], [178, 16], [178, 36], [8, 36]], '2025年3月12日', 0.9864), ([[210, 16], [430, 16], [430, 36], [210, 36]], '报销单', 0.9932)]这个阶段你只需要验证一件事:文字能不能被识别出来。如果连清晰印刷体都识别不准,先别继续往下做,优先检查图片质量和模型是否下载完整。
3.2 生产化封装:单例 Reader 和后处理
入门版代码能跑,但离“能用”还差两步。第一步是初始化慢的问题:easyocr.Reader()要加载模型权重、构建计算图,第一次初始化实测可能等十几秒甚至更久,如果每次调用都重新初始化,业务上完全没法接受。
我的处理方式是把 Reader 做成全局单例,用lru_cache包一层:
from functools import lru_cache import easyocr import torch @lru_cache(maxsize=1) def get_reader(): return easyocr.Reader(['ch_sim', 'en'], gpu=torch.cuda.is_available())依赖包第一次加载完成之后,后续每次调用都会直接命中缓存,不会再重新初始化。
第二步是后处理。OCR 识别出来的文本顺序未必符合阅读顺序,尤其是版面复杂的图片。我会先把识别结果按坐标重排,再做拼接。坐标排序的原则是:先按 y 坐标从上到下分块,同一垂直区域内的文字再按 x 坐标从左到右。简单版可以这样:
def extract_text(image_path: str) -> str: reader = get_reader() result = reader.readtext(image_path, detail=1, paragraph=False) lines = [] for bbox, text, conf in result: if conf < 0.4: continue y_center = sum(p[1] for p in bbox) / 4.0 x_center = sum(p[0] for p in bbox) / 4.0 lines.append((y_center, x_center, text, conf)) lines.sort(key=lambda item: (round(item[0] / 20), item[1])) return "\n".join(text for _, _, text, _ in lines)注意那个round(item[0] / 20),它代表把高度在 20 像素范围内的文字视为同一行。这个阈值不是拍脑袋定的,图片分辨率不同阈值也要跟着调,否则要么把两行并成一行,要么一行被拆成两段。我在实际项目里通常会先打印出 bbox 坐标,观察行间距再定阈值。
3.3 用 LangChain 的@tool把它变成大模型能调用的工具
封装函数后,接入 LangChain 非常顺,用@tool装饰器:
from langchain_core.tools import tool @tool def extract_text_from_image(image_path: str) -> str: """从本地图片中提取所有可见文字,返回纯文本。 适用于报销单、名片、合同扫描件、快递单等图片场景。 入参必须是本地可访问的图片文件路径。""" text = extract_text(image_path) return text if text.strip() else "图片中未识别到明确文字,请确认图片是否清晰。"这里有一个无数新手会忽略的细节:docstring 就是大模型的说明书。模型决定什么时候调用这个工具、传入什么参数,完全靠函数描述来判断。你如果写“图片文字提取”,信息量太少,大模型遇到“帮我看看这张图里写了什么”反而犹豫要不要调;把适用场景、入参要求、返回值写清楚,调用准确率会明显提升。
另外,函数名本身就是工具名,命名建议用动词开头,比如extract_text_from_image,一眼就知道干什么。入参的类型注解image_path: str也不能省,LangChain 会基于它生成参数 schema,如果漏了类型注解,模型可能传一个 dict 进来,工具层直接炸掉。
4. 完整实战:识别报销单并输出结构化 JSON
4.1 场景设计与链路结构
纸上谈兵没意思,我拿一个真实做过的模拟项目来演示完整链路:企业内部做了一个票据报销小工具,用户上传一张报销单图片,系统需要提取“发票号码、开票日期、金额、收款方”四个字段,并以 JSON 返回给上游业务系统。
链路分成三段:
- 图片路径进入 OCR 工具,输出整张图的文字块,并清洗拼接;
- 拼接后的文本作为上下文交给大模型,系统提示词里限定输出 schema;
- 大模型返回结构化 JSON,落到业务系统。
整体结构用表格拉出来更清楚:
| 环节 | 输入 | 处理 | 输出 |
|---|---|---|---|
| OCR 工具 | 图片路径 | EasyOCR 检测 + 识别 + 坐标排序 | 文本块 |
| 大模型抽取 | OCR 文本 | 按提示词抽取指定字段 | JSON 字符串 |
| 业务接口 | JSON | 校验字段并入库 | 结构化数据 |
4.2 完整代码实现
先定义 OCR 工具,这部分直接复用上一章封装好的函数和@tool。接着创建大模型实例,我用的是 LangChain 官方 OpenAI 兼容 SDK:
import os from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate llm = ChatOpenAI( model="your-model-name", # 换成你实际使用的模型名 api_key=os.getenv("YOUR_API_KEY"), # 环境变量注入,不要写成明文 base_url="http://your-llm-endpoint", # 本地模型或兼容服务地址 temperature=0.1, )如果你用的是本地模型,也可以换成对应的 LangChain 封装类,核心逻辑不变。
接着构造 System Prompt,这一步直接决定抽取质量:
prompt = ChatPromptTemplate.from_messages([ ( "system", "你是票据信息抽取助手。你只能根据用户提供的OCR识别文本回答问题," "不要编造任何原文中不存在的字段。输出必须严格是JSON对象," "包含且仅包含以下字段:invoice_no, invoice_date, amount, payee。" ), ( "human", "以下是OCR识别出的报销单文字:\n{context}\n\n" "请提取指定字段,并以JSON格式输出。" ), ])最后把工具绑定到大模型上,直接调用,不需要开一个完整 Agent:
tools = [extract_text_from_image] llm_with_tools = llm.bind_tools(tools)但这里有个关键设计:对于“先用 OCR 工具识别,再把识别结果喂给大模型抽取字段”的流程,更稳的做法是显式控制调用顺序,而不是让 Agent 自由发挥。因为 OCR 工具本身没有副作用,大模型完全有能力判断是否需要调用,但如果你已经知道业务一定需要 OCR 这一步,就不妨直接调用函数拿结果,再交给模型处理。这样省 token、省时间、还好调试。
所以我实际跑通的代码是这样的:
image_path = "path/to/reimbursement.jpg" ocr_text = extract_text_from_image.invoke({"image_path": image_path}) chain = prompt | llm_with_tools response = chain.invoke({"context": ocr_text}) print(response.content)模型返回的内容就是 JSON 字符串,解析一下就可以落库:
import json data = json.loads(response.content) # {'invoice_no': '...', 'invoice_date': '2025-03-12', 'amount': '1280.00', 'payee': '某供应商'}如果想要一定格式稳定的 JSON,可以在创建大模型时加上model_kwargs={"response_format": {"type": "json_object"}},前提是你的模型服务端支持这个参数。支持的话强烈建议加上,能省掉大量解析异常。
4.3 识别效果优化:参数选择和预处理
整个流程跑通之后,真正花时间的是调参。readtext有几个参数我几乎每次都用:
| 参数 | 说明 | 推荐设置 |
|---|---|---|
gpu | 是否使用 GPU | torch.cuda.is_available() |
detail | 是否返回坐标和置信度 | 中间处理开1,最终调用可设0 |
paragraph | 是否合并成段落 | False,避免丢掉文本顺序信息 |
min_size | 小于该尺寸的文本框会被忽略 | 默认 20,低分辨率图适当调小 |
rotation_info | 额外检测的旋转角度列表 | 有旋转图时设为[0, 90, 180, 270],会变慢 |
batch_size | 推理批次 | CPU 小一点,GPU 可以调大 |
遇到识别效果差,我的处理顺序是:
- 先放大图片。
cv2.resize把短边拉到至少 1000 像素以上,很多低分辨率图在放大后识别率明显提升; - 转灰度图,必要时再做一个二值化,把背景噪声去掉;
- 处理旋转。拍照上传的图片经常带一点角度,直接设
rotation_info,让 EasyOCR 多角度找文本,别自己在外面绕圈校正。
预处理代码很常见:
import cv2 def preprocess(image_path: str): img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.resize(gray, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU) return binary这个优化思路适用于绝大多数“OCR 识别不对”的问题:先把图变清楚,再谈模型参数。
5. 复盘:我踩过的坑与排查手册
5.1 模型下载卡住:学会离线初始化
第一次跑 EasyOCR,初始化 Reader 时卡了十几分钟,不知道的人以为程序死循环了。其实就是模型在下载,但网络质量拉胯、没有进度条反馈,观感极差。
如果碰到网络受限,正确做法是手动准备模型文件。从一台已经能正常下载的机器上,把整个.EasyOCR/model目录拷贝到目标机器对应目录,然后初始化时关闭自动下载:
reader = easyocr.Reader(['ch_sim', 'en'], download_enabled=False)这样就不会再去尝试访问网络,整个初始化流程稳定可控。拷贝的时候注意保留目录结构,特别是craft_mlt_25k.pth这类大文件,完整文件名和路径对不上就会加载失败。
5.2 装了 GPU 版 PyTorch 却用不了
这是个非常坑的场景:明明按照网上的教程装了 GPU 版 PyTorch,一跑 EasyOCR 还是 CPU 模式,或者直接警告 CUDA initialization failed。排查顺序我建议这样:
- 先看物理层:运行
nvidia-smi,确认显卡驱动能识别; - 再查 PyTorch 层:运行
python -c "import torch; print(torch.cuda.is_available())"; - 如果第一步正常、第二步返回 False,基本就是 PyTorch 版本和驱动版本不匹配,需要重装对应 CUDA 版本的 PyTorch。
这里我给个经验:如果你的业务场景不是高清大图批量识别,CPU 跑 EasyOCR 完全能接受,一张普通发票识别时间也就一两秒。不用为了“必须用 GPU”这种心理包袱折腾半天。
5.3 OCR 文字顺序和预期不一致
我在前面提到了坐标排序,这里再展开讲为什么必须做。EasyOCR 默认按检测到的文本框方式返回结果,如果图片有多个区块,顺序经常不是阅读顺序。比如报销单里金额在右下角,日期在左上角,默认输出可能把金额排在前面,大模型抽取字段时一旦依赖顺序,容易字段错位。
我的排序代码里有一个巧点:先按round(y_center / 行高)分桶,再按x_center排序。这样做比单纯按 y 排序更稳,因为实际拍摄的图片,同一行的文字 y 坐标往往有抖动,直接排序会乱掉。你可以在自己的数据上调一下行高阈值,一般取图片高度的 2% 左右,效果不错。
5.4 图片质量差导致识别错乱
当时被一张拍照亮度不均的单据折磨过,EasyOCR 识别结果有大量乱码。后来用 5.1 里的那套预处理流程,先放大两倍、再灰度化、再 Otsu 二值化,识别准确率直接上了一个台阶。
还有一个小技巧:遇到文字带有阴影或者背景有底纹,可以先做一个形态学操作把噪声去掉,或者用cv2.fastNlMeansDenoisingColored做一下降噪。图片预处理没有银弹,但“放大 + 灰度 + 二值化”是最通用的一档,适用面极广。
5.5 LangChain 工具调用失败的几个原因
这套链路里,最容易出问题的不是 OCR,而是 LangChain 和模型之间的工具调用。我总结过几个常见原因:
| 现象 | 原因 | 解决方式 |
|---|---|---|
| 模型不调用工具 | 函数描述不清晰 | 重写 docstring,写清适用场景 |
| 工具收到错误参数 | 入参类型未标注 | 给函数参数加类型注解 |
| 返回内容解析失败 | 大模型输出非标准 JSON | 启用response_format或做二次解析 |
| 上下文过长 | OCR 文本太多,超过模型窗口 | 先裁剪无用行,或分段处理再合并 |
工具描述这事我反复强调过,它直接决定大模型的行为。你可以做一个无本试验:把 docstring 改成“这个工具能获取图像信息”,模型调用率会显著下降;改成“当用户上传报销单、截图、名片等图片时,必须优先调用此工具提取文字再回答”,模型几乎每次都能命中。
6. 落盘后的几点个人体会与下一步扩展
这一整套方案做下来,我最满意的地方是“本地可控”。图片不用上传任何外部服务,模型文件都在自己机器上,数据隐私风险小,离线也能跑,对于一个内部工具来说这是压倒性的优势。开发体验上,EasyOCR 的安装和调用足够简单,LangChain 的工具化封装又是成熟的套路,两者组合的投资回报率很高。
做下来也有几点体会。一是 OCR 工具一定不要在大模型链路里放得太“自由”,Agent 自主调工具看起来炫酷,但生产环境里显式控制调用顺序会稳得多,调试和排错都容易。二是精度优化永远优先做在输入侧,把图片处理干净比在模型参数上折腾更有效,我在多个项目里反复验证过这一点。三是别迷信某一个参数是银弹,清晰图、模糊图、旋转图、表格图,场景不同,参数组合完全不同。
后续如果你想扩展,我可以给几个明确的方向:把整份扫描 PDF 按页转图片后批量提取文本,走 RAG 流程做文档问答;对输出的 OCR 文本做表格结构重建,把块状文本还原成行列;或者把抽取出来的结构化字段接一条人工复核的 Web 界面,做成半自动的票据录入系统。手写体识别和强旋转版面识别也有空间,但那已经是 EasyOCR 能力边界之外的事了,到时候大概率要换更重的方案。至少在这些场景里,这整套工具链作为第一版完全够用,而且能让业务先跑起来。