LangChain + EasyOCR 图片文字提取与结构化 JSON 输出实战 想做真正能落地的 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 是 OCRLangChain 是 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逐个说下用途easyocrOCR 引擎本体依赖torch安装时会顺带拉进 CPU 版 PyTorchlangchain核心框架用来写工具链和 Agent 编排逻辑langchain-openaiLangChain 的 OpenAI 兼容 SDK实际使用中也可以换成你正在用的其他模型 SDK接口思路一致opencv-python图片读取、缩放、灰度化、二值化这些预处理操作都靠它pillow处理图片格式转换和基础操作opencv 对某些格式支持不全时能补位。有一个版本坑我必须提前说langchain从 0.1 到 0.2 再到 0.3API 变动不算小网上很多教程是旧版本写法。我自己使用时是锁版本装的比如pip install langchain0.2,0.4 langchain-core0.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_enabledFalse就可以完全离线运行。具体代码我在第 5 章的排查部分会贴出来。3. 从“能用”到“好用”三版本代码拆解3.1 入门版三行代码跑通 OCR先把最简单的代码跑通建立信心import easyocr reader easyocr.Reader([ch_sim, en], gpuFalse) result reader.readtext(sample_invoice.jpg, detail1, paragraphFalse)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(maxsize1) def get_reader(): return easyocr.Reader([ch_sim, en], gputorch.cuda.is_available())依赖包第一次加载完成之后后续每次调用都会直接命中缓存不会再重新初始化。第二步是后处理。OCR 识别出来的文本顺序未必符合阅读顺序尤其是版面复杂的图片。我会先把识别结果按坐标重排再做拼接。坐标排序的原则是先按 y 坐标从上到下分块同一垂直区域内的文字再按 x 坐标从左到右。简单版可以这样def extract_text(image_path: str) - str: reader get_reader() result reader.readtext(image_path, detail1, paragraphFalse) 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(keylambda 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. 完整实战识别报销单并输出结构化 JSON4.1 场景设计与链路结构纸上谈兵没意思我拿一个真实做过的模拟项目来演示完整链路企业内部做了一个票据报销小工具用户上传一张报销单图片系统需要提取“发票号码、开票日期、金额、收款方”四个字段并以 JSON 返回给上游业务系统。链路分成三段图片路径进入 OCR 工具输出整张图的文字块并清洗拼接拼接后的文本作为上下文交给大模型系统提示词里限定输出 schema大模型返回结构化 JSON落到业务系统。整体结构用表格拉出来更清楚环节输入处理输出OCR 工具图片路径EasyOCR 检测 识别 坐标排序文本块大模型抽取OCR 文本按提示词抽取指定字段JSON 字符串业务接口JSON校验字段并入库结构化数据4.2 完整代码实现先定义 OCR 工具这部分直接复用上一章封装好的函数和tool。接着创建大模型实例我用的是 LangChain 官方 OpenAI 兼容 SDKimport os from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate llm ChatOpenAI( modelyour-model-name, # 换成你实际使用的模型名 api_keyos.getenv(YOUR_API_KEY), # 环境变量注入不要写成明文 base_urlhttp://your-llm-endpoint, # 本地模型或兼容服务地址 temperature0.1, )如果你用的是本地模型也可以换成对应的 LangChain 封装类核心逻辑不变。接着构造 System Prompt这一步直接决定抽取质量prompt ChatPromptTemplate.from_messages([ ( system, 你是票据信息抽取助手。你只能根据用户提供的OCR识别文本回答问题 不要编造任何原文中不存在的字段。输出必须严格是JSON对象 包含且仅包含以下字段invoice_no, invoice_date, amount, payee。 ), ( human, 以下是OCR识别出的报销单文字\n{context}\n\n 请提取指定字段并以JSON格式输出。 ), ])最后把工具绑定到大模型上直接调用不需要开一个完整 Agenttools [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是否使用 GPUtorch.cuda.is_available()detail是否返回坐标和置信度中间处理开1最终调用可设0paragraph是否合并成段落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, fx2, fy2, interpolationcv2.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_enabledFalse)这样就不会再去尝试访问网络整个初始化流程稳定可控。拷贝的时候注意保留目录结构特别是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 能力边界之外的事了到时候大概率要换更重的方案。至少在这些场景里这整套工具链作为第一版完全够用而且能让业务先跑起来。