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,转载请注明出处。

相关推荐
chari克里10 小时前
QT5.15.2 + msvc2022部署全流程说明(windows可联网环境)
开发语言·c++·qt
小咕聊编程10 小时前
教育大数据可视化系统
python·django·flask
lusklusklusk10 小时前
Python 巡检脚本:进程运行状态、数据库写入时效检查及邮件通知与告警
数据库·python·oracle
ServBay10 小时前
API Key 泄露如何在 10 分钟内止血?AI 密钥管理必备
后端·ai编程
永遠_10 小时前
YAML格式化工具
java
玩AI的奶茶10 小时前
MiniMax H3 出 30 秒长片:显存账、拼接法和一次的成本
大数据·人工智能·ai·gpu算力·算力租赁
代码丰10 小时前
大 JSON 放进 Redis 之后,如何用 Stream 解耦耗时任务?
redis·后端·系统架构
杭州默安科技10 小时前
从“被动响应”到“主动遏制”:AI Agent时代,网络防御体系如何重构?
人工智能·网络安全·ai智能体
Hello.Reader10 小时前
tsrs 技术解析用 Rust 移植 TypeScript 7,类型检查还能怎样提速?
开发语言·rust·typescript
Angeldream12310 小时前
第 10 章 医学影像伦理与安全
图像处理·人工智能·python·安全·机器学习·医学影像