恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
本地OCR+大模型:从图片到结构化文档的完整工作流实战
首页
资讯中心
/
本地OCR+大模型:从图片到结构化文档的完整工作流实战
本地OCR+大模型:从图片到结构化文档的完整工作流实战
发布时间:2026/10/20 21:37:17
不管你是做运营、做财务、还是单纯想整理自己的资料手头一定堆着大量这样的东西发票、合同扫描件、产品说明书、名片、手写便签甚至是手机拍的一页书。这些内容有一个共同点——信息都在图片里但你要用的时候只能一张张打开去看再手动录入、整理、归档。这个活儿看起来不起眼但真做起来又烦又耗时尤其发票和合同这种需要核对细节的眼睛盯久了很容易漏字。我一直在搭一套本地运行的OCR识别工具来解决这个问题。整套工作流非常纯粹本地跑OCR识别出图片里的文字把结果交给大模型去整理、纠错、提取关键字段最后输出一份结构化的JSON或者格式化文本。整个过程不需要上传任何图片到第三方网站隐私有保障运行也很快。这篇文章就把这套经验工作流完整分享一下从设计思路、环境准备到可直接运行的代码、提示词模板再到我踩过的坑一次说清楚。1. 工作流设计思路与方案选型1.1 为什么选择“OCR 大模型”两段式架构明确一下要解决的问题现实中接触到的文档图片往往并不规整。可能是手机随手拍的一张会议白板可能是扫描歪了的合同页也可能是折叠过再摊开的发票。它们的共同特点是文字密度高、排版不规则、存在手写或印章遮挡。针对这种场景单纯靠OCR识别出文字还不够。因为OCR输出的往往是碎片化的文本块阅读顺序混乱有些地方还会出现识别错误。这时候如果直接把结果拿给人工去读依然很费劲。所以方案要拆成两段第一段用OCR把图片里的文字尽可能完整地抠出来。第二段把杂乱的文本交给大模型让它根据文档类型去理解结构、纠正明显错误、提取关键字段。这个两段式设计的好处在于每次只让一个环节做它最擅长的事情。OCR擅长像素级文字识别大模型擅长语义理解和结构整理两者各司其职结果稳定且可控。1.2 工具选型PaddleOCR OpenAI兼容接口OCR引擎的选择我对比过几个主流方案Tesseract老牌开源但中文识别效果一般对倾斜和复杂背景的适应力较差。云服务OCR识别效果好但涉及图片上传隐私是个问题而且调用次数多了有成本。PaddleOCR百度开源中文识别效果出色支持80多种语言能检测倾斜文本本地运行免上传识别速度也快。我最终选的是PaddleOCR实在是它的识别精度和部署成本综合下来最优。尤其是中文场景它对常见字体、打印体、甚至一些模糊扫描件的识别表现都很稳定。大模型部分思路是“能本地跑就本地跑不能本地跑就云端API”。现在主流的兼容OpenAI接口格式的本地推理框架App支持得都很好配置一个base_url就能无缝对接本地模型或云服务提供的兼容接口。所以代码里我统一用OpenAI客户端的方式调用切换模型时只需要改两个环境变量。1.3 整体架构与目录结构这套工作流的运行逻辑很简单几步就讲清楚了输入一张图片路径。OCR引擎识别图片中的所有文字。对OCR文本做轻量清洗去多余符号、空行等。构造提示词把清洗后的文本和历史经验模板发送给大模型。大模型返回结构化结果默认JSON保存到本地。项目目录结构大致如下ocr-workflow/ ├── ocr_workflow.py # 主脚本 ├── .env # 存放API Key和模型配置 ├── requirements.txt # 依赖清单 ├── inputs/ # 放待识别的图片 └── outputs/ # 结构化结果输出目录这个结构很轻量没有复杂的框架依赖一个Python文件走天下复制到任何一台机器上都能直接跑。2. 环境准备与依赖安装2.1 创建虚拟环境避免依赖冲突PaddleOCR依赖的底层库如paddlepaddle对Python版本有一定要求不推荐直接装到系统全局环境里否则很容易把其他项目的依赖搞坏。先建一个独立的虚拟环境是必要的。# Python 3.9 或 3.10 均可 python3 -m venv venv source venv/bin/activate # Windows下执行 venv\Scripts\activate2.2 安装PaddleOCR及注意事项PaddleOCR的核心依赖是PaddlePaddle深度学习框架。安装时有几个坑要提前知道如果你没有NVIDIA显卡不要装默认的GPU版本直接装CPU版本就行体积小、兼容性更好。测试阶段千万不要跳过模型下载这一步PaddleOCR第一次运行时会自动下载检测模型和识别模型如果你的网络不稳建议先把模型下载好或者手动指定模型路径。# CPU版本推荐无显卡用户 pip install paddlepaddle pip install paddleocr # 如果你有NVIDIA显卡且CUDA版本匹配可以装GPU版 # pip install paddlepaddle-gpu装完验证一下是否正常from paddleocr import PaddleOCR ocr PaddleOCR(use_angle_clsTrue, langch) result ocr.ocr(test.png) print(result)跑通这一步OCR环境就算准备好了。2.3 大模型调用准备大模型部分只需要一个兼容OpenAI接口的端点。如果你有云厂商的API Key配置一下base_url和api_key就能用如果你想真正完全本地化用Ollama或类似工具部署一个本地模型然后把base_url指向http://localhost:11434/v1即可。依赖也很简单pip install openai python-dotenv在项目根目录创建.env文件内容如下LLM_API_KEYsk-xxx LLM_BASE_URLhttps://api.your-provider.com/v1 LLM_MODEL你的模型名保持这个习惯所有敏感配置都走环境变量不要把Key硬编码在代码里。3. 核心代码实现3.1 OCR识别模块OCR模块的代码不长但有几个关键点全局只初始化一次PaddleOCR对象。这个引擎初始化时需要加载模型如果每次调用都重新初始化性能会非常差。识别结果是个嵌套列表结构不同版本略有差异但基本逻辑是一样的第二层是文本框坐标和文字内容、置信度的元组。# -*- coding: utf-8 -*- import os import json import re from pathlib import Path import pandas as pd import openai from openai import OpenAI from paddleocr import PaddleOCR from dotenv import load_dotenv load_dotenv() # 初始化OCR引擎全局复用 ocr_engine PaddleOCR(use_angle_clsTrue, langch, show_logFalse) # 初始化大模型客户端 client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), ) LLM_MODEL os.getenv(LLM_MODEL, qwen2.5:7b) def extract_text_from_image(image_path: str) - str: 从图片中提取文字行返回拼接后的纯文本。 try: result ocr_engine.ocr(image_path, clsTrue) except Exception as e: return fOCR识别失败: {e} lines [] for page in result: # 某些版本返回的page可能为空列表 if not page: continue for item in page: # item的结构: [坐标框, (文本, 置信度)] text item[1][0] conf item[1][1] # 过滤置信度太低的识别结果减少干扰 if conf 0.5: lines.append(text) return \n.join(lines)置信度的阈值0.5是我试出来的经验值。设置太低会把无意义的噪点识别结果都放进来太高又会漏掉部分模糊文字。实际使用中可以根据你的图片质量微调扫描件通常可以提高到0.6手机拍的照片建议保持0.5。3.2 文本预处理模块OCR原始文本经常包含各种问题行与行之间乱序、多余的空格和特殊符号、把“。”识别成了“.”等等。预处理不做太重的活毕竟后续有大模型兜底但把明显干扰项清掉能让大模型更专注。def clean_ocr_text(raw_text: str) - str: 清洗OCR文本去除明显的断裂和干扰符号。 if not raw_text: return lines [line.strip() for line in raw_text.splitlines() if line.strip()] # 常见误识别符号替换 cleaned [] for line in lines: line re.sub(r[·•●◆], , , line) # 项目符号变成逗号 line re.sub(r[|丨], , line) # 竖线视为分隔 cleaned.append(line) text \n.join(cleaned) return text3.3 大模型格式化模块这是整个工作流的核心智能化模块。之前OCR结果给到普通脚本只能做固定规则处理现在大模型能理解语义、纠正错误、按需结构化。def format_with_llm(raw_text: str, doc_type: str 通用) - str: 调用大模型把OCR文本整理为结构化JSON。 doc_type: 通用 / 发票 / 合同 / 名片等 instruction DOC_TYPE_PROMPTS.get(doc_type, DOC_TYPE_PROMPTS[通用]) prompt f 你是一个文档信息结构化助手。输入的是一条混有杂讯的OCR识别文本 请根据文档类型 {doc_type} 完成以下任务{instruction} 严格按以下约束 1. 只输出合法的JSON对象不要包含Markdown标记或解释性文字 2. 时间统一格式为YYYY-MM-DD金额统一为数字保留两位小数 3. 缺失字段用null绝对不要凭空编造内容 4. 原始文本中明显是识别错误的地方如“0”和“O”混淆可以结合上下文修正 OCR文本开始 {raw_text} OCR文本结束 resp client.chat.completions.create( modelLLM_MODEL, messages[ {role: system, content: 你是一个信息抽取助手只输出干净的JSON不做任何解释。}, {role: user, content: prompt}, ], temperature0.1, max_tokens4000, response_format{type: json_object}, # 部分模型支持 ) content resp.choices[0].message.content.strip() # 兼容某些模型返回带json包裹的情况 if content.startswith(): content re.sub(r^(?:json)?\s*|\s*$, , content) return content这里有个参数细节值得特别说明temperature0.1是为了让输出尽量稳定、贴近事实不要创造性发挥毕竟文档结构化不是写作文。max_tokens设大一些长合同文本的提取结果可能超过默认值截断后就拿不到完整JSON了。3.4 主流程串联在主函数中把整个流程串起来同时支持单文件和批量目录两种模式def process_image(image_path: str, doc_type: str 通用) - dict: 处理单张图片返回结构化结果。 print(f[1/4] OCR识别: {image_path}) raw_text extract_text_from_image(image_path) print(f识别到 {len(raw_text)} 字符) print([2/4] 清洗文本) cleaned_text clean_ocr_text(raw_text) print([3/4] 大模型结构化) result_json format_with_llm(cleaned_text, doc_type) print([4/4] 后处理与保存) try: result_dict json.loads(result_json) except json.JSONDecodeError: # JSON解析失败时保存原始响应供人工检查 result_dict {raw_response: result_json} # 自动生成输出路径 in_path Path(image_path) out_dir Path(outputs) / in_path.stem out_dir.mkdir(parentsTrue, exist_okTrue) # 保存原始OCR文本方便溯源 (out_dir / ocr_raw.txt).write_text(raw_text, encodingutf-8) (out_dir / cleaned.txt).write_text(cleaned_text, encodingutf-8) # 保存结构化JSON (out_dir / result.json).write_text( json.dumps(result_dict, ensure_asciiFalse, indent2), encodingutf-8, ) print(f结果已保存到: {out_dir}) return result_dict def process_directory(dir_path: str, doc_type: str 通用, pattern: str *.png): 批量处理文件夹内所有匹配的图片。 img_dir Path(dir_path) img_files list(img_dir.glob(pattern)) list(img_dir.glob(*.jpg)) \ list(img_dir.glob(*.jpeg)) list(img_dir.glob(*.pdf)) if not img_files: print(f目录下没有找到匹配的图片: {dir_path}) return for img_file in img_files: try: process_image(str(img_file), doc_type) except Exception as e: print(f处理 {img_file} 时出错: {e}) if __name__ __main__: import sys if len(sys.argv) 2: print(用法: python ocr_workflow.py 图片路径或目录 [文档类型]) sys.exit(1) target sys.argv[1] dtype sys.argv[2] if len(sys.argv) 2 else 通用 if Path(target).is_dir(): process_directory(target, dtype) else: process_image(target, dtype)跑起来之后输出效果大概是这样python ocr_workflow.py inputs/invoice.jpg 发票输出JSON大概长这样{ 发票号码: 12345678, 开票日期: 2024-11-15, 购买方: { 名称: 某科技有限公司, 税号: 91... }, 销售方: { 名称: 某贸易有限公司, 税号: 91... }, 金额: 1000.00, 税额: 130.00, 价税合计: 1130.00 }4. 提示词设计与结构化输出经验4.1 通用文档提示词模板大模型工作得好不好一半看模型本身一半看提示词。我曾经用“帮我提取一下发票信息”这种模糊提示词输出忽好忽坏。后来改成模板化的约束提示词稳定性明显提升。核心要点就三条明确输出格式、约束缺失字段的处理方式、规定如何对待识别错误。DOC_TYPE_PROMPTS { 通用: 分析文档的整体结构输出JSON对象包含字段doc_title(文档标题) created_date(文档日期不可识别则为null)parties(涉及的主体数组 每个元素包含name和role)summary(200字以内的内容摘要) key_points(要点列表尽可能覆盖所有重要信息)。, 发票: 输出JSON对象包含invoice_code(发票代码)invoice_number(发票号码) issue_date(开票日期)buyer_name(购买方名称)buyer_tax_id(购买方税号) seller_name(销售方名称)seller_tax_id(销售方税号) total_amount(金额纯数字)total_tax(税额纯数字) total_with_tax(价税合计纯数字)items(发票明细列表每项包含name和amount)。, 合同: 输出JSON对象包含contract_title(合同名称)contract_no(合同编号) party_a(甲方名称)party_b(乙方名称)sign_date(签订日期) effective_date(生效日期)total_amount(合同总金额若有) duration(合同期限)key_clauses_abstract(关键条款摘要数组 每条包含clause_title和clause_content)。, }4.2 发票专用提取逻辑发票识别最能体现这套工作流的价值因为发票上的信息维度是固定的对字段要求也最严格。识别发票时有几个特殊注意点发票号码和代码通常印刷字体特殊远看像数字的“0”可能是字母“O”大模型结合上下文能修正。金额是分栏印刷的OCR容易把“¥1000”读成“¥1000”或者漏掉小数位。提示词里明确要求金额输出为纯数字并保留两位小数大模型会自动补全。发票代码和号码在多数电子发票上没有缺失就给null不要臆造。我在实际过程中发现PaddleOCR对新版数电发票全数字化电子发票的识别效果尤其好因为这种发票版式干净、字体统一几乎没有手写干扰。4.3 合同关键条款抽取合同识别的难点在于合同页数多、条款层级复杂。我的实践经验是识别多页合同时一页一页处理比一次塞给OCR更好因为PaddleOCR处理超长图片时会丢失细节。如果合同页很多建议先用OCR识别出全文然后把大模型接口改成“分块提取再汇总”的策略。分块思路简单说一下把全文按长度切成多个块每块用提示词提取“要点列表”最后再用一次大模型调用把各块要点汇总去重。虽然调用次数多了但长文本效果远好于一次塞给大模型——因为上下文窗口有限强行塞进去会导致中间内容被忽略。5. 常见问题与排查技巧实录5.1 OCR识别率低怎么办识别率是这套流程的生命线OCR都认不对后面大模型再强也无力回天。提升识别率的经验排序如下优先提升图片本身质量扫描分辨率至少300dpi手机拍照时要保证光照均匀不要有阴影遮挡。如果图片歪斜PaddleOCR自带的use_angle_cls角度分类器能自动纠正但纠正能力有限严重的90度或180度翻转还是得先预处理。遇到印章盖住了正文识别结果里会出现乱码这时候不要纠结后续大模型处理时把明显乱码的字段设为null更现实不要试图强行推断。还有个细节PaddleOCR对长图支持有限如果一张图里上下排了多张票据比如一张A4纸扫描了4张发票识别效果会明显下降。这种情况建议先用图像处理工具把图切分开再逐张识别。5.2 大模型输出不稳定大模型输出不稳定一般表现为有时返回正常JSON有时返回Markdown代码块有时直接输出解释文本。排查从这几个方向入手检查response_format是否传了{type: json_object}部分模型支持但部分模型会报错报错时去掉这行参数靠提示词约束即可。提示词中加上“只输出JSON不做任何解释”这句约束能拦住大多数废话输出。如果模型经常在JSON内部插入注释或多余的换行就在后处理时增一层修复逻辑把最外层{...}提取出来再解析。我在post-process里还加了异常兜底逻辑如果json.loads失败不直接崩溃而是把原始响应存成文本文件方便人工核对而不是让整个批量任务中断。5.3 性能优化建议这套工作流的性能瓶颈主要有两个OCR引擎的推理速度以及大模型的响应延迟。OCR部分优化经验PaddleOCR在CPU上识别一张普通发票大约需要1-2秒如果图片很大或者文字很多可能需要5秒以上。批量处理几百张图时建议用paddleocr自带的enable_mkldnnTrue参数开启CPU加速亲测推理速度能提升50%以上。大模型部分优化经验如果用的是本地模型延迟取决于你的机器配置。7B级别的模型在消费级显卡上输出2000多token大约需要1-3分钟速度不算快。批量场景建议先把OCR文本全部准备好再异步调用大模型接口而不是逐张串行处理。同时建议在代码里加上失败重试机制大模型接口偶尔超时是常态重试一次往往就好了。5.4 常见问题速查表问题可能原因解决方案PaddleOCR运行时报错“模型下载失败”网络无法访问下载源手动下载pdmodel文件放到~/.paddleocr/目录下OCR识别结果全为空图片太暗或文字太小先用OpenCV做灰度化、增强对比度预处理大模型返回内容被截断max_tokens设置太短提升至4000以上或者分块处理JSON解析抛异常模型输出有格式杂质用正则提取最外层花括号内容再解析发票金额小数位丢失OCR识别精度不足提示词明确“金额用数字保留两位小数”6. 实操经验总结这套工作流从雏形到稳定运行前后迭代了三个版本。最初版本只有简单的OCR转文本整理工作全靠人工后来接入大模型后体验完全不同。根据我个人的经验再分享两个比较实在的技巧第一不要只依赖默认的通用类型。花点时间把你日常处理最多的文档类型发票、合同、报销单的专用提示词写出来结构化的精准度会提升很多。提示词模板是可以积累沉淀的资产用得越多越顺手。第二结构化输出以后建议在代码里加上导出到Excel或CSV的步骤。处理完几十张票据后直接拿表格去汇总比一个JSON一个JSON地打开方便太多。数据管理这块表格永远比文本直观。如果有人想从零搭建这套工具按这篇文章的顺序走一遍就能跑通。先学会跑通单张图片再扩展批量最后再根据你自己的文档类型去调提示词。这套方法不挑场景换个提示词模板就能应付新的需求是一项值得投入的基础设施。