穿越弱光与遮挡读取信息
这张图是一份在昏暗环境下拍摄的巴西酒吧菜单。三块玻璃面板上用金色文字列出了酒款,但吊灯挡住了左侧面板的部分区域,右侧面板几乎在画面边缘才隐约可见,玻璃反光在表面四处散开。

提示词没有提到亮度、裁剪,或任何具体技巧。它只要求获取内容:
image_path = Path("images/bar_menu.jpg")
image_part = types.Part.from_bytes(
data=image_path.read_bytes(), mime_type="image/jpeg"
)
prompt = (
"Read every drink listed on this menu board, including the ones "
"partially hidden behind the lamps and on the side panels. For "
"each drink, give me the name and beer style."
)
标准视觉能顺利读出光线较好的中间面板,从“FRUTADAS”部分提取了 8 款啤酒,从“CHOPP”提取了 4 款。但左侧面板返回如下:
[ANSWER]
### Left Panel (Partially visible)
* **[Unreadable] LAGER** | [Style unreadable]
* **RED LAGER** | [Style unreadable]
* **[Unreadable] LAGER** | [Style unreadable]
* **[Unreadable]GER** | [Style unreadable]
* **HONKERS ALE** | [Style unreadable]
* **[Unreadable]SELLE** | [Style unreadable]
六条目,大多名称不可读,全部风格缺失。模型知道那里有文字,但看不清。
Agentic 调用运行了两轮代码。第一轮,裁剪出三块面板:
[CODE]
middle_panel = [200, 320, 950, 780]
left_panel = [200, 0, 950, 260]
right_panel = [200, 840, 700, 1000]
# ... crops and saves each region

观察这些裁剪图后,模型判断左右面板需要更大放大倍率。于是进行第二轮:
[CODE]
left_panel_top = [100, 0, 500, 250]
left_panel_bottom = [500, 0, 980, 250]
right_panel_top = [100, 800, 600, 1000]

通过额外放大,模型读回了标准视觉标注为不可读的名称:左侧面板的 Amber Lager、Honkers Ale(Goose Island)、Demoiselle(Colorado),以及右侧面板的 Wals Dubbel、Tripel、Quadrupel、Petroleum。它还识别出左侧面板标题为“MISTURAS”,右侧面板标题为“ALCOÓLICAS”。
提示词从未说“裁剪面板”或“放大暗部区域”。它只要求获取酒款及其风格,模型从目标倒推方法。
与货架示例不同的是第二次处理。在第一轮裁剪之后,模型检查已有结果,判断左右面板仍难以读取,于是进一步对问题区域进行更紧的放大,而无需额外指示。
循环实现了自我纠正。
这正是留白方法论的价值:模型不仅做一次预处理,而是迭代,直到结果达到提示词设定的标准。
用 Gemini 3 的 Agentic Vision 进行计数与标注
酒吧菜单之所以奏效,是因为提示词留出了方法空间,模型自己找到了路径。但这并不总成立。有时模型需要明确的工具指示,下面这个示例展示了边界所在。
下图中,硬币散落在桌面和一本黑色文件夹上。有的重叠,有的部分被遮挡,铜色和银色混在一起,很容易数错。

一个合理的起点是请模型对每枚硬币进行标注,这样它可以据此计数。Google 文档确认,agentic vision 能在沙盒中用 PIL 和 OpenCV 直接在图像上绘制边界框和标签。因此:
image_path = Path("images/coins.jpg")
image_part = types.Part.from_bytes(
data=image_path.read_bytes(), mime_type="image/jpeg"
)
prompt_v1 = (
"Count every coin in this image. Draw a bounding box or circle "
"around each one and number them. Check for overlapping or "
"partially hidden coins. Give me the final count."
)
response_v1 = client.models.generate_content(
model=MODEL,
contents=[image_part, prompt_v1],
config=agentic_config,
)
print_response(response_v1, show_thoughts=True)
模型裁剪了五个区域,分别分析,并返回了 34 的计数。但请注意答案格式:
[ANSWER] Final Count: 34 coins in total.
[
{"box_2d": [387, 175, 451, 236], "label": "1"},
{"box_2d": [356, 196, 407, 248], "label": "2"},
{"box_2d": [319, 231, 375, 274], "label": "3"},
...
]
它以 JSON 返回了边界框坐标,而不是在图像上画出任何东西。提示词说了“draw(绘制)”,但模型把它理解为描述性任务,给出结构化文本输出。
在 代码执行文档中可以看出端倪:“虽然模型会自动对小细节进行放大,但对于其他任务,您应在提示中明确要求使用代码。” “draw” 这个词足够含糊,以至于模型选择了纯文本路径。
用您希望它运行的代码方式来描述任务,可以消除这种歧义:
prompt_v2 = (
"Count every coin in this image. Use Python to draw a numbered "
"bounding box on the image around each coin you find. Check for "
"overlapping or partially hidden coins. After annotating, give "
"me the final count."
)
response_v2 = client.models.generate_content(
model=MODEL,
contents=[image_part, prompt_v2],
config=agentic_config,
)
print_response(response_v2, show_thoughts=True)
这一次,模型写出了真正的绘图代码:
[CODE]
import PIL.Image
import PIL.ImageDraw
img = PIL.Image.open('input_file_0.jpeg')
width, height = img.size
coins = [
[383, 175, 452, 227], [352, 196, 411, 255], ... # 34 total
]
draw = PIL.ImageDraw.Draw(img)
for i, coin in enumerate(coins):
ymin, xmin, ymax, xmax = coin
left, top, right, bottom = (xmin*width/1000, ymin*height/1000,
xmax*width/1000, ymax*height/1000)
draw.rectangle([left, top, right, bottom], outline='red', width=3)
draw.text((left, top), str(i + 1), fill='red')
[RESULT] (Outcome.OUTCOME_OK)
Total coins detected: 34
[IMAGE] image/png, 3,685,043 bytes
[ANSWER] I have identified and counted a total of 34 coins.

每枚硬币都有红色方框与编号。真实数量是 35 或 36,取决于您如何判断画面底部相互重叠的两枚硬币。
模型给出了 34,漏掉了簇状区域中的一两枚。但标注起到了可视化草稿本的作用:相比一个需要您“相信”的数字,您得到了一个可核查的产物,可以逐框核对。
两次调用的完整未截断输出见 此 gist。
多步提取与绘图
前面的示例每次都聚焦一个主要任务(读文本、数物体)。这一次把多个步骤串在一起:从照片中裁剪表格、将数字提取为 DataFrame,并生成图表。图片是一张在桌面上拍摄的类似美国国税局(IRS)风格的税表,旁边有计算器。

提示词一次性给出了完整流水线:
image_path = Path("images/data_table.jpg")
image_part = types.Part.from_bytes(
data=image_path.read_bytes(), mime_type="image/jpeg"
)
prompt = (
"Crop and zoom into the table in this image. Extract the first "
"10 rows of data into a structured format. Identify the column "
"headers. Then create a bar chart comparing the values in the "
"numeric columns for those 10 rows. Summarize which row has the "
"highest and lowest values."
)
response = client.models.generate_content(
model=MODEL,
contents=[image_part, prompt],
config=agentic_config,
)
print_response(response, show_thoughts=True)
模型用了四轮代码,从整张照片走到最终图表。前三轮是逐步放大:先裁出“Single or Married Filing Separately”表格,再到列标题,然后到前 10 行数据,期间生成了五张中间图。下图是首次裁剪、将表格区隔离出来:

到了第四轮,读出的细节已足以提取数字。它在一个代码块中构建了 pandas DataFrame、打印表格,并生成分组柱状图:
[CODE]
import matplotlib.pyplot as plt
import pandas as pd
data = [
{"Higher Paying Job Wage": "$0 - 9,999",
"$0 - 9,999": 0, "$10,000 - 19,999": 940,
"$20,000 - 29,999": 1020, "$30,000 - 39,999": 1020},
... # 10 rows total
]
df = pd.DataFrame(data)
df.set_index("Higher Paying Job Wage").plot(kind='bar', figsize=(12, 6))
plt.title('Tax Table: Single or Married Filing Separately')
plt.ylabel('Tax Amount')
plt.savefig('tax_bar_chart.png')
提取出的表格:
|
Higher Paying Job Wage |
$0 - 9,999 |
$10,000 - 19,999 |
$20,000 - 29,999 |
$30,000 - 39,999 |
|
$0 - 9,999 |
$0 |
$940 |
$1,020 |
$1,020 |
|
$10,000 - 19,999 |
$940 |
$1,540 |
$1,620 |
$2,020 |
|
$20,000 - 29,999 |
$1,020 |
$1,620 |
$2,100 |
$3,100 |
|
$30,000 - 39,999 |
$1,020 |
$2,020 |
$3,100 |
$4,100 |
|
$40,000 - 59,999 |
$1,870 |
$3,470 |
$4,550 |
$5,550 |
|
$60,000 - 79,999 |
$1,870 |
$3,470 |
$4,690 |
$5,890 |
|
$80,000 - 99,999 |
$2,000 |
$3,810 |
$5,090 |
$6,290 |
|
$100,000 - 124,999 |
$2,040 |
$3,840 |
$5,120 |
$6,320 |
|
$125,000 - 149,999 |
$2,040 |
$3,840 |
$5,120 |
$6,910 |
|
$150,000 - 174,999 |
$2,220 |
$4,830 |
$6,910 |
$8,910 |
以及生成的柱状图:

完整未截断输出见 此 gist。
一次 API 调用就产出了结构化的 DataFrame、标注出最大/最小行的总结、以及可直接用于发布的柱状图。提示词列出了四步(裁剪、提取、制图、总结),模型将其视作流水线的各阶段,前一步的输出喂给下一步。
前面的示例用循环来细化单一操作(更好的裁剪、更好的标注)。这里展示了循环把不同操作串联起来,每一步都建立在上一轮的产出之上。
结语
Agentic vision 将一次 API 调用变成一个反馈循环,模型编写代码、查看产出、决定下一步。
货架示例展示了基本范式:裁剪过小难读的区域,再基于放大版本回答。酒吧菜单加入了第二重维度:模型先裁一轮,发现还不够,然后自行把问题区域放得更近。
对硬币而言,标注把一个“猜测”变成了可核查的产物。税表则在一次请求内串起四个不同操作(裁剪、提取、计算、绘图)。
每个示例也揭示了提示词的门道。设定完整性门槛(“列出所有商品”)会推动模型更全面。留白方法让它自主选择预处理路径。
但像“draw”这类含糊动词,有时会把它带偏;改成“Use Python to draw”即可纠正。
套路是:描述您要的产出;当任务存在歧义时,明确工具;其余交给循环处理。
本教程所有代码可在 gemini-3-flash-preview 和 google-genai SDK 上运行。若想更深入,DataCamp 的 Introduction to AI Agents 课程详细讲解了这一循环模式。
Gemini 3 中的 Agentic Vision 常见问答
Gemini 3 Flash 中的 agentic vision 是什么?
Agentic vision 是一项功能,Gemini 3 Flash 能在回复过程中编写并运行 Python 代码,在回答前对图像进行处理。它在沙盒环境中使用 Think、Act、Observe 循环,对图像进行裁剪、放大、标注和增强,且在一次 API 调用内完成。
如何在 Gemini API 中启用 agentic vision?
在 GenerateContentConfig 中添加代码执行工具:tools=[types.Tool(code_execution=types.ToolCodeExecution())]。这会为模型提供一个带 43 个预装库(包括 PIL、OpenCV 和 matplotlib)的 Python 沙盒环境。
为什么开启后模型有时仍会跳过代码执行?
模型只有在判断一次性查看图像不够时才会写代码。如果它能从原始分辨率自信作答,就会跳过代码执行路径。这是设计使然,Google 文档也有说明。
提示词中“draw”和“Use Python to draw”有何区别?
说“draw(绘制)”含糊不清,模型可能返回文本描述或 JSON 坐标,而不是实际图像。加上“Use Python to draw(使用 Python 绘制)”就能让模型编写可执行代码,产出可视化结果,更可靠地触发代码执行路径。
我可以将 agentic vision 与自定义函数调用结合使用吗?
不能。代码执行与自定义函数调用不能在同一个 API 请求中同时使用。如需两者,必须分别调用。

我是一名数据科学内容创作者,拥有超过 2 年的经验,并在 Medium 上拥有较大的读者群。我喜欢用略带讽刺的笔调撰写有关 AI 和机器学习的深度文章——毕竟总得想点办法让这些内容不那么枯燥。我已发表 130 多篇文章,并制作了一门 DataCamp 课程,另有一门正在筹备中。我的内容已被超过 500 万人次阅读,其中有 2 万人在 Medium 和 LinkedIn 上成为关注者。
您把一张图片发给视觉 API,请它读取一个很小的价格标签。返回的结果信心十足,却错得离谱。杂乱照片里数物体同样如此:模型只扫了一眼整张图片,就随口猜了个相差甚远的数字。
Gemini 3 Flash 的处理方式不同。开启代码执行后,它可以在生成回复的过程中编写并运行 Python,先对图像进行裁剪、放大和标注,再给出答案。Google 将此称为 agentic vision,并报告在视觉基准上带来 5-10% 的质量提升。
本教程通过四个难度逐步提升的示例展开:从货架读取小字、在昏暗的酒吧菜单上提取酒名、统计散落的硬币、以及将表格数据提取为图表。
什么是 Agentic Vision?“Think、Act、Observe” 循环
思路很直接。开启代码执行后, Gemini 3 可以在回复中途暂停,在沙盒里编写并运行 Python,查看代码产出,再决定下一步要做什么。
对视觉任务而言,这通常意味着模型会裁切图像区域、放大、绘制边界框,或调整对比度,从而提取原始分辨率下不可见的细节。
这会以一个三阶段循环展开。
在 Think 阶段,模型会查看已有内容(原图,或已裁剪/标注的版本),并规划下一步。它可能判断某个价格标签太小,需要裁剪并放大后才能读。
Act 会把计划变成 Python。沙盒预装了 43 个库(PIL、OpenCV、matplotlib 等),模型可以按需编写任意图像处理代码。
代码运行后,Observe 阶段闭环。输出(新图像、打印文本、计算值)会回灌到上下文。接着,模型要么直接回答,要么再循环一轮。
单次 API 调用可以多次循环。在下面的示例中,您会看到它在一次请求中裁剪了货架的七个区域。另一个示例中,它将表格数据提取到 pandas DataFrame 并生成柱状图,同样在一次调用内完成。
如果您对 agentic 循环的概念还不熟悉,DataCamp 的 AI Agent Fundamentals 路线有深入讲解。
限制与边界
为 Agentic Vision 配置您的环境
在 ai.google.dev 获取 API 密钥,并将其存为环境变量
GOOGLE_API_KEY,或写入.env文件。然后安装 SDK 和图像库:先创建客户端:
这个
timeout=600_000很重要。Agentic vision 调用会在沙盒内多轮生成与执行代码,单次请求可能需要约四分钟。httpx 的默认超时会在此之前就断开连接,留给您一个静默的
ReadTimeout,完全不知发生了什么。接下来,设置 agentic 配置:
tools是开启代码执行的开关。它为模型提供了一个带 43 个库(numpy、pandas、PIL、matplotlib、OpenCV 等)的 Python 沙盒。每个代码块有 30 秒运行时间,沙盒在出错时最多重试 5 次。thinking_level="HIGH"让模型在写代码前有更充足的规划空间。对多步骤视觉任务而言,这个差异很明显。设置include_thoughts=True会在响应部分暴露推理过程,便于调试。media_resolution用于设置循环开始前输入图像占用的 token 数。MEDIA_RESOLUTION_HIGH是不错的默认值。超高密度图像可提升至MEDIA_RESOLUTION_ULTRA_HIGH,若更重视速度可降到MEDIA_RESOLUTION_LOW。相同配置也适用于 Vertex AI,仅需少量调整。关于 Gemini 3 API 的更全面讲解(超出视觉范畴),请参阅配套教程。
Gemini 3 中的 Agentic Vision:用裁剪与放大读取细节
下图是德国一家杂货店的货架,摆着三排商品。每层货架边缘都有价格标签,底部附近有一张“Liebe Kunden”(亲爱的顾客)告示。若您自己放大,标签是可读的;但对于一次性查看整张图的模型来说,它必须决定哪些看得清、哪些看不清。
解析多部分响应
Agentic 的响应不是单一文本块。它会以部件列表的形式到达:思考、模型写的代码、执行结果、生成的图像,以及最终答案。下面这个辅助函数可逐一查看:
本教程的每个示例都使用相同的函数,因此定义一次反复复用即可。
现在载入货架图片,并写一个提示词,请模型读取告示并列出每件商品与其价格。
先在未开启代码执行的情况下运行:
以下是精简后的输出:
告示的翻译是正确的,模型也读出了能识别的 Uncle Ben's 标签。但约有三成条目是“(标签被遮挡)”,Oryza、Reis-fit 和 Reiskugeln 等品牌完全没有出现。
现在用相同提示词,切换到 agentic 配置:
输出结构不同了。不再是单个
[ANSWER],而是完整走了一遍 Think/Act/Observe 循环:模型生成的七个裁剪区域:
观察这些裁剪图后,模型给出了完整答案:
两次调用的完整未截断输出见 此 gist。
Agentic 的响应读出了三层货架上 30 多款商品。标准视觉标为“(遮挡)”的 Oryza、Reis-fit、Madras Curry、Reiskugeln 和 Steam Basmati 等项目,都以名称和价格出现。
差异并非“幻觉 vs. 准确性”;标准视觉如实承认看不清。Agentic vision 则是不接受这个限制,直到放大到能看清为止。
提示词也有影响。要求模型“列出所有可见商品名及价格”为完整性设定了门槛,推动它走向裁剪放大。
如果提示词更含糊,比如“描述这张图片”,即便开启代码执行,也很可能只给出一次性答案,因为模型看不到进一步处理的必要。