多模态开发(02):图像输入与结构化提取

QuibblerAgentQuibblerAgent 2026-10-30 约 19 分钟 3 次阅读

在 Agent 系列里我们用十几行代码让模型看过一次图:把截图转成 base64 塞进消息就完事。那样跑通 demo 够了,可一旦进了生产,问题接踵而来——为什么同一张图三个厂商标价差三倍?为什么手机拍的图在服务端被"躺平"识别?为什么换了图片顺序答案就变了?这篇是《多模态开发实战》第 2 篇,把图像输入这件事按工程口径完整拆一遍:消息格式三家对照、token 成本核算、细节度控制、多图顺序,最后落一个"截图结构化提取"的完整实战。文中计费与格式均为 2026 年 10 月各家官方文档口径。


1、图像输入的两种方式

图像进消息只有两条基本通路:给一个模型侧能拉取的 URL,或者把字节流 base64 内嵌进请求体。OpenAI 两者的字段位置相同——都写在 image_url 里,前者放链接,后者放 data: 协议的 Data URL;Anthropic 把它们区分为 source.type 的 url 与 base64 两种取值;Gemini 的旧 API 里对应 file_data 与 inline_data。

以 OpenAI Responses API 为例,同一张图两种传法如下:

{"type": "input_image", "image_url": "https://cdn.example.com/ui.png"}
{"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo..."}

两种写法能力完全等价,选择只取决于工程约束。按四个维度决策:

维度 URL 直传 base64 内嵌
请求体大小 只传链接,几乎为零 字节膨胀约 33%
隐私 图片须公网可访问 数据不出这次请求
来源 依赖图床可用性 本地文件、用户上传直接用
多轮会话 每轮重传链接即可 每轮重发全量字节

经验法则:图片已经在公网(OSS、CDN、网页截图服务)就用 URL;图片来自用户上传或内网系统就 base64。base64 的多轮膨胀问题有第三条路——三家的 Files API 都支持"上传一次、拿 file_id 反复引用",Anthropic 官方文档明确推荐多轮 Agent 场景走这条路,能把每轮请求体从几十 MB 压回几 KB。

2、三家图像消息结构对照

三家的消息骨架都是"一条 user 消息里塞多个内容块",但字段命名各不相同,完整定义见各家官方图像指南。先看 OpenAI(Responses API):

{
  "role": "user",
  "content": [
    {"type": "input_text", "text": "描述这张界面截图"},
    {"type": "input_image", "image_url": "https://cdn.example.com/ui.png", "detail": "high"}
  ]
}

图像块类型叫 input_image,detail 是细节度开关;老的 Chat Completions API 用的是 image_url 类型加一层嵌套对象 {"url": ..., "detail": ...},大量存量代码还是这个形状。Anthropic 的写法则是"图像块 + source 子对象":

{
  "role": "user",
  "content": [
    {"type": "image", "source": {
      "type": "base64",
      "media_type": "image/png",
      "data": "iVBORw0KGgoAAAANSU..."
    }},
    {"type": "text", "text": "描述这张界面截图"}
  ]
}

注意 Anthropic 的图像块没有 detail 参数——后面会讲为什么。另外 media_type 只接受 JPEG、PNG、GIF、WebP 四种(动图取首帧),OpenAI 与 Gemini 各自多出非动图 GIF 与 HEIC/HEIF 支持,格式转换必须在进消息前做掉。Gemini 走 contents → parts 两层,内嵌图像用 inline_data:

{
  "contents": [{
    "role": "user",
    "parts": [
      {"inline_data": {"mime_type": "image/png", "data": "iVBORw0KGgoAAAANSU..."}},
      {"text": "描述这张界面截图"}
    ]
  }]
}

Gemini 2026 年新推的 Interactions API 把结构简化成了顶层 input 数组({"type": "image", "data": ...}),但现有 SDK 与生态仍以 generateContent 为主,写兼容代码按上面这个口径来。直接对接原生 API 要维护三套结构,用 LangChain 可以统一成 OpenAI 风格:

from langchain_core.messages import HumanMessage

msg = HumanMessage(content=[
    {"type": "text", "text": "描述这张界面截图"},
    {"type": "image_url", "image_url": {"url": "data:image/png;base64," + b64}},
])
# ChatOpenAI / ChatAnthropic / ChatGoogleGenerativeAI 都接受这种写法

三家集成各自负责把 image_url 块翻译成原生格式:Anthropic 侧自动转成 base64 source,Gemini 侧转成 inline_data。这套统一口径贯穿本系列后续所有代码。

3、图像 token 成本核算

图像按 token 计费,但三家换算口径完全不同,这是多模态账单失控的头号原因。OpenAI 分两代:tile 系模型(gpt-4o、gpt-4.1、o 系、gpt-5、gpt-5.1)把图缩进 2048×2048、短边压到 768px 以内后按 512×512 切片,费用为"基础 token + 每片单价×片数",gpt-4o 是 85+170×片数,gpt-5.1 降到 70+140×片数;新的 patch 系模型(gpt-5.x 后期与 gpt-6-astra)改按 32×32 像素块计数再乘模型系数,超过 30000 个块直接拒绝请求。

Anthropic 的公式最直白:每 28×28 像素记一个视觉 token,即 ⌈宽/28⌉×⌈高/28⌉,随后按模型分档封顶——标准档(200k 上下文的常规模型)长边超 1568px 自动降采样、token 封顶 1568;高分辨率档(Claude 4.7 及之后)长边放到 2576px、封顶 4784,细节见 Anthropic Vision 文档。Gemini 则是"两边 ≤384px 收 258 token 一口价,否则按 768×768 切片、每片 258",切片数由短边决定(短边除以 1.5 得步长再数格子),规则见 Gemini 图像理解文档。

同一张 1920×1080 的截图,三家账单如下:

口径 计费公式 该图 token
OpenAI gpt-4o(high) 85 + 170×6 片 1105
OpenAI gpt-5.1(high) 70 + 140×6 片 910
Anthropic 标准档 降采样后计块,封顶 1560
Anthropic 高分辨率档 69×39 块 2691
Gemini 258×6 片 1548

数字差距不大,但成本敏感度在分辨率维度:Anthropic 高分辨率档处理 4K 图会到 4784 token 封顶值,而低细节档(下一节)只要它的零头。Anthropic 文档给过一笔账:按 Haiku 4.5 每百万输入 token 1 美元计,1000×1000 的图每千张约 1.3 美元——单张不痛,日跑十万张截图的流水线就是每月几百美元的事。设计图像管线时先估单图 token,再乘日量级。

4、细节度控制策略

很多任务根本用不到全分辨率:判断截图是什么页面、有没有报错弹窗、主色调是什么,512×512 的低档视角足够。OpenAI 的 detail 参数有四个取值:low(固定低费率,gpt-4o 只要 85 token)、high(完整切片)、auto(默认,按图和问题自动选)、original(不缩放原图)。Gemini 3 起提供 media_resolution 参数压低每图 token 上限。Anthropic 没有参数可调——发送的分辨率就是细节度,想省钱就在客户端自己缩图。

厂商 参数 低档行为
OpenAI detail 固定低费,忽略原分辨率
Gemini media_resolution 限制每图 token 上限
Anthropic 无参数 客户端预缩放

什么时候低档够用?给一个判断标准:答案不依赖小字和密集细节时用低档。页面类型分类、有无弹窗、图表类型识别、UI 整体布局描述,低档损失很小;读表格数字、识别小号正文、数密集图标、比对两图差异,必须高档。生产管线里不建议留 auto——同一批图会因尺寸不同走不同档位,成本与行为都不可复现,显式声明档位是更稳的选择。拿不准就做一次 A/B:同一批图低高档各跑一遍比准确率,低档达标就全量切低档,成本常能降一个数量级。

5、多图输入与顺序

多图输入没有新语法——content 数组里放多个图像块即可,限额比想象中宽松:OpenAI 每请求 1500 张,Gemini 3600 张,Anthropic 视模型 100 到 600 张(超过 20 张时会收紧单图尺寸上限,长边约 2000px)。真正微妙的是顺序敏感性:Anthropic 官方建议图像放在文本之前(长文档同理,模型对开头内容注意力更足);Gemini 官方建议恰好相反,单图场景文本放图前。两家指南方向相反,说明这是经验规律而非硬约束。

工程上别赌任何一家的默认偏好,做两件事:

  1. 给每张图配文字标签:图前插 Image 1:、Image 2: 短文本块,prompt 里显式引用编号而非位置
  2. 顺序敏感任务上线前调换图片顺序测试一遍,答案漂移就说明 prompt 依赖位置,改用编号引用

6、截图结构化提取实战

第一个完整实战:把一张产品截图变成结构化数据——页面标题、主要操作、可交互元素清单。这正是 UI 走查、竞品分析、自动化测试数据准备的通用底座。延续 Agent 系列的 LangChain 口径,先定义输出 schema:

from typing import Literal
from pydantic import BaseModel, Field

class UIElement(BaseModel):
    element_type: Literal["button", "input", "link", "text", "image"] = Field(
        description="元素类型")
    text: str = Field(description="元素上的可见文字,没有则空字符串")
    location: str = Field(description="位置描述,如「顶部导航栏右侧」")

class ScreenshotInfo(BaseModel):
    page_title: str = Field(description="页面或对话框标题")
    primary_action: str = Field(description="页面的主要操作目的")
    elements: list[UIElement] = Field(description="识别到的可交互元素")
    language: str = Field(description="界面语言,如 zh-CN、en")

Pydantic 模型就是模型的输出契约,Field(description=...) 会进入约束描述,实测对字段对齐率帮助明显。调用侧把图像消息与 with_structured_output 组合:

import base64
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

llm = ChatOpenAI(model="gpt-5.1", temperature=0).with_structured_output(ScreenshotInfo)

def extract(path: str) -> ScreenshotInfo:
    b64 = base64.b64encode(open(path, "rb").read()).decode()
    msg = HumanMessage(content=[
        {"type": "text", "text": "提取这张产品截图的结构化信息,逐个识别可交互元素。"},
        {"type": "image_url", "image_url": {
            "url": f"data:image/png;base64,{b64}", "detail": "high"}},
    ])
    return llm.invoke([msg])

info = extract("checkout.png")
print(info.page_title, len(info.elements))  # 收银台 14

结构化输出与图像输入完全正交:with_structured_output 走 tool calling 约束输出格式,图像块照常作为输入参与理解。detail: "high" 在这里不能省——按钮上的小字是提取目标,低档会把 14 个元素认成 8 个。反过来做页面分类时把这个参数改成 low,单图成本立降到约十分之一。

7、常见坑与规避

图片过大被拒。各家硬上限:Anthropic 单图 8000×8000px、base64 后 10 MB(Bedrock 5 MB),inline 的 Gemini 请求总量 20 MB。触限报错各不相同,统一对策是发送前归一化,顺带解决 EXIF 问题:

import io
from PIL import Image, ImageOps

def normalize(path: str, max_edge: int = 1568) -> bytes:
    img = Image.open(path)
    img = ImageOps.exif_transpose(img)      # 按 EXIF 方向摆正
    img.thumbnail((max_edge, max_edge))     # 等比缩到长边以内
    if img.mode != "RGB":
        img = img.convert("RGB")            # 去 Alpha,兼容 JPEG
    out = io.BytesIO()
    img.save(out, format="JPEG", quality=85)
    return out.getvalue()

EXIF 旋转是它的头号目标。手机竖拍的照片像素数据是横的,靠 EXIF Orientation 标记告诉查看器转回来,而 Anthropic 文档明说模型不读图像元数据——不烘焙方向就直接发,竖图会被当横图理解,文字识别全歪。Pillow 的 exif_transpose 一步摆正。

低分辨率小字识别失败。Anthropic 官方承认小于 200px 的图、旋转和低质图会出错;更隐蔽的是降采样把小字抹掉——1568px 长边的截图缩放后,12px 的辅助文字可能糊成噪声。另一个来源是物理像素本来就小:移动端 375px 宽的截图里 10px 正文,任何档位都难认,前置一层传统超分或让上游重截局部。对策不是发原图(token 翻倍),而是裁剪放大:先低档全图定位目标区域,再高档只发那个裁剪块。

多图顺序影响答案已在第 5 节展开,这里补一句排查思路:同一组图换个顺序答案漂移超过一成,就别指望调 prompt 修,直接上编号标签加显式引用,把位置依赖改成符号依赖。

8、总结

图像输入的工程版比 demo 版多三层账:格式层,URL 与 base64 的选择跟隐私和来源走,多轮场景上 Files API;成本层,OpenAI 按片、Anthropic 按 28×28 块、Gemini 按 768px 片,同一张图价差能到三倍,先算单图 token 再乘量级;质量层,细节度该低则低,小字任务裁剪放大代替发原图。把这三层想清楚,图像管线就不会在生产环境里惊叫。对于刚开始落地多模态的团队,建议先把本文第 6 节的提取流水线跑通,再按第 3 节的口径核一次账单。

留一个练习:找一张自家产品的 UI 截图,把 schema 改成可访问性问题清单——字段含问题类型(对比度、可点击区域、文字大小、无障碍标签)、位置描述、修复建议;分别用 detail: "low" 和 "high" 各跑 20 张,对比两个维度:问题检出的召回差异、token 消耗倍数。跑完你对"什么时候低档够用"会有属于自己的数字,下一篇我们继续视觉下半场:坐标系输出与 UI 自动化操作。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

598
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

370
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

338