多模态输入:图片与语音怎么传给大模型
多模态大模型支持接收图片、音频等非文本输入。主流方案:图片通过 image_url content block 传给 vision 模型(如 gpt-4o);语音先用 Whisper API 转成文字再送入对话模型。两者均走 OpenAI 兼容接口,对接方式一致。
你可能是这么撞上这篇文章的:产品经理丢过来一个需求,“用户拍张截图就能自动生成工单描述”,或者”客服录音自动转文字存进工单系统”。你打开文档一看,vision 和语音转写居然不是同一套 API,一个塞 content 数组,一个是独立的 audio.transcriptions 端点,命名风格都不太一样。别慌,这篇把两条链路的坑一次说完,包括你大概率会踩的三个:base64 图片把上下文撑爆、Whisper 报 413、多轮对话里图片重复计费。
图片输入:两种传法
| 方式 | 适用场景 | 限制 |
|---|---|---|
| 公网 URL | 图片已有公开地址 | 模型需能访问该 URL |
| base64 编码 | 本地文件、私有图片 | 图片越大请求体越大,建议压缩到 1 MB 以内 |
| 文件上传(Files API) | 复用同一图片多次 | 需先上传获取 file_id |
支持格式:PNG、JPEG、WebP、GIF(静态帧)。最大分辨率因模型而异,通常 2048×2048 以内效果最佳。
三种方式怎么选,不用纠结太久,记住这个判断顺序就行:图片本来就在你自己的对象存储或 CDN 上、且地址长期有效 → 用公网 URL,最省事,请求体也小;图片是用户临时上传、本地生成、或者压根没有公网地址(比如企业内网截图)→ 只能 base64;同一张图要在一个会话里反复问好几轮(“这张图里的表格第二行是什么” “第三列呢”)→ 上传一次拿 file_id,后面每轮都引用它,省得每次都把几百 KB 的 base64 塞进请求体。实际项目里我见过最常见的翻车,是团队图省事全用 base64,图片一大,请求体轻松破 2MB,网关超时、或者触发反向代理的 client_max_body_size 限制,报的错却是一个不相关的 502,排查了半天才发现是图片没压缩。
Python:图片 URL 传入
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.lidayun.com/v1"),
)
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "图片里有什么?请详细描述。"},
{
"type": "image_url",
"image_url": {
"url": "https://example.com/sample.jpg",
"detail": "high", # low / high / auto
},
},
],
}
],
max_tokens=512,
)
print(resp.choices[0].message.content)
跑完这段代码,你应该在终端看到一段完整的自然语言描述,比如”图片中是一张办公室场景,桌上有一台笔记本电脑……”这类几十到几百字的文本,而不是空字符串或者报错。如果 resp.choices[0].message.content 是空的,先别怀疑模型能力,大概率是这三个原因之一:一是图片 URL 模型访问不到(内网地址、需要登录态的地址、防盗链拦截),二是 detail 设成 "low" 导致模型看不清细节只能给出模糊回答,三是图片本身确实信息量很少(比如纯色背景)。养成习惯:调试阶段先把 max_tokens 调大一点(比如 1024),避免回答被截断误判成”模型看不懂”。
detail 这个参数很多人第一次用会忽略,但它直接决定了这次调用花多少钱、模型看得多细。默认是 "auto",模型自己判断;如果你只是要一个粗略分类(“这张图是发票还是合同”),显式传 "low" 能省下不少 token,响应也更快;如果要做 OCR 级别的精读(提取表格里的具体数字),必须用 "high",否则模型经常会把数字看错或者干脆说”看不清”。
Python:本地图片 base64 传入
import base64, mimetypes
def image_to_data_url(path: str) -> str:
mime, _ = mimetypes.guess_type(path)
with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
return f"data:{mime};base64,{b64}"
data_url = image_to_data_url("/tmp/screenshot.png")
resp = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": "这张截图的错误原因是什么?"},
{"type": "image_url", "image_url": {"url": data_url}},
],
}
],
)
print(resp.choices[0].message.content)
这里有个很多人不知道的细节:base64 编码本身会让数据体积膨胀约 33%(Base64 是把每 3 字节原始数据编成 4 字节可打印字符),一张 1.5MB 的截图编码完差不多变成 2MB 的字符串,直接塞进 JSON 请求体。如果你的服务前面挂了 Nginx,默认 client_max_body_size 通常是 1MB,超了会直接 413,而且这个 413 是你自己网关返回的,跟大模型服务商没关系——排查时先看响应状态码是谁返回的,别一上来就怀疑 API key 或者模型。实际项目里我们的做法是:上传前用 Pillow 把长边压到 1600px 以内、JPEG 质量压到 85,这样绝大多数截图能压到 300KB 以内,肉眼几乎看不出差别,模型识别效果也不受影响(除非你需要读很小的文字)。
还有一个更容易被忽略的坑:多轮对话里如果每一轮都把同一张图片的 base64 塞进 messages 历史,上下文会指数级膨胀。比如你做了个”针对这张图连续追问”的功能,第一轮请求带一张 300KB 图片折算大约 400 token(detail=high 情况下更高),如果你把完整的 messages 历史(含图片)原样带入第二轮、第三轮,图片会被重复计费 N 次,五轮对话下来 token 消耗可能翻 5 倍,账单涨到你都不敢看。正确做法是:图片只在首轮传入,模型给出的描述文字进入历史记录留存,后续追问只带纯文本历史 + 新问题,不再重复带图;如果确实需要模型”记得”图片细节,用 Files API 上传一次拿 file_id,用引用代替重复编码。
语音输入:Whisper 转写
# 先转写音频,再送入对话模型
audio_resp = client.audio.transcriptions.create(
model="whisper-1",
file=open("/tmp/recording.mp3", "rb"),
language="zh", # 指定语言可提升准确率
)
transcript = audio_resp.text
print("转写结果:", transcript)
# 再用转写结果调对话模型
chat_resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": transcript}],
)
print(chat_resp.choices[0].message.content)
支持音频格式:mp3、mp4、mpeg、mpga、m4a、wav、webm;文件大小上限通常 25 MB。这个 25MB 的坎很多人第一次做语音功能会栽在这——一段 20 分钟的会议录音用 44.1kHz 立体声录制,随随便便就是 100MB+,直接调用会收到 413 Request Entity Too Large 或者服务商返回的 file too large 错误。解决办法不是压缩音质(会掉转写准确率),而是先用 ffmpeg 把音频转成单声道、16kHz 采样率(人声转写完全够用,Whisper 内部本身就是按 16kHz 处理的),命令大概是 ffmpeg -i input.mp3 -ac 1 -ar 16000 output.mp3,同样时长的文件能压缩到原来的四分之一左右;如果还是超限,就按静音切片,用 pydub 的 silence.split_on_silence 在停顿处切开分段转写,再把每段文字拼起来。
transcriptions.create 还有几个参数决定了转写质量,值得展开说说:
| 参数 | 作用 | 什么时候用 |
|---|---|---|
language | 指定源语言(ISO-639-1,如 "zh"、"en") | 明确知道语言时一定要传,不传的话模型要先猜语言,偶尔会把中文方言、夹杂英文的录音猜成别的语种,导致转写完全跑偏 |
prompt | 给模型一段上下文提示,比如专业术语、人名 | 转写行业黑话、产品名、生僻词时加上,比如 prompt="讨论内容涉及:算力、token、RAG、向量数据库",能显著减少专有名词被听写成同音字 |
response_format | json(默认)/ text / srt / vtt / verbose_json | 做字幕用 srt/vtt 直接带时间轴;只要纯文本用 text;需要每个词的时间戳做后续对齐用 verbose_json |
temperature | 转写的随机性,默认 0 | 基本不用改,0 就是最稳定、最贴近实际发音的结果 |
实际排查中常见的两个错误:一是 401 Unauthorized,先确认 OPENAI_API_KEY 有没有读到(很多人在 .env 里配了但没 load_dotenv() 或者进程没重启),再确认 base_url 有没有指错环境;二是转写结果里中文标点混乱、简繁混杂,这通常不是你的问题,是模型本身的输出习惯,如果对格式要求严格,加一步后处理用正则统一标点即可,不用怀疑是编码问题(response.text 本身就是 UTF-8 字符串,不需要你手动 decode)。
Node.js:图片 URL 传入
import OpenAI from "openai";
import fs from "fs";
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL ?? "https://api.lidayun.com/v1",
});
const resp = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "user",
content: [
{ type: "text", text: "图片里有什么?" },
{ type: "image_url", image_url: { url: "https://example.com/sample.jpg" } },
],
},
],
});
console.log(resp.choices[0].message.content);
detail 参数:控制分辨率与成本
| detail 值 | 行为 | token 消耗 |
|---|---|---|
"low" | 固定缩至 512×512 处理 | ~85 tokens |
"high" | 先缩至 2048×2048,再切 512 块 | 按块计费,约 170+ tokens/块 |
"auto" | 根据图片尺寸自动选择 | 推荐默认 |
常见问题
图片 URL 模型访问不到怎么办? 改用 base64 传入,或将图片托管到公网 CDN。内网/私有 URL 模型无法请求,会报错或返回空描述。
多张图片怎么传? 在 content 数组里追加多个 image_url block,顺序即为模型看图顺序,最多通常支持 10-20 张,具体看模型文档。
Whisper 转写准确率不高? 指定 language 参数(如 "zh")、提供 prompt(上下文词汇)可显著提升中文准确率。
更多接入基础见大模型 API 接入完全指南与接入教程专题。如需批量图片处理,请结合并发控制与速率限制;结构化提取图片信息可参考 JSON mode 使用。