☰
本地OCR+大模型:构建发票合同文档结构化识别工作流
2026/10/20 21:52:11 网站建设 项目流程

上个月我帮某工作室搭了一套本地文档识别流程,对方财务同事的反馈是:终于不用再对着发票逐格抄明细了。这套东西本质上就是“本地创建一个OCR工具 + 用大模型整理格式输出”的经验工作流,专门处理文档、发票、合同这类细碎又杂乱的识别任务。整条链路跑通之后,不只是发票,合同条款、说明书页、随手拍的名片、甚至手写备注都能统一变成结构化数据,直接进表格或数据库。这篇文章就把我的完整做法拆开:为什么这么选型、环境怎么搭、发票合同各自怎么调、Prompt怎么设计、代码怎么让大模型帮忙生成,以及我实测下来踩过的坑和保留意见。

1. 为什么我放弃在线识别,转而走“本地OCR + 大模型整理”的组合路线

1.1 报销和归档场景的真实痛点

先说最典型的使用场景。报销发票、合同归档、供应商资质文件整理,这些东西有几个共同特点:数量多、格式杂、字段细碎。一张增值税发票上,既有购买方名称、销售方名称、发票号码,也有中间的商品明细表格,还有右下角一堆备注、收款人、复核人。合同就更麻烦了,动辄十几页,页眉页脚、条款编号、金额大写小写混在一起。如果靠人手工录入,一张发票三分钟,一份合同半小时,一次集中报销能花掉大半天。

我一开始也试过在线OCR工具,网页上传图片,等它识别完再复制粘贴。问题很明显:一是发票合同这类文件直接传第三方服务,心里不踏实,尤其是带税号、银行账号、合同金额的内容;二是批量处理能力太弱,网页工具一张一张点,免费的还限制次数;三是识别结果就是一堆文本,字段结构还是要自己整理,遇到表格就更难受,经常把“金额”和“税额”串行。

所以“本地OCR + 大模型整理”这个组合,本质上就是两条腿走路:本地OCR负责把图像里的文字抠出来,大模型负责把这些文字理解成结构化字段。前者解决“看得见字”的问题,后者解决“看得懂逻辑”的问题。

1.2 本地识别的三个不可替代优势

第一个优势是隐私可控。发票、合同、内部单据这些内容,数据主权很重要。本地OCR工具所有图像处理都在本机完成,图片不会离开你的硬盘,这一步就已经解决了最大的心理障碍。

第二个优势是批量处理和成本。在线OCR按张计费或者按次数计费,一百张发票就是一百次调用,累计起来成本不低。本地OCR装好之后,CPU跑也行,GPU跑更快,批量识别只是时间问题,没有额外费用。实测下来,CPU上单张普通发票大约两三秒,GPU会明显更快,完全能满足个人或小团队的需求。

第三个优势是可调参数、可扩展。本地OCR引擎可以针对发票、合同、截图等不同场景调整预处理流程,也能接入表格识别、方向分类、版面分析这些增强功能。在线工具给的是一个黑盒,遇到倾斜图片、阴影遮挡、低分辨率扫描件,你只能干等它出错,却没有任何办法干预。本地工具不一样,识别不好可以改参数重跑,这是非常核心的差别。

1.3 为什么输出结果一定要再过一遍大模型

很多人会问:OCR已经识别出文字了,为什么还要大模型?因为OCR输出的本质是“按行排列的文本流”,不是“结构化的字段”。比如识别结果是:

发票号码:12345678 开票日期:2024年06月18日 货物或应税劳务名称:*办公用品* 金额 ¥ 56.00 税额 ¥ 3.36 价税合计(大写)伍拾玖元叁角陆分 ¥59.36

这段文字人眼能看懂,但要让程序直接入库,需要明确哪些是键、哪些是值、金额对应的是哪一行、表格里的多个商品条目怎么切分。这里需要的不是“识别能力”,而是“语义理解能力”。大模型恰好擅长这件事。

另外,OCR本身不是100%准确的,倾斜、模糊、印章遮挡都会造成个别字符错乱。大模型在看OCR结果时,会结合上下文对明显错误做一定程度的推理纠正。比如“发票号玛”会被它理解成“发票号码”,“¥59.3G”会结合“价税合计(大写)伍拾玖元叁角陆分”推理成“¥59.36”。这种能力是传统规则匹配很难实现的。

1.4 为什么不直接把图片丢给大模型

这里还有一个容易踩的误区。现在不少大模型支持直接读图,那我是不是可以跳过OCR,直接把发票图片发给大模型?我的建议是:这个场景下最好别。直接读图有两个问题:第一,token成本高,一张高清发票图可能相当于几千甚至上万token,批量处理时开销很大;第二,密集细碎的小字,比如发票表格里的内文、合同的注释条款,多模态大模型直接看图反而容易漏词、出现幻觉,它可能“看个大概”就把字段填了。

而先用本地OCR把文字精确提取出来,再以文本形式交给大模型,等于先做了一次确定性极高的压缩。大模型只需要处理纯文本,输出会更稳定,也更容易通过提示词约束格式。整套链路也更省钱、更快。

2. 最小可用环境:把本地OCR跑起来需要准备什么

2.1 先搭出一个能跑的最小环境

我这套流程用的核心工具是Python生态下的开源OCR引擎,文档和社区都比较成熟,中文识别效果也够用。硬件上,普通办公电脑就行,纯CPU识别速度尚可,如果你有NVIDIA显卡,安装GPU版依赖之后速度会明显提升。操作系统Windows、macOS、Linux都可以,下面命令基本通用。

建议先建一个干净的虚拟环境,避免依赖冲突:

python -m venv ocr_env # Windows: ocr_env\Scripts\activate # macOS / Linux: source ocr_env/bin/activate pip install --upgrade pip pip install paddlepaddle pip install paddleocr pip install opencv-python pip install requests

这里解释一下为什么装这几个包。paddlepaddle是深度学习框架,OCR引擎本身依赖它做模型推理;paddleocr是我们真正使用的OCR工具库,封装了文本检测、方向分类、文字识别等能力;opencv-python用于图像预处理;requests用于后续调用大模型接口。

安装过程中最容易遇到的坑是版本冲突。如果你电脑里已经装了其他深度学习库,建议在虚拟环境里重新装,不要图省事直接往全局环境塞。另外,第一次运行OCR引擎时,它会自动下载检测模型和识别模型,如果网络不稳定下载失败,多试几次,或者手动把模型放到用户目录下的隐藏文件夹里。

2.2 用一张图验证识别链路

环境装好之后,先用一张清晰的扫描图片跑通最小流程。图片不要太复杂,最好是白底黑字、没有倾斜的文字截图,这样能快速验证“模型是否装载成功、识别结果是否正常”。

from paddleocr import PaddleOCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) result = ocr.ocr('sample.png', cls=True) if result and result[0]: for item in result[0]: box = item[0] text = item[1][0] score = item[1][1] print(f"{text}\t置信度: {score:.4f}")

这里use_angle_cls=True表示启用方向分类器,可以自动判断图片是否旋转了90度或180度,对手机拍摄的发票尤其重要。lang='ch'是加载中文识别模型。看到识别文字一行行打印出来,说明本地OCR已经通了。

2.3 让大模型直接帮你生成可运行的代码

标题里说“给大模型生成代码就能直接跑”,这不是一句空话。在我搭这套工作流的时候,真正写代码的时间很少,大部分代码是大模型生成的。关键是把需求描述清楚。

你可以直接把下面这段需求发给大模型:

我需要一个Python脚本,完成以下工作流: 1. 读取本地某个目录下的所有图片(jpg/png); 2. 使用开源OCR库识别图片中的中文文字; 3. 把每张图片的OCR识别结果按行拼接成一段文本; 4. 把这段文本发送给本地大模型接口,接口地址为 http://127.0.0.1:8000/v1/chat/completions; 5. 让大模型根据我提供的字段模板提取结构化信息,并输出JSON; 6. 把每张图片对应的JSON结果保存到一个整体结果文件里。 请生成完整可运行的脚本,并注明需要安装哪些Python包。

大模型会给你一份代码骨架。你只需要补上OCR初始化代码、大模型接口地址和Prompt模板,整个工作流就跑起来了。为什么这条路可行?因为这个任务边界非常清晰:输入是图片,中间是OCR调用和API调用,输出是JSON。这种“管道式”脚本正是大模型最擅长的内容,它不涉及复杂的业务逻辑和架构决策。

3. 发票、合同、普通文档的识别差异在哪里,参数怎么调

3.1 三类文档的版面特点完全不同

发票、合同、普通文档虽然都是“文字图片”,但结构差异极大,优化的重点也完全不同。我整理了一个对比:

文档类型典型版面识别难点优化方向
发票表格密集、键值对、明细区域小字号、框线干扰、金额对齐高清扫描、表格识别、方向校正
合同长段落、条款编号、页眉页脚文字量大、跨页、字体多样分段识别、多页合并、段落切分
普通文档正文为主,可能有分栏或插图图片遮挡、分栏顺序、无关噪声版面分析、去除页眉页脚、栏序重排

我刚开始的时候拿着同一套参数去跑发票和合同,结果发票识别出的表格里数字串行,合同的条款又乱成一团。后来才明白,OCR不是一键万能,必须针对场景做预处理和参数调整。

3.2 图像预处理:能不能看清决定了识别上限

OCR的识别效果,很大程度上在图像预处理阶段就已经决定了。很多模糊、倾斜、曝光过度的图片,不经过处理直接识别,结果就是一团乱码。我常用的几个手段:

  • 灰度化:去掉颜色干扰,让文字和背景的对比更明显。
  • 二值化:把图片变成黑白两种像素,减少光影干扰,适合扫描件。
  • 对比度增强:对颜色偏淡、字迹发虚的图片特别有效。
  • 透视校正:发票手机拍照时经常有角度倾斜,先用边缘检测找到发票外边界,再做透视变换拉正。

这里给一个简单的透视校正思路:先用OpenCV读图、转灰度、边缘检测,再找出最大轮廓的四个顶点,最后用cv2.warpPerspective拉正。实际操作中,只要发票四个角能找出来,效果就非常明显。不过要注意,透视校正不是越“重”越好。有些图片本来就很正,再做轮廓检测反而可能裁掉边缘内容。

3.3 OCR参数调整的三个重点

第一个重点是方向分类。use_angle_cls=True一定要开。手机拍照的图片经常旋转90度,尤其是扫描合同的时候,某一页是倒的,很常见。方向分类器会自动把图片转正,再进入文字识别,能少很多麻烦。

第二个重点是检测框参数。OCR引擎先把文字区域框出来,再识别框内文字。对于发票这种小字号密集场景,可以把检测相关的阈值调低一些,让模型尽量把小字也框出来,但代价是增加了误检噪声。合同这种大段文字场景,反而希望框更完整,避免一句被切成好几段。

第三个重点是表格识别。发票里的明细表格如果只用普通文本识别,行和列的位置关系会丢失。更好的做法是启用独立的表格结构识别能力,让模型先还原表格线条,再往单元格里填文字。PaddleOCR 的PP-Structure模块就支持这样的功能,会把表格输出成类似HTML的结构,方便后续解析。如果你处理的表格不多,也可以先普通识别,然后把大模型当作“表格理解器”,让它根据文本和坐标重新组织行、列结构,这个方案更轻量。

3.4 关于识别清晰度的补充说明

扫描件不是分辨率越高越好。我试过把一张A4合同按600DPI扫描,结果单页图片体积巨大,OCR耗时翻倍,识别率却没有明显提升,反而因为分辨率太高,原本纸张的纹理和噪点被放大,干扰检测框。我的经验是200到300DPI是性价比最合适的区间。手机拍摄的话,保证文字区域在画面中占比大一些,光线均匀,不要开HDR叠加效果,识别效果会好很多。

4. 让大模型把杂乱OCR结果“规整”成结构化输出的Prompt设计

4.1 OCR结果为什么不能直接入库

举一个我实际遇到过的例子。一张发票的OCR识别结果长这样:

票 号码 12345678 开票日期: 2024年06月18日 购 买 方 名 称:某某科技有限公司 纳税人识别号:91110000XXXXXXXXXX 货物或应税劳务、服务名称 规格型号 单位 数量 单价 金额 税率 税额 *办公用品* 批 1 56.00 56.00 6% 3.36 价税合计(大写)伍拾玖元叁角陆分 (小写)¥59.36

这已经算是很理想的结果了。但注意几个问题:第一,字段之间有大量空格、换行,程序无法直接做键值匹配;第二,“票 号码”中间有空格,直接拿来入库会出错;第三,表格里的“56.00”出现了两次,一次是单价,一次是金额,程序不知道取哪个;第四,“税率6%”和“税额3.36”需要合并理解。

这些问题靠正则表达式可以解决一部分,但只要发票版式稍微变化,正则就崩了。大模型更适合处理这种“格式不定但语义稳定”的情况。

4.2 一套好用的Prompt模板

我实践下来,Prompt不要写得太长,但必须把“输入是什么、输出是什么、边界是什么”说清楚。下面是我常用的模板,你可以直接复制改字段:

你是一个文档信息结构化助手。我会给你一段OCR从图片中识别出的原始文本。 原始文本可能存在:错别字、多余空格、换行混乱、表格行列错位、字段遗漏。 请你根据以下要求整理信息: 1. 从文本中提取以下字段: - 发票号码 - 开票日期 - 购买方名称 - 销售方名称 - 商品明细列表(每个商品包含名称、数量、单价、金额、税率、税额) - 金额(不含税) - 税额 - 价税合计 2. 如果原文中某个字段无法确定,字段值为空字符串,不要编造。 3. 金额保留两位小数,日期格式统一为YYYY-MM-DD。 4. 仅输出一个JSON对象,不要输出任何解释性文字,不要使用markdown代码块。 原始OCR结果: {raw_ocr_text}

这里最关键的一点是“不要编造”。大模型在不确定时倾向于“补全”信息,在发票识别中这是致命的,可能出现一个号码被填错、一张发票被虚构出一个不存在的商品。必须明确告诉它,原文本里没有的就留空,让后续人工复核。

另外,接口调用时把temperature参数设为0或者接近0,减少随机性。如果接口支持response_format约束为JSON对象,也一并打开,这样可以极大降低输出非JSON格式的概率。

4.3 校验环节:大模型也会漂移

大模型输出不是每次都稳定,偶尔会出现字段名不一致、JSON里多一个逗号、甚至把商品明细塞到别的地方。因此我用一个双层校验:第一层是JSON语法解析,用Python的json.loads,解析失败就自动重试一次,重试时在Prompt里强调“请严格输出合法JSON”;第二层是业务校验,根据文档类型检查关键字段是否齐全、金额是否合理。

例如发票场景,我会检查“价税合计”是否等于“金额 + 税额”的近似值;“开票日期”是否符合日期格式;“商品明细”是否至少有一条记录。合同场景会更简单一些,主要检查标题、合同编号、甲方乙方、签订日期是否存在。如果校验不通过,把错误信息拼到新的Prompt里,让大模型基于上次输出做修正,而不是直接重新跑一遍,这样更省token,准确率也更高。

我自己在实际项目中,会把所有校验不通过的图片单独放进一个“需人工复核”目录,而不是直接报错中断。这个策略很实用,因为批量处理时只要有一张图识别失败,整个流程卡住就很烦。宁可让它先跳过、标记,最后统一人工看。

5. 从一张图到一份结构化JSON:完整工作流代码骨架与实测

5.1 一个真正能跑起来的完整脚本

下面这段脚本就是“最小可运行版”。它完成的事情是:读一张图片,OCR识别,整理成文本,交给大模型接口,解析返回的JSON,打印并保存。你可以把它保存成ocr_workflow.py跑跑看。

import json import sys import requests from paddleocr import PaddleOCR # 初始化OCR ocr = PaddleOCR(use_angle_cls=True, lang='ch', show_log=False) LLM_API_URL = "http://127.0.0.1:8000/v1/chat/completions" LLM_MODEL = "local-model" PROMPT_TEMPLATE = """ 你是一个文档信息结构化助手。我会给你一段OCR从图片中识别出的原始文本。 原始文本可能存在:错别字、多余空格、换行混乱、表格行列错位、字段遗漏。 请提取发票号码、开票日期、购买方名称、销售方名称、商品明细列表、 金额(不含税)、税额、价税合计等字段。 如果字段无法确定,请填空字符串,不要编造。 仅输出一个JSON对象,不要输出任何解释性文字。 原始OCR结果: {raw_ocr_text} """ def ocr_image(image_path: str) -> str: result = ocr.ocr(image_path, cls=True) lines = [] if result and result[0]: for item in result[0]: text = item[1][0] lines.append(text) return "\n".join(lines) def call_llm(raw_text: str) -> dict: prompt = PROMPT_TEMPLATE.format(raw_ocr_text=raw_text) payload = { "model": LLM_MODEL, "messages": [ {"role": "system", "content": "你是一个严谨的文档结构化助手。"}, {"role": "user", "content": prompt} ], "temperature": 0.1, "response_format": {"type": "json_object"} } resp = requests.post(LLM_API_URL, json=payload, timeout=120) resp.raise_for_status() content = resp.json()["choices"][0]["message"]["content"] return json.loads(content) def process_image(image_path: str) -> dict: raw_text = ocr_image(image_path) return call_llm(raw_text) if __name__ == "__main__": image_path = sys.argv[1] output_path = sys.argv[2] if len(sys.argv) > 2 else "result.json" result = process_image(image_path) print(json.dumps(result, ensure_ascii=False, indent=2)) with open(output_path, "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2)

如果你本地的大模型接口不支持response_format这个参数,就去掉payload里那一行,一般不影响使用。如果你的接口不是OpenAI兼容格式,改成你实际调用的请求体格式就行。

5.2 处理PDF和多页合同时的调整

合同通常不是单张图片,而是十几页PDF。这时候要先做一次PDF转图片,再逐页识别,最后汇总成一个JSON数组。转图片我常用pymupdf(也就是fitz),因为它速度快、使用简单。

import fitz def pdf_to_images(pdf_path: str, dpi: int = 200, output_prefix: str = "page"): doc = fitz.open(pdf_path) paths = [] for i, page in enumerate(doc): pix = page.get_pixmap(dpi=dpi) out_path = f"{output_prefix}_{i + 1}.png" pix.save(out_path) paths.append(out_path) return paths

逐页识别完成后,有两个选择:一是每一页分别让大模型提取信息,最后合并;二是把整份合同OCR文本拼接成一个大文本,一次性交给大模型。我建议根据内容多少决定。如果合同是标准模板,甲方乙方信息在第一页,条款在中后页,可以先逐页识别,再把“页码”信息也传给大模型,让它知道什么信息出现在第几页。这样对后续定位条款内容非常有帮助。

5.3 实测效果与几个失败案例

我拿五十张发票样张和一份三十页的模拟合同测过这套链路。发票场景下,字段级抽取值准确率大约在九成出头,“价税合计”这种关键字段的准确率会更高一点;合同场景下,因为本质上是从大段文字里抽取信息,准确率取决于条款表述是否标准,整体可用性很高。但必须实话实说,离“全自动无人复核”还有距离。

失败案例集中在三类:第一类是印章正好盖在关键数字上,比如发票号码被红色印章覆盖,OCR识别成乱码,大模型也无法准确推断;第二类是非常规版式,比如横版发票、多栏合同、带骑缝章的扫描件,检测框会乱;第三类是小票和手写单据,字体极度不规则,大模型再强也难以正确处理。对这些情况,我的策略是:识别置信度低的片段保留在原始文本里,同时在输出中增加一个low_confidence_fields字段,提示哪些字段可能是错的,方便人工复核。

6. 折腾一个多月后,我想分享的避坑清单和效率心得

6.1 最容易踩的五个坑

坑现象原因解决办法
表格识别串行发票明细中金额成了税额普通OCR只能输出文本流,丢失行列关系用表格结构识别,或让大模型按上下文重建表格
“0/O”“1/l”混淆金额、账号识别错误小字号字体中相似字符难区分用价税合计大写校验;低置信度字段人工复核
OCR结果顺序混乱合同条款被切得七零八落检测框坐标排序不对按坐标先排序再输出;多栏文档用版面分析
大模型编造字段发票号码被“推理”成另一个数字Prompt没有强调不要编造Prompt明确“不确定就留空”;温度设为0
长文本超上下文三十页合同无法一次性处理单次输入长度超限分页识别,分批交给大模型,最后合并

6.2 把工作流固化成“一键脚本”

当你确认某类文档的识别效果稳定之后,建议把整个流程固化成一个带命令行入口的脚本。输入是目录,输出是文件夹下的JSON和汇总的CSV,日志里记录每张图的识别状态和耗时。这样财务同事不需要懂技术,双击一下就能把整个目录的发票全部处理完。

具体做法很简单:用Python的argparse接收目录参数;遍历目录下所有图片和PDF;对每个文件调用处理函数;结果写进以文件名命名的JSON;最后用csv模块把所有结果汇总成一张表。如果某张图片处理失败,不要停,记录错误后继续下一个文件。

6.3 给刚开始尝试的人一点实在建议

如果你想复现这套工作流,我的建议是先不要追求一步到位。先拿十张不同类型的发票,跑通“图片 → OCR → 大模型 → JSON”的最小链路,确认大模型输出的JSON格式稳定,再去调识别参数和处理PDF。等这套流程跑顺了,再扩展合同、多页文档等复杂场景。我最终能总结出这些经验,不是靠一次规划出来的,而是靠一次次跑偏、返工、记录攒出来的。尤其是Prompt的调整,每改一次都要保留一份样本输出,这样才知道改动到底有没有用。

最后再分享一个小习惯:我会把每次跑完的结果目录按日期命名,原始图片、OCR中间文本、大模型输出、最终JSON四层文件都保留下来。这样一旦后续发现某张发票数据有误,可以回溯问题出在OCR、Prompt还是校验环节。这个习惯帮我在批量处理上百张文件时省了很多排查时间,强烈建议你也保留。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询