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 图片 |
|
本地/私密图片 |
Base64 编码传入(增大约 33%,最稳定) |
多图对比 |
content 列表中放多个 |
OCR / 文字提取 |
明确指令「只输出文字」,加结构要求 |
结构化提取 |
Vision + |
多轮追问 |
图片只传一次(首轮),后续纯文字对话 |
成本提示: 图片比文字贵,生产环境中:
优先用低分辨率模式(
detail: low)大批量时先压缩/裁剪图片再传输
缓存相同图片的分析结果
下一章 → RAG - Chunking:给模型接入外部知识库