03 · Vision(多模态)#

现代 LLM 不仅能读文字,还能「看」图片——这就是 Vision / 多模态能力。

应用场景:

  • 图片描述与分析(电商、医疗影像、卫星图)

  • 截图 OCR(从 UI/PDF 截图提取文字)

  • 文档理解(表格、图表、手写笔记)

  • 视觉 Q&A(「图中有几个人?」)

本章覆盖:URL 图片、Base64 本地图片、多图、OCR、结构化信息提取。

import base64
import json
import os
import urllib.request
import litellm
from dotenv import load_dotenv

litellm.drop_params = True
load_dotenv()
MODEL = os.getenv("LLM_MODEL")

def _c(**kw):
    m = kw.get("model", MODEL)
    if any(m.startswith(p) for p in ("openai/gpt-5", "openai/o1", "openai/o3", "openai/o4")):
        kw.pop("temperature", None)
    return litellm.completion(**kw)


def url_to_base64(url: str, timeout: int = 10) -> tuple:
    """Download an image URL and return (base64_str, mime_type)."""
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        data = resp.read()
        content_type = resp.headers.get("Content-Type", "image/jpeg").split(";")[0]
    return base64.b64encode(data).decode("utf-8"), content_type


def make_image_content(url: str) -> dict:
    """Download image and return image_url content block using base64."""
    b64, mime = url_to_base64(url)
    return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}


print(f"使用模型: {MODEL}")
print("注:图片通过 Base64 传输,避免 URL 下载权限问题")
使用模型: openai/gpt-5-mini
注:图片通过 Base64 传输,避免 URL 下载权限问题

1. 图片输入基础#

Vision 模型通过特殊的消息格式接收图片:

content = [
    {"type": "text", "text": "问题"},
    {"type": "image_url", "image_url": {"url": "..."}},
]

图片可以是公开 URL(模型直接拉取)或 Base64 编码(本地/私密图片)。

def ask_about_image(image_url: str, question: str) -> str:
    """下载图片并以 Base64 传给模型,保证兼容性。"""
    image_content = make_image_content(image_url)
    response = _c(
        model=MODEL,
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": question},
                    image_content,
                ],
            }
        ],
    )
    return response.choices[0].message.content


# 使用 Python 官方 logo(可靠的公开图片)
python_logo_url = "https://www.python.org/static/community_logos/python-logo-master-v3-TM.png"

result = ask_about_image(python_logo_url, "请描述这张图片的内容,包括颜色、文字和设计风格。")
print(result)
这是一张 Python 官方标志的图片。具体内容描述如下:

- 颜色:左侧图形由两条相互盘绕的蛇状图形组成,上半部分为蓝色(带有从深到浅的渐变),下半部分为黄色(同样有渐变)。蛇眼为白色小圆点。文本为中灰色,右上角有浅灰色的商标符号“™”。图形下方有一个很淡的灰色投影,增强立体感。
- 文字:右侧为小写英文“python”,字形简洁、圆润、无衬线风格,字母间距适中,整体偏细的笔画使得视觉轻盈。商标符号“™”位于文字右上角,字号较小。
- 设计风格:现代、简约且亲和力强。图形使用平面化设计结合柔和渐变与微弱投影,线条圆润,造型抽象但具象化为两条互相缠绕的蛇,传达动态与协作感。整体布局为图形在左、文字在右的水平排列,视觉平衡且易于识别。

2. Base64 本地图片#

当图片存储在本地时,使用 Base64 编码传入。

格式:data:image/<格式>;base64,<编码内容>

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import io

def create_sample_chart() -> str:
    """生成一张示例图表,返回 Base64 PNG 字符串。"""
    fig, ax = plt.subplots(figsize=(6, 4))
    categories = ["Python", "JavaScript", "Java", "C++", "Go"]
    popularity = [30, 25, 20, 15, 10]
    colors = ["#3776ab", "#f7df1e", "#ed8b00", "#004482", "#00add8"]
    bars = ax.bar(categories, popularity, color=colors)
    ax.set_title("编程语言流行度(示例)", fontsize=14)
    ax.set_ylabel("占比 %")
    ax.set_ylim(0, 35)
    for bar, val in zip(bars, popularity):
        ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
                f"{val}%", ha="center", va="bottom")
    buf = io.BytesIO()
    plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
    plt.close()
    buf.seek(0)
    return base64.b64encode(buf.read()).decode("utf-8")


# 生成图表并用 Base64 传给模型
chart_b64 = create_sample_chart()
print(f"图表 Base64 大小: {len(chart_b64):,} 字符")

response = _c(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "这张图表展示了什么数据?请描述主要内容和各类别的比较。"},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{chart_b64}"}},
            ],
        }
    ],
)
print(response.choices[0].message.content)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 21344 (\N{CJK UNIFIED IDEOGRAPH-5360}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 27604 (\N{CJK UNIFIED IDEOGRAPH-6BD4}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 32534 (\N{CJK UNIFIED IDEOGRAPH-7F16}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 31243 (\N{CJK UNIFIED IDEOGRAPH-7A0B}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 35821 (\N{CJK UNIFIED IDEOGRAPH-8BED}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 35328 (\N{CJK UNIFIED IDEOGRAPH-8A00}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 27969 (\N{CJK UNIFIED IDEOGRAPH-6D41}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 34892 (\N{CJK UNIFIED IDEOGRAPH-884C}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 24230 (\N{CJK UNIFIED IDEOGRAPH-5EA6}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 65288 (\N{FULLWIDTH LEFT PARENTHESIS}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 31034 (\N{CJK UNIFIED IDEOGRAPH-793A}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 20363 (\N{CJK UNIFIED IDEOGRAPH-4F8B}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
/var/folders/km/c2z87ytx2fvbh8_v94cn7l4w0000gp/T/ipykernel_80129/588769704.py:19: UserWarning: Glyph 65289 (\N{FULLWIDTH RIGHT PARENTHESIS}) missing from font(s) DejaVu Sans.
  plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
图表 Base64 大小: 14,876 字符
这张柱状图展示了几种编程语言的占比(百分比),主要内容和比较如下:

- 图中类别与数值:Python 30%、JavaScript 25%、Java 20%、C++ 15%、Go 10%。纵轴单位为“比例 %”,每根柱子上方标注了具体百分比。
- 排序与总体趋势:从左到右呈递减趋势,Python 最高,Go 最低,整体按流行/使用比例依次递减。
- 具体比较:
  - Python 比第二名 JavaScript 高 5 个百分点(30% vs 25%)。
  - Python 比第三名 Java 高 10 个百分点,比 C++ 高 15 个百分点,比 Go 高 20 个百分点。
  - JavaScript 比 Java 高 5 个百分点,比 C++ 高 10 个百分点,比 Go 高 15 个百分点。
  - Java 比 C++ 高 5 个百分点,比 Go 高 10 个百分点;C++ 比 Go 高 5 个百分点。
- 视觉要点:每种语言用不同颜色表示,数值清晰标注,便于直接比较各语言的相对占比。

(注:图表未标明来源和样本范围,具体含义应结合原始数据来源确认。)
# Base64 膨胀率说明
# 创建一个本地图片文件来演示
local_path = "/tmp/sample_chart.png"
fig, ax = plt.subplots(figsize=(4, 3))
ax.plot([1, 2, 3, 4], [10, 20, 15, 25], marker="o")
ax.set_title("Sample")
plt.savefig(local_path, dpi=72)
plt.close()

file_size = os.path.getsize(local_path)
with open(local_path, "rb") as f:
    b64_str = base64.b64encode(f.read()).decode()
b64_size = len(b64_str.encode())

print(f"原始文件:    {file_size:,} bytes")
print(f"Base64 编码: {b64_size:,} bytes")
print(f"膨胀率:      {b64_size/file_size:.2f}x(Base64 固定增加约 33%)")
原始文件:    9,366 bytes
Base64 编码: 12,488 bytes
膨胀率:      1.33x(Base64 固定增加约 33%)

3. 多图输入#

一次请求可以传入多张图片——对比分析、序列图、前后对比等场景。

def create_comparison_charts() -> tuple:
    """生成两张不同的图表用于对比演示。"""
    def to_b64(fig):
        buf = io.BytesIO()
        fig.savefig(buf, format="png", bbox_inches="tight", dpi=80)
        plt.close(fig)
        buf.seek(0)
        return base64.b64encode(buf.read()).decode()

    # 图1:折线图(月度趋势)
    fig1, ax1 = plt.subplots(figsize=(5, 3))
    months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
    ax1.plot(months, [100, 120, 115, 140, 135, 160], marker="o", color="blue", label="Revenue")
    ax1.plot(months, [80, 90, 95, 100, 110, 120], marker="s", color="red", label="Cost")
    ax1.set_title("Monthly Revenue vs Cost")
    ax1.legend()
    ax1.set_ylabel("Amount ($K)")

    # 图2:饼图(市场份额)
    fig2, ax2 = plt.subplots(figsize=(5, 3))
    labels = ["Product A", "Product B", "Product C", "Others"]
    sizes = [35, 30, 20, 15]
    ax2.pie(sizes, labels=labels, autopct="%1.0f%%", startangle=90)
    ax2.set_title("Market Share by Product")

    return to_b64(fig1), to_b64(fig2)


b64_line, b64_pie = create_comparison_charts()

response = _c(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "请对比分析这两张图表,分别说明它们展示了什么数据,以及各自的优缺点。"},
                {"type": "text", "text": "图表 1:"},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_line}"}},
                {"type": "text", "text": "图表 2:"},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_pie}"}},
            ],
        }
    ],
)
print(response.choices[0].message.content)
下面分别对两张图表的内容、可读性与优缺点做对比分析,并给出改进建议。

一、图表 1(折线图)
- 展示内容(可读数据)  
  - 标题:Monthly Revenue vs Cost(每月收入与成本)  
  - 横轴:月份(Jan–Jun)  
  - 纵轴:金额($K),有两条曲线:Revenue(收入,蓝色)和 Cost(成本,红色)。  
  - 大致数值(单位:千美元):  
    - 收入:100, 120, 115, 140, 135, 160  
    - 成本: 80,  90,  95, 100, 110, 120  
  - 从数值可得的简单结论:总体上收入呈上升趋势(3月有小降),成本也逐步上升;两者差值(利润粗估)整体增大但有波动(例如4月和6月利润较高)。

- 优点  
  - 清晰展示时间序列变化,便于看出趋势和拐点(如3月收入下降、成本平稳上升)。  
  - 两条曲线同轴比较,直观对比收入与成本的相对关系。  
  - 使用不同颜色和点标记,图例明确,容易区分两条曲线。  
  - 有网格线、轴标签和单位,阅读数值更方便。

- 缺点/可改进点  
  - 纵轴是否从 0 开始会影响视觉判断(当前范围放大可能夸大变化),建议在可接受的情况下保留 0 以避免误导或明确标注刻度范围。  
  - 未直接展示“利润/毛利率”这一重要指标,读者需手动计算。可以添加第三条“利润”曲线或在两条曲线之间用阴影表示差额。  
  - 数据点上没有数值标签,若需要精确比较建议标注或在交互版中悬浮显示。  
  - 仅 6 个月样本,若要判断季节性或年度趋势需更长时间数据。  
  - 色盲友好性可再考虑(选择对比度更高的调色板)。

- 改进建议(简洁)  
  - 添加利润曲线或在曲线间填充颜色表示差额;标注关键数据点与百分比变化;确认纵轴起点与读者期望一致;若需要更精确比较,可用分组柱状图并列显示月度收入与成本。

二、图表 2(饼图)
- 展示内容(可读数据)  
  - 标题:Market Share by Product(按产品的市场份额)  
  - 分类及占比(图中标签):Product A 35%,Product B 30%,Product C 20%,Others 15%。  
  - 目的是展示各产品在总体中的组成份额。

- 优点  
  - 一目了然地呈现构成比例,适合向非专业观众快速传达“份额构成”概念。  
  - 类别较少(4 类),饼图的可读性在类别不多时较好。

- 缺点/可改进点  
  - 饼图不便于精确比较相近份额(例如 35% vs 30%),人眼难以比较角度差异,精确度不如条形图。  
  - “Others” 类含糊,若想更深入分析应拆分或说明包含哪些项目。  
  - 如果需要展示多时期或分组比较(随时间变化的市场份额),饼图不适合。  
  - 颜色对比与标签位置要注意,防止遮挡或读取困难(当前图内置标签,但若字号小则不易读)。  
  - 对色盲用户不友好时需选取色盲友好调色板并加上数值/百分比标签。

- 改进建议(简洁)  
  - 若需精确比较,改用水平条形图并按占比排序;若保留饼图,明确标注百分比数值、用不同纹理或色彩提高可读性,或者使用 donut(环形)并在中心注明总量/主体信息。  
  - 说明 Others 的组成或在补充表格中列出明细。

三、总体比较与推荐
- 用途不同:图表1 更适合展示随时间的变化与比较(趋势分析);图表2 更适合展示总体构成(份额分布)。两者互为补充,而非替代。  
- 若你需要同时呈现“随时间的份额变化”(例如各产品随月度的市场份额),应用堆叠面积图、分组条形图或一组小倍图(small multiples),而非单一饼图。  
- 如果目的是精确比较(数字多且接近),优先选择条形图或折线图;如果目的是快速传达总体构成且类别少且差异明显,饼图可接受。

如需,我可以:
- 基于这两张图的原始数据(请提供 CSV 或数值)帮你重绘并给出几种替代图(条形图、带阴影的收入/成本图、堆叠面积图等)并展示改进后的效果。

4. OCR:从图片提取文字#

Vision 模型能识别图片中的文字,且理解上下文——比传统 OCR 工具更智能。

def create_text_image(text_lines: list) -> str:
    """创建包含文字的图片,用于 OCR 演示。"""
    fig, ax = plt.subplots(figsize=(8, 4))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, len(text_lines) + 1)
    ax.axis("off")
    for i, line in enumerate(reversed(text_lines)):
        ax.text(0.5, i + 0.5, line, fontsize=11, va="center", family="monospace")
    ax.set_facecolor("white")
    fig.patch.set_facecolor("white")
    buf = io.BytesIO()
    plt.savefig(buf, format="png", bbox_inches="tight", dpi=120, facecolor="white")
    plt.close()
    buf.seek(0)
    return base64.b64encode(buf.read()).decode()


# 创建一张包含代码/文字的图片
code_lines = [
    "def fibonacci(n):",
    "    if n <= 1:",
    "        return n",
    "    return fibonacci(n-1) + fibonacci(n-2)",
    "",
    "# Test",
    "print([fibonacci(i) for i in range(10)])",
]

code_img_b64 = create_text_image(code_lines)

response = _c(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": "请精确提取图片中的代码,保持缩进和格式,只输出代码块,不要解释。",
                },
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{code_img_b64}"}},
            ],
        }
    ],
)
print("提取的代码:")
print(response.choices[0].message.content)
提取的代码:
```python
def fibonacci(n):

    if n <= 1:

        return n

    return fibonacci(n-1) + fibonacci(n-2)


# Test

print([fibonacci(i) for i in range(10)])
```

5. 结构化信息提取#

Vision + JSON Output:从图片中提取结构化数据。

典型场景:图表数据提取、名片识别、发票解析。

def create_data_chart() -> str:
    """创建一张包含明确数据的图表。"""
    fig, ax = plt.subplots(figsize=(7, 4))
    quarters = ["Q1 2024", "Q2 2024", "Q3 2024", "Q4 2024"]
    revenue =  [1.2, 1.5, 1.8, 2.1]
    profit =   [0.3, 0.4, 0.5, 0.7]
    x = range(len(quarters))
    bars1 = ax.bar([i - 0.2 for i in x], revenue, 0.4, label="Revenue (M$)", color="steelblue")
    bars2 = ax.bar([i + 0.2 for i in x], profit,  0.4, label="Profit (M$)",  color="green")
    ax.set_xticks(list(x))
    ax.set_xticklabels(quarters)
    ax.set_title("XYZ Corp Quarterly Financials 2024")
    ax.legend()
    ax.set_ylabel("Million USD")
    for bar in bars1:
        ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
                f"${bar.get_height():.1f}M", ha="center", va="bottom", fontsize=8)
    for bar in bars2:
        ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
                f"${bar.get_height():.1f}M", ha="center", va="bottom", fontsize=8)
    buf = io.BytesIO()
    plt.savefig(buf, format="png", bbox_inches="tight", dpi=100)
    plt.close()
    buf.seek(0)
    return base64.b64encode(buf.read()).decode()


chart_b64 = create_data_chart()

response = _c(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": (
                        "从图表提取数据,输出纯 JSON(不要 markdown 代码块)格式:\n"
                        '{"company": str, "year": int, "quarters": [{"quarter": str, '
                        '"revenue_m": float, "profit_m": float}]}'
                    ),
                },
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{chart_b64}"}},
            ],
        }
    ],
    response_format={"type": "json_object"},
)

data = json.loads(response.choices[0].message.content)
print(json.dumps(data, indent=2))
{
  "company": "XYZ Corp",
  "year": 2024,
  "quarters": [
    {
      "quarter": "Q1 2024",
      "revenue_m": 1.2,
      "profit_m": 0.3
    },
    {
      "quarter": "Q2 2024",
      "revenue_m": 1.5,
      "profit_m": 0.4
    },
    {
      "quarter": "Q3 2024",
      "revenue_m": 1.8,
      "profit_m": 0.5
    },
    {
      "quarter": "Q4 2024",
      "revenue_m": 2.1,
      "profit_m": 0.7
    }
  ]
}

6. 图片 + 多轮对话#

Vision 可以和多轮对话结合:图片只传一次(首轮),后续只传文字。

class VisionChat:
    """支持图片的多轮对话。"""

    def __init__(self, image_b64: str, mime: str = "image/png", system: str = ""):
        self.messages = []
        if system:
            self.messages.append({"role": "system", "content": system})
        self._data_url = f"data:{mime};base64,{image_b64}"
        self._image_sent = False

    def chat(self, question: str) -> str:
        if not self._image_sent:
            user_content = [
                {"type": "image_url", "image_url": {"url": self._data_url}},
                {"type": "text", "text": question},
            ]
            self._image_sent = True
        else:
            user_content = question

        self.messages.append({"role": "user", "content": user_content})
        response = _c(model=MODEL, messages=self.messages)
        answer = response.choices[0].message.content
        self.messages.append({"role": "assistant", "content": answer})
        return answer


# 对图表进行多轮追问
vc = VisionChat(
    chart_b64,  # 上一节生成的财务图表
    system="你是一位数据分析师,请简洁地回答关于图表的问题。"
)

questions = [
    "这张图表展示了什么公司的数据?",
    "哪个季度的利润率最高?",
    "全年收入合计是多少?",
]

for q in questions:
    print(f"问:{q}")
    print(f"答:{vc.chat(q)}")
    print()
问:这张图表展示了什么公司的数据?
答:图表展示的是 XYZ Corp(XYZ 公司)的数据。

问:哪个季度的利润率最高?
答:Q4 2024,利润率最高,约33.3%(0.7/2.1)。

问:全年收入合计是多少?
答:全年收入合计为 6.6 百万美元($6.6M)。

7. Token 成本:图片 vs 文字#

图片比文字消耗更多 token,了解成本有助于优化开销。

# 纯文字请求(对比基准)
text_only = _c(
    model=MODEL,
    messages=[{"role": "user", "content": "描述一张包含柱状图的财务报告图表"}]
)
text_tokens = text_only.usage.prompt_tokens

# 图片请求(使用上节创建的图表)
img_resp = _c(
    model=MODEL,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "描述这张图表"},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{chart_b64}"}},
            ],
        }
    ],
)
img_tokens = img_resp.usage.prompt_tokens

print(f"纯文字 prompt tokens:  {text_tokens}")
print(f"图片请求 prompt tokens: {img_tokens}")
print(f"图片额外消耗:          ~{img_tokens - text_tokens} tokens")
print()
print("图片 token 定价规律(OpenAI GPT-4o):")
print("  低分辨率 (detail: low)  → 固定 85 tokens/张")
print("  高分辨率 (detail: high) → 按 512×512 tile 计算,每 tile ~170 tokens")
print("  2048×2048 大图         → 约 765 tokens")
纯文字 prompt tokens:  19
图片请求 prompt tokens: 300
图片额外消耗:          ~281 tokens

图片 token 定价规律(OpenAI GPT-4o):
  低分辨率 (detail: low)  → 固定 85 tokens/张
  高分辨率 (detail: high) → 按 512×512 tile 计算,每 tile ~170 tokens
  2048×2048 大图         → 约 765 tokens

小结#

场景

方案

公开 URL 图片

image_url 传 URL(但部分来源会拒绝 API 下载)

本地/私密图片

Base64 编码传入(增大约 33%,最稳定)

多图对比

content 列表中放多个 image_url

OCR / 文字提取

明确指令「只输出文字」,加结构要求

结构化提取

Vision + json_object 输出格式

多轮追问

图片只传一次(首轮),后续纯文字对话

成本提示: 图片比文字贵,生产环境中:

  • 优先用低分辨率模式(detail: low

  • 大批量时先压缩/裁剪图片再传输

  • 缓存相同图片的分析结果

下一章 → RAG - Chunking:给模型接入外部知识库