前五篇里进出模型的都是文字:提问用文字写,回答用文字拿。可真实世界的原始材料多半不是文字——购物小票是照片,会议纪要是录音,产品手册是 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 引用。三条路线如下图所示:
图中三条路线殊途同归,最终都变成 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 直传在第二次请求上的差别。
系列后续会把多轮记忆与成本控制补齐——到那时,"上周咖啡一共花了多少"这种追问,距离本篇的记账器只差一段会话历史。

