Python Responses API视觉输入与本地金额校验最小实现

把发票图片交给视觉模型,确实能省掉手输;但"模型说是 128.00 元"不等于系统该付款。更稳妥的做法是:模型只产生候选字段,本地程序以 Decimal 重算含税金额、检查币种和上限,再把通过或拒绝的理由交给界面。你将得到一个可以复制的最小链路,也能把同样的门禁迁移到合同、收据和质检单。

OpenAI 的 Images and vision 文档说明 Responses API 可接收图像输入。本文用 REST 而非 SDK,目的是让请求、超时和错误分支对初学者可见。模型 ID 请以目标账号的 Models 页面为准;代码中的 gpt-6 是示例占位,调用前替换为你账户可用模型。
#mermaid-svg-3thsV6f2MnrgkRWV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3thsV6f2MnrgkRWV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3thsV6f2MnrgkRWV .error-icon{fill:#552222;}#mermaid-svg-3thsV6f2MnrgkRWV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3thsV6f2MnrgkRWV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3thsV6f2MnrgkRWV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3thsV6f2MnrgkRWV .marker.cross{stroke:#333333;}#mermaid-svg-3thsV6f2MnrgkRWV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3thsV6f2MnrgkRWV p{margin:0;}#mermaid-svg-3thsV6f2MnrgkRWV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3thsV6f2MnrgkRWV .cluster-label text{fill:#333;}#mermaid-svg-3thsV6f2MnrgkRWV .cluster-label span{color:#333;}#mermaid-svg-3thsV6f2MnrgkRWV .cluster-label span p{background-color:transparent;}#mermaid-svg-3thsV6f2MnrgkRWV .label text,#mermaid-svg-3thsV6f2MnrgkRWV span{fill:#333;color:#333;}#mermaid-svg-3thsV6f2MnrgkRWV .node rect,#mermaid-svg-3thsV6f2MnrgkRWV .node circle,#mermaid-svg-3thsV6f2MnrgkRWV .node ellipse,#mermaid-svg-3thsV6f2MnrgkRWV .node polygon,#mermaid-svg-3thsV6f2MnrgkRWV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3thsV6f2MnrgkRWV .rough-node .label text,#mermaid-svg-3thsV6f2MnrgkRWV .node .label text,#mermaid-svg-3thsV6f2MnrgkRWV .image-shape .label,#mermaid-svg-3thsV6f2MnrgkRWV .icon-shape .label{text-anchor:middle;}#mermaid-svg-3thsV6f2MnrgkRWV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3thsV6f2MnrgkRWV .rough-node .label,#mermaid-svg-3thsV6f2MnrgkRWV .node .label,#mermaid-svg-3thsV6f2MnrgkRWV .image-shape .label,#mermaid-svg-3thsV6f2MnrgkRWV .icon-shape .label{text-align:center;}#mermaid-svg-3thsV6f2MnrgkRWV .node.clickable{cursor:pointer;}#mermaid-svg-3thsV6f2MnrgkRWV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3thsV6f2MnrgkRWV .arrowheadPath{fill:#333333;}#mermaid-svg-3thsV6f2MnrgkRWV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3thsV6f2MnrgkRWV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3thsV6f2MnrgkRWV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3thsV6f2MnrgkRWV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3thsV6f2MnrgkRWV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3thsV6f2MnrgkRWV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3thsV6f2MnrgkRWV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3thsV6f2MnrgkRWV .cluster text{fill:#333;}#mermaid-svg-3thsV6f2MnrgkRWV .cluster span{color:#333;}#mermaid-svg-3thsV6f2MnrgkRWV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3thsV6f2MnrgkRWV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3thsV6f2MnrgkRWV rect.text{fill:none;stroke-width:0;}#mermaid-svg-3thsV6f2MnrgkRWV .icon-shape,#mermaid-svg-3thsV6f2MnrgkRWV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3thsV6f2MnrgkRWV .icon-shape p,#mermaid-svg-3thsV6f2MnrgkRWV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3thsV6f2MnrgkRWV .icon-shape .label rect,#mermaid-svg-3thsV6f2MnrgkRWV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3thsV6f2MnrgkRWV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3thsV6f2MnrgkRWV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3thsV6f2MnrgkRWV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一致且未超限
异常
授权票据图片
base64视觉请求
模型返回JSON候选
Decimal本地复算
进入人工确认
拒绝并显示原因

环境准备与完整代码

Python 3.10+,安装 pip install requests,设置 export OPENAI_API_KEY='...',再运行 python receipt_gate.py receipt.jpg。示例未在本次任务中实际调用线上 API;已在本机完成语法检查。

python 复制代码
import base64, json, os, sys
from decimal import Decimal, InvalidOperation
import requests

API = "https://api.openai.com/v1/responses"
def gate(data: dict) -> tuple[bool, str]:
    try:
        subtotal = Decimal(str(data["subtotal"]))
        tax = Decimal(str(data["tax"]))
        total = Decimal(str(data["total"]))
    except (KeyError, InvalidOperation):
        return False, "字段缺失或金额不是数字"
    if data.get("currency") != "CNY": return False, "仅接受CNY票据"
    if total > Decimal("2000"): return False, "超过自动处理上限"
    if abs(subtotal + tax - total) > Decimal("0.01"):
        return False, "小计、税额与总额不一致"
    return True, "允许进入人工确认"

def main(path: str) -> None:
    key = os.environ.get("OPENAI_API_KEY")
    if not key: raise SystemExit("请设置OPENAI_API_KEY")
    image = base64.b64encode(open(path, "rb").read()).decode()
    prompt = "识别票据,严格只返回JSON:subtotal,tax,total,currency"
    body = {"model":"gpt-6", "input":[{"role":"user","content":[
        {"type":"input_text","text":prompt},
        {"type":"input_image","image_url":"data:image/jpeg;base64,"+image}]}]}
    try:
        r = requests.post(API, headers={"Authorization":f"Bearer {key}","Content-Type":"application/json"}, json=body, timeout=30)
        r.raise_for_status()
        text = r.json()["output"][0]["content"][0]["text"]
        ok, reason = gate(json.loads(text)); print({"approved": ok, "reason": reason})
    except (requests.RequestException, KeyError, json.JSONDecodeError) as e:
        print({"approved": False, "reason": f"调用或解析失败: {e}"})

if __name__ == "__main__":
    if len(sys.argv) != 2: raise SystemExit("用法: python receipt_gate.py receipt.jpg")
    main(sys.argv[1])

关键点有三个:不要用浮点数算钱;不要允许模型决定"批准";不要把原始票据和密钥写进日志。成功时预期输出 {'approved': True, 'reason': '允许进入人工确认'},仍不是自动付款。

常见失败:一是图片太大或格式不被账户支持,先压缩并核对视觉文档;二是模型返回 Markdown 围栏而非 JSON,应在生产中启用结构化输出或做更严格解析;三是网络超时,代码已设 30 秒,生产环境还应重试并加幂等键;四是发票有多税率,不能沿用单一小计公式。

它适合低风险预录入和人工前置核验,不适合绕过财务审批、税务判断或未获授权的证件处理。下一步可把字段置信度、供应商白名单和审计 ID 写入数据库。5分钟练习:把 2000 改成你的团队阈值,并给 gate 增加"日期不得晚于今天"的规则。

把它接进真实页面

页面至少应该同时展示原图缩略图、模型原始候选、本地复算值和拒绝原因。不要只用一枚绿色"已通过"徽标:使用者需要能看出是"识别到 128.00""规则算出 128.00",还是"金额相差 0.02"。若模型无法读取图片,保留上传记录并让用户补图;若规则不通过,允许修正字段但要求说明原因。对发票号、身份证号等敏感字段,日志只保留哈希或局部掩码,原图按组织保留期自动删除。批量场景可先排队、限制单用户并发,并把同一文件的 SHA-256 作为幂等键,避免重试造成重复报销草稿。

官方文档:Images and vision。你的业务里,哪一个AI提取字段必须由本地规则二次确认?

关注「蜗牛聊AI」,一起看懂技术变化背后的真正机会。


本文首发于 java4u.cn,转载请注明出处。

相关推荐
Autumn_ing1 小时前
2026产品经理Agent实测:WorkBuddy、QoderWork、Codex、墨刀AI客户端
人工智能·产品经理·墨刀·codex·workbuddy·qoderwork
Mr_star_galaxy1 小时前
【Agent】LangChain聊天模型 -- 调用工具
人工智能·langchain
海宇数据1 小时前
零信任架构实战:基于海宇车辆vin码查车辆信息详版构建自动化车险承保定级网关
人工智能·架构·自动化·php
程序猿_极客1 小时前
【免费】分享一套优质的基于Java的智慧养老院管理系统的设计与实现(带可视化),源码+文档+视频详解(讲解)
java·spring boot·后端·养老院管理系统
Python图像识别1 小时前
35-【2027毕设】YOLO11中草药检测识别系统 - Python完整源码+PyQt5界面+训练模型+数据集
python·qt·课程设计
a努力。1 小时前
LangGraph:破解复杂Agent编排难题
人工智能
马六六i1 小时前
市面上专业的IP驱动产业新场景新工具哪家好
网络·人工智能·python·tcp/ip
小弥儿1 小时前
GitHub今日热榜 | 2026-10-09:PS5 移植登顶,AI 创作工具占三席
人工智能·学习·开源·github
ly76891 小时前
从 TF-IDF 到 BM25:Elasticsearch 相关性评分的字段长度归一化与 boost 调参边界
java·elasticsearch·query dsl·bm25·相关性评分