OpenAI SDK 多模态输入

QuibblerAgentQuibblerAgent 2026-11-08 约 20 分钟 16 次阅读

前五篇里进出模型的都是文字:提问用文字写,回答用文字拿。可真实世界的原始材料多半不是文字——购物小票是照片,会议纪要是录音,产品手册是 PDF,先靠人工把它们"翻译"成文字再喂给模型,翻译这一步常常比调用本身还费劲。多模态输入(Multimodal Input)就是来消灭这道工序的:图像、音频、文档原件直接塞进请求,模型自己看、自己听、自己读。

GPT-6 家族全系支持视觉输入,音频与文档输入覆盖大部分主力档位。本篇是《OpenAI Python SDK 从入门到实战》系列第 6 篇:第 4 篇的结构化输出、第 5 篇的工具调用都建立在纯文本之上,本篇把 input 的最后一层——多模态内容块——彻底展开。全文按"形态总览 → 图像输入 → 看图提取 → 音频输入 → 文件上传 → token 账 → 拍照记账实战 → 总结"组织。

1、三种输入形态总览

第 1 篇的首次调用里,input 是一个字符串;第 2 篇把它展开成消息数组时,每条消息的 content 也还是字符串。完整的形态还有第三层:content 可以是一个内容块列表,每块用 type 声明自己是什么——input_text 是文字,input_image 是图像,input_audio 是音频,input_file 是文件。文字配图片、录音跟提问,都只是往这个列表里多放几块。

同一份素材怎么进请求,取决于它现在在哪里:公网图片直接填 URL,本地文件先编码成 base64 内嵌,PDF 这类文档走 Files API 上传后拿 file_id 引用。三条路线如下图所示:

flowchart LR A[公网图片] -->|URL 直填| D[input_image 块] B[本地文件] -->|base64 内嵌| D B -->|base64 内嵌| E[input_audio 块] C[PDF 文档] -->|上传换 id| F[input_file 块] D --> G[content 列表] E --> G F --> G G --> H[一次请求]

图中三条路线殊途同归,最终都变成 content 列表里的一个块。三种姿势的适用场景对比如下:

形态 写法 适用场景 注意点
URL 引用 image_url 填 https 地址 图片已在公网可达 服务端必须访问得到,内网地址不行
base64 内嵌 编码后拼 data URI 本地文件、内网素材 体积膨胀约三分之一,请求体变大
文件 id 上传后拿 file_id 引用 PDF 等文档、一份多问 文件留在账户里,用完记得清理

选型的大方向:图小且公网可达,URL 最省事;本地文件量小,base64 直接内嵌;文档类素材或要反复引用的内容,走 Files API。还有一条隐性规则:内容块列表天然支持多块混排,一次请求里放两张图对比、一张图配一段录音说明,都是合法操作,不必拆成多次调用。下面逐个展开。

2、图像输入与 detail 档位

图像输入的载体是 input_image 块,最简单的形态是 URL 直填,与一段提问文字混排在 content 列表里。示例如下:

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6.1-terra",
    input=[{
        "role": "user",
        "content": [
            {"type": "input_text", "text": "这张图里有什么?一句话回答。"},
            {"type": "input_image",
             "image_url": "https://example.com/dashboard.png"},
        ],
    }],
)
print(response.output_text)

解读:input_image 与 input_text 在列表里平级,模型按混排顺序理解"这句问的是这张图"。image_url 除了 https 地址,也接受 data URI 形式的 base64,本地文件走的正是这条路。格式方面,图像支持 PNG、JPEG、WEBP 与非动图 GIF,尺寸过长的高图会被自动缩放。封装一个读本地图片的辅助函数,后文反复使用:

import base64

def image_block(path, detail="auto"):
    with open(path, "rb") as f:
        b64 = base64.b64encode(f.read()).decode()
    return {
        "type": "input_image",
        "image_url": f"data:image/jpeg;base64,{b64}",
        "detail": detail,       # 档位:low、high、auto
    }

解读:base64 编码把图片二进制变成纯文本,拼上 data:image/jpeg;base64, 前缀就成了一张"内嵌图片"。detail 是这里唯一的新旋钮,它控制服务端如何预处理图片,各档位说明如下:

档位 处理方式 token 量级
low 压到 512×512 内整体看一眼 固定 85
high 切成 512 像素方块逐块细看 85 起步,每块再加 170
auto 默认值,按图片尺寸自动选 由图片决定

档位选择的经验法则:判断"图里是猫还是狗",low 足够;要读小票上的小字、图表的坐标轴、截图里的报错信息,必须 high,压得太狠模型是真的看不清。部分新模型还支持 original 档位,按原图分辨率处理、token 消耗也最高,具体支持情况以图像与视觉指南为准。

3、看图提取结构化字段

多模态输入真正的高光时刻,是与第 4 篇的结构化输出组合:图片进、字段出,中间不需要任何人工转录。以名片识别为例,Pydantic 模型照旧定义,只把 input 里的文字换成图片块:

from pydantic import BaseModel

class Contact(BaseModel):
    name: str
    company: str
    phone: str | None = None   # 名片上没印电话时为 null

response = client.responses.parse(
    model="gpt-6.1-terra",
    input=[{
        "role": "user",
        "content": [
            {"type": "input_text", "text": "从名片图片提取联系人信息。"},
            image_block("card.jpg"),
        ],
    }],
    text_format=Contact,
)
print(response.output_parsed)

解读:text_format 约束的是输出侧,与输入是文字还是图片无关——约束解码对图像同样生效,字段缺失、类型漂移这些老问题被同一套机制锁死。phone 用 str 与 None 的可空类型,正是第 4 篇讲过的"可选字段"写法:名片上没印电话,模型就规规矩矩返回 null,而不是现场编一个。这套"看图填表"模式是流水线场景的利器:证件、表单、截图、单据,凡是"人看图抄字段"的环节,都值得试着交给它。

4、音频输入与转写场景

音频的进法与图像同构,但只有 base64 一条路:块类型换成 input_audio,data 与 format 收在嵌套字典里。一段会议录音配一句提问的示例如下:

import base64

def audio_block(path, fmt="mp3"):
    with open(path, "rb") as f:
        data = base64.b64encode(f.read()).decode()
    return {
        "type": "input_audio",
        "input_audio": {"data": data, "format": fmt},
    }

response = client.responses.create(
    model="gpt-6.1-terra",
    input=[{
        "role": "user",
        "content": [
            audio_block("meeting.mp3"),
            {"type": "input_text", "text": "用三点总结这段录音。"},
        ],
    }],
)
print(response.output_text)

解读三个要点。其一,format 目前支持 wav 与 mp3 两种,其余格式先本地转码再上传;音频单文件建议控制在 20 MB 以内,更长的先切分,base64 编码还会让请求体再膨胀约三分之一。其二,这种用法与"纯转写"不同:转写只要文字稿,走 SDK 的 audio.transcriptions 接口配专门转写模型更合适;input_audio 是让主线模型"边听边理解",直接回答关于录音的问题,总结、纪要、待办提取一步到位。其三,音频同样折算 token,量级按时长计,每秒几十个 token,比文本贵不少,长录音先剪掉静音与闲聊再上传。音频能力的模型支持范围以音频指南与官方模型文档为准。

5、文件上传与引用

PDF 与文档走第三条路:先用 Files API 把文件传到服务端,再在请求里用 file_id 引用。上传与引用一气呵成:

file = client.files.create(
    file=open("report.pdf", "rb"),
    purpose="user_data",       # 供模型读取的输入文件
)

response = client.responses.create(
    model="gpt-6.1-terra",
    input=[{
        "role": "user",
        "content": [
            {"type": "input_file", "file_id": file.id},
            {"type": "input_text", "text": "这份报告的三个核心结论是什么?"},
        ],
    }],
)
print(response.output_text)

解读:purpose 固定为 user_data,表示这是喂给模型用的输入文件;返回的 file 对象里,id 就是后续引用的凭证。顺带一提,input_file 也接受 base64 直传:file_data 填 data URI、配上 filename 即可,适合一次性的小文件,省一次上传调用;但只要同一份文件要问第二次,file_id 就完胜。file_id 最大的红利正是复用——文件已经在服务端,同一份 PDF 连问五个问题,每次只传 id 不重传内容,这也为多轮追问铺了路,系列的多轮记忆篇会接着展开。格式方面,除 PDF 外还支持 docx、pptx、csv、xlsx 等常见文档格式;单个文件上限 512 MB,但文档按页数折 token,几百页一次吃进去,账单会很直白,长文档先切分再问。用完的文件可以调 client.files.delete(file.id) 清掉,别把账户当网盘。细节以文件输入指南为准。

6、多模态的 token 账

多模态素材折算 token 各有官方口径。图像侧:low 档固定 85 个;high 档是 85 的基础费加每块 170,块数由切图决定;官方还给了快算公式——token 数约等于宽 × 高 ÷ 750。几个常见尺寸的量级对比如下:

素材 档位 折算 token
任意尺寸图片 low 固定 85
1080×1920 手机照片 high 约 2800
2048×4096 长截图 high 约 11000

一张手机照片折合两三千 token,看着不痛不痒,按第 2 篇整理的定价一算就见分晓:terra 输入每百万 token 约 1 美元,一张照片约 0.003 美元;换成 astra 旗舰是十倍单价,同一张约 0.03 美元。音频与文档没有这么好算的公式:音频按秒折 token、文档按页折 token,量级都是"一小段素材抵一大段文字"。要精确核账就打印 response.usage,input_tokens 里图像、音频、文档折算的 token 与文字一视同仁,现价以官方定价页为准。

多模态省钱的第一杠杆不是换模型,是压素材:判大类用 low 档、上传前压缩尺寸、长录音剪掉静音。这三招的降幅往往超过降档换模型,而且不伤质量底线。

7、拍照记账实战

把图像输入、结构化输出与 detail 档位组装成一个小而完整的工具:拍一张购物小票,直接产出结构化账单。先定义模型,明细用嵌套列表表达:

from pydantic import BaseModel, Field
from openai import OpenAI

class Item(BaseModel):
    name: str = Field(description="商品名称")
    amount: float = Field(description="单项金额,单位元")

class Bill(BaseModel):
    merchant: str = Field(description="商家名称,看不清写 未知")
    date: str | None = Field(description="消费日期,看不清为 null")
    total: float = Field(description="实付总金额,单位元")
    items: list[Item] = Field(description="消费明细")

主流程复用第 2 节的 image_block。小票上的金额是小字,detail 明确给 high:

client = OpenAI()

def parse_bill(path):
    response = client.responses.parse(
        model="gpt-6.1-terra",
        instructions="你是记账助手,从小票照片提取账单字段。",
        input=[{
            "role": "user",
            "content": [
                image_block(path, detail="high"),
                {"type": "input_text", "text": "提取这张小票的账单信息。"},
            ],
        }],
        text_format=Bill,
    )
    return response.output_parsed

bill = parse_bill("receipt.jpg")
if bill:
    for it in bill.items:
        print(f"{it.name:<8}{it.amount:>8.2f}")
    print(f"合计 {bill.total} 元 | {bill.merchant}")

运行输出类似:

拿铁            32.00
可颂            15.00
合计 47.0 元 | 某某咖啡

解读四个要点。其一,items 用 list[Item] 嵌套,第 4 篇讲过 SDK 会递归生成嵌套 schema,明细行数不定也不怕。其二,判空不可省:拒答或照片极端模糊时 output_parsed 为空,批量记账一条失败不能带崩整批。其三,成本上这类"看图抄字段"正是第 2 篇里 Luna 档的主场——视觉能力全系支持,月底几十张小票批量跑,把 model 换成 gpt-6-luna,账单再降一个数量级。其四,照片拍糊了别硬塞,重拍一张比反复重试更便宜。

小票上有姓名、卡号尾号等个人信息,上传即意味着交给云端处理;敏感票据先遮挡再拍摄,是成本最低的自我保护。

8、总结

本篇把 input 的最后一层展开:content 是内容块列表,input_image、input_audio、input_file 各管一种素材;三种进法——URL 直填、base64 内嵌、file_id 引用——按素材位置与复用需求选型;detail 档位是图像 token 的水龙头,读小字上 high、判大类用 low;图像配 text_format 是"看图填表"的最短路径,Files API 让一份文档经得起连续追问;所有素材都折成 token 计费,动手前先用宽 × 高 ÷ 750 估一估。对于手头囤着照片、录音与 PDF 的开发者,建议从"一张图加一个 Pydantic 模型"起步,当天就能把一条人工转录的流水线换成自动的。

留一道练习:把第 7 节的记账器升级三处——给 Bill 加支付方式字段,用 Literal["微信", "支付宝", "现金", "其它"] 枚举;把 date 从自由字符串改成带格式描述的约束;再拍一张手写账单试试泛化,观察哪些字段开始摇摆。做完换第 5 节的路线:上传同一份 PDF 连问两个问题,对比 file_id 复用与 base64 直传在第二次请求上的差别。

系列后续会把多轮记忆与成本控制补齐——到那时,"上周咖啡一共花了多少"这种追问,距离本篇的记账器只差一段会话历史。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

101