1. 从一张发票扫描件说起:OCR 加大模型到底能解决什么
发票、合同、快递单、体检报告,这些扫描件或手机拍出来的图片,本质上是「像素」而不是「数据」。财务想把它录进系统,法务想检索里面的条款,运营想统计金额和日期,靠人工一个字一个字敲,慢且容易错。传统 OCR 能帮你把字认出来,但认出来是一回事,能不能变成程序能直接用的结构化 JSON 又是另一回事。
我先把这条链路拆清楚:第一步是 OCR,把图片变成带坐标的文字块,比如「发票号码」在左上角,「价税合计」在右下角;第二步是大模型,把这些散落的文字块按语义拼回字段,输出{"invoice_no": "...", "total_amount": ...}这样的 JSON。很多人卡在第二步——OCR 结果是一堆带[x,y]坐标的碎片,直接丢给模型,它要么把表格读串行,要么把金额和税额搞混。
这篇要解决的就是「一次跑通」:给你可复制的 OCR 调用配置、大模型提示词模板,以及用 TaoToken 统一 Key 把两步串起来的验证动作。适合谁?做票据识别、合同要素抽取、档案数字化的后端和算法同学,以及想快速验证文档解析链路的独立开发者。核心检索词就三个:OCR 精准识别、大模型结构化还原、TaoToken 统一 Key。读完你能拿到一份能直接改参数就跑的代码,而不是又一篇讲原理的科普。
2. TaoToken 前置准备:一个 Key 打通 OCR 后处理的大模型调用
2.1 为什么这里需要统一 Key
OCR 那一步通常用本地库或者云服务,问题不大。真正麻烦的是第二步的大模型调用:你可能今天试 DeepSeek,明天想换 Qwen 做对比,后天又要上 Claude 处理复杂合同。每换一个模型就换一套 SDK、换一个 Key、换一种请求格式,代码里到处是 if-else。TaoToken 的价值就在这——它提供统一的 API 通道,Base URL 和 Key 不变,只改model字段就能切换后端模型,特别适合做「同一份 OCR 结果跑多个模型比效果」这种验证。
官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台生成 Key。API 地址是 https://taotoken.net/api ,注意这个不带 UTM 参数,直接填进代码里。
2.2 拿 Key 和确认模型 ID
登录后进控制台,找到 API Keys 页面新建一个 Key,复制出来先存到环境变量里,别硬编码进代码。模型 ID 这块要注意:TaoToken 的模型名和各家官方可能略有差异,以控制台或文档里列出的为准。文档地址:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果你只是想先验证链路通不通,用模型对话页面手动发一条请求最快:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
2.3 环境变量配置
我习惯把 Key 和 Base URL 都放环境变量,这样换机器、换项目都不用改代码。Linux/macOS 下:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Windows PowerShell:
$env:TAOTOKEN_API_KEY="sk-你的key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"注意:Key 泄露等于别人能花你的额度,别提交到 Git。用
.env文件的话记得加进.gitignore。
2.4 依赖安装
OCR 我用 PaddleOCR,中文票据识别效果稳,本地跑不花钱。大模型调用用 OpenAI 兼容的 SDK,因为 TaoToken 走的是 OpenAI 兼容协议,这样代码最通用。
pip install paddleocr paddlepaddle openai python-dotenv如果你机器没有 GPU,PaddleOCR 会自动走 CPU,速度慢一点但能跑。第一次运行会自动下载模型权重,耐心等几分钟。
3. 可复制配置:OCR 提取加提示词模板的完整代码
3.1 OCR 提取文字与坐标
先写 OCR 部分,目标是把图片变成「文本 + 坐标」的列表。PaddleOCR 返回的结构里,rec_texts是识别出的文字,rec_polys是对应的四点坐标,我取左上角点作为位置标记。
from paddleocr import PaddleOCR import json ocr = PaddleOCR(use_angle_cls=True, lang="ch") def extract_ocr_blocks(image_path): result = ocr.ocr(image_path, cls=True) blocks = [] for line in result[0]: box = line[0] # 四点坐标 text = line[1][0] # 识别文本 score = line[1][1] # 置信度 x = int(box[0][0]) y = int(box[0][1]) blocks.append({"text": text, "x": x, "y": y, "score": round(score, 3)}) # 按从上到下、从左到右排序,方便模型理解阅读顺序 blocks.sort(key=lambda b: (b["y"] // 20, b["x"])) return blocks if __name__ == "__main__": blocks = extract_ocr_blocks("invoice_sample.jpg") print(json.dumps(blocks, ensure_ascii=False, indent=2))跑完你会看到类似这样的输出,每个文字块都带坐标:
[ {"text": "增值税电子普通发票", "x": 320, "y": 45, "score": 0.998}, {"text": "发票号码", "x": 60, "y": 120, "score": 0.995}, {"text": "24312000000012345678", "x": 160, "y": 120, "score": 0.991} ]3.2 大模型提示词模板
这一步是关键。OCR 结果直接丢给模型,它容易把相邻字段串起来。我的做法是把坐标信息保留,并在提示词里明确要求「按坐标判断位置关系」。下面这个模板可以直接用:
PROMPT_TEMPLATE = """你是一个文档结构化抽取引擎。下面是一张发票的OCR结果, 每个元素包含文本和左上角坐标[x, y]。 OCR结果: {ocr_json} 请完成两件事: 1. 根据坐标判断文本的位置关系,还原字段归属(比如"发票号码"右边的数字才是号码值)。 2. 输出严格的JSON,字段如下: {{ "invoice_code": "发票代码,没有则null", "invoice_no": "发票号码", "invoice_date": "开票日期,格式YYYY-MM-DD", "buyer_name": "购买方名称", "seller_name": "销售方名称", "total_amount": "价税合计,数字类型", "items": [{{"name": "货物名称", "amount": "金额"}}] }} 要求: - 只输出JSON,不要任何解释文字。 - 金额字段去掉货币符号和千分位,转成数字。 - 找不到的字段填null,不要编造。 """3.3 用 TaoToken 串联两步
把 OCR 结果塞进模板,通过 TaoToken 调用大模型。注意base_url和api_key都从环境变量读,model字段按你控制台里可用的模型填。
import os import json from openai import OpenAI client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) def structure_document(ocr_blocks, model="deepseek-chat"): prompt = PROMPT_TEMPLATE.format( ocr_json=json.dumps(ocr_blocks, ensure_ascii=False) ) resp = client.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], temperature=0, response_format={"type": "json_object"}, ) return resp.choices[0].message.content if __name__ == "__main__": blocks = extract_ocr_blocks("invoice_sample.jpg") raw = structure_document(blocks) data = json.loads(raw) print(json.dumps(data, ensure_ascii=False, indent=2))temperature=0是为了让输出稳定,response_format指定 JSON 能减少模型加废话的概率。如果你的模型不支持这个参数,去掉它,靠提示词约束也行。
3.4 配置文件形式(可选)
如果你用 Cline、CC Switch 这类工具做调试,配置可以写成 JSON。以 Cline 的 MCP 或自定义 provider 为例,核心三件套是 Base URL、Key、Model ID:
{ "provider": "openai-compatible", "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的key", "model": "deepseek-chat" }Codex 的auth.json同理,把base_url指向 TaoToken,api_key填你的 Key,模型 ID 按控制台填。三件套缺一不可,尤其是 Model ID 写错会直接报模型不存在。
4. 验证请求:跑通一次并检查结构化结果
4.1 先做最小连通性测试
别一上来就跑完整链路,先用一条最简单的请求确认 Key 和网络没问题:
from openai import OpenAI import os client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL"), ) resp = client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": "回复两个字:通了"}], ) print(resp.choices[0].message.content)如果打印出「通了」,说明 Key、Base URL、模型 ID 三件套都对。这一步能帮你把「配置问题」和「业务问题」分开,省得后面排查时抓瞎。
4.2 跑完整链路并校验
连通后跑 3.3 的完整代码,拿一张真实发票测试。我实测下来,一张普通增值税发票的 OCR 大概 2 到 5 秒,大模型结构化 3 到 8 秒,整体十秒内能出结果。输出类似:
{ "invoice_code": "031002100311", "invoice_no": "24312000000012345678", "invoice_date": "2024-06-15", "buyer_name": "某某科技有限公司", "seller_name": "某某商贸有限公司", "total_amount": 1130.00, "items": [ {"name": "办公用品", "amount": 1000.00} ] }4.3 怎么判断结果可信
光看输出不够,得校验。我一般做三件事:第一,金额字段做类型检查,确保是数字不是字符串;第二,日期用正则匹配\d{4}-\d{2}-\d{2};第三,把total_amount和 OCR 原文里的金额做一次字符串比对,对不上就标记人工复核。下面是个简单的校验函数:
import re def validate(data, ocr_blocks): errors = [] if not isinstance(data.get("total_amount"), (int, float)): errors.append("total_amount 不是数字") if data.get("invoice_date") and not re.match(r"\d{4}-\d{2}-\d{2}", data["invoice_date"]): errors.append("invoice_date 格式错误") ocr_text = "".join(b["text"] for b in ocr_blocks) if data.get("invoice_no") and data["invoice_no"] not in ocr_text: errors.append("invoice_no 在OCR原文中找不到,可能幻觉") return errors这个校验能挡住大部分模型幻觉。实测下来,加了校验之后,需要人工复核的比例从三成降到一成左右。
5. 常见报错排查:401、local proxy failed、reading choices 怎么解
5.1 401 Unauthorized
最常见的就是 Key 没读到或者填错。先确认环境变量真的生效了:
echo $TAOTOKEN_API_KEY如果输出为空,说明export没在当前终端生效,或者你换了终端窗口。另一个坑是 Key 前后带了空格或换行,复制的时候容易带上。代码里可以加一句os.getenv("TAOTOKEN_API_KEY").strip()兜底。还有一种情况是 Key 被禁用或额度用完,去控制台看一眼状态。
5.2 local proxy failed 或连接超时
这个报错通常是本地网络环境或代理设置导致的。检查你的HTTP_PROXY、HTTPS_PROXY环境变量是不是指向了一个不可用的地址:
env | grep -i proxy如果有值但你并不需要,先unset HTTP_PROXY HTTPS_PROXY再跑。另外确认base_url写的是https://taotoken.net/api,别多加斜杠或者写成别的路径。防火墙拦截 HTTPS 出站也会导致超时,换个网络环境试试能快速定位。
5.3 reading choices 报错或返回结构异常
resp.choices报IndexError或者NoneType,一般是请求根本没成功,返回体里没有choices字段。先把原始返回打出来看:
resp = client.chat.completions.create(...) print(resp.model_dump_json(indent=2))常见原因是模型 ID 写错,服务端返回了错误信息而不是正常补全。还有一种是你用了response_format={"type": "json_object"}但模型不支持,也会异常。去掉这个参数重试,如果好了就是它的问题。
5.4 OAuth 或鉴权相关报错
如果你在 Claude Code、Codex 这类工具里配置,报 OAuth 相关错误,通常是工具默认走了官方登录流程,而你要用的是 API Key 模式。检查工具的配置文件,确认auth类型是api_key而不是oauth,Base URL 指向 TaoToken。CC Switch 里切换 provider 时,记得把三件套(Base URL、Key、Model ID)都填全,只填 Key 不填 Base URL 会走默认官方地址,自然鉴权失败。
5.5 JSON 解析失败
模型返回的内容带了 ```json 代码块标记,json.loads会报错。两个办法:一是提示词里强调「只输出JSON,不要markdown标记」;二是代码里做清洗:
raw = raw.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip() data = json.loads(raw)实测下来,temperature=0加上明确的提示词,九成以上情况能直接解析。
6. 把链路固定下来:从验证到日常使用的几个动作
跑通一次不算完,要让它稳定可用,我建议做三件事。第一,把 OCR 和大模型两步封装成一个函数,输入图片路径、输出结构化字典,中间的错误都捕获并记录日志,这样批量处理时不会因为一张图失败就整个中断。第二,把提示词模板抽到单独的配置文件里,不同票种(增值税发票、火车票、合同)用不同模板,改起来不用动主逻辑。第三,建一个小测试集,十张到二十张标注好的图片,每次改提示词或换模型都跑一遍,用 4.3 的校验函数统计准确率,避免「感觉变好了」这种主观判断。
模型选择上,简单票据用轻量模型就够,复杂合同再上能力更强的。TaoToken 的好处是切换成本低,同一份 OCR 结果改个model字段就能横向对比。如果你要长期跑批量任务或者做 Agent 编排,可以看看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,按用量规划比单次调用更划算。日常调试和验证模型效果,用模型对话页面手动发请求最快:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。Key 管理和新建在控制台:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。
最后说个我踩过的坑:OCR 的坐标排序别偷懒用默认顺序,PaddleOCR 返回的顺序是按检测框来的,不一定符合阅读顺序。我一开始没排序,模型把「销售方」和「购买方」的名字对调了,排查半天才发现是输入顺序的问题。加上按y再按x排序之后,字段归属准确率明显提升。这个细节比换模型管用。