你将做出什么
把一张图表截图交给多模态模型,它会很流畅地告诉你“4 月练习时长约 90 小时,6 月是 3 月的近两倍”。流畅不等于读对了:它可能漏掉一个月、把右轴的数字当成左轴、没看到坐标轴从 50 开始,或者把自己估出来的数说成图上写的。
本课先用 Python 生成一张你知道全部答案的图表,再用两段完整提示词让模型先描述结构、再抽取数字,最后用核对脚本逐项比较模型结果与标准答案,并针对每类错误给出修订请求。交付物是:图表与答案文件、模型的原始回答、核对报告和修订记录。
本课的图表数据是原创虚构的。生成图表和核对脚本已在本站用 Python 3.12 实际运行;“把截图交给模型”的步骤是练习方案(本站未实跑),本站没有调用任何模型,文中出现的模型输出都是为演示核对方法而编写的虚构示例。
准备什么
- Python 3.12 和一个空文件夹。两段代码只用标准库。
- 一个浏览器,用来打开 SVG 图片并截图。
- 一个你已经在用、支持上传图片的助手。如果你的工具只能输入文字,这一课无法完成:把文件名或图片描述贴给它,不等于让它看到了图。
- 建议先读 OCR 实操。OCR 关注“字认对没有”,本课关注“数字和图形之间的关系读对没有”。
第一步:生成一张答案已知的图表
用真实工作中的图表练习,你很难判断模型哪里错了,因为你自己也不知道准确数字。所以先自己做一张。下面的脚本画出一张双轴图:柱子是每月练习时长(左轴,单位小时),折线是每月完成练习数(右轴,单位个)。图里有意设置了三个常见陷阱:左轴从 50 而不是 0 开始;只有部分柱子和点印了数字;两条坐标轴单位不同。
保存为 make_chart.py,在空文件夹中运行 python make_chart.py:
import json
from pathlib import Path
MONTHS = ["1月", "2月", "3月", "4月", "5月", "6月"]
HOURS = [62, 75, 58, 91, 87, 104] # 柱:练习时长(小时,左轴 50~110)
EXERCISES = [12, 15, 11, 20, 18, 24] # 折线:完成练习数(个,右轴 0~30)
LABELED = {"hours": ["2月", "6月"], "exercises": ["6月"]}
LEFT, RIGHT = (50, 110), (0, 30)
X0, X1, Y0, Y1 = 70, 570, 330, 70 # 绘图区:左、右、底、顶
def y_left(v):
return Y0 - (v - LEFT[0]) / (LEFT[1] - LEFT[0]) * (Y0 - Y1)
def y_right(v):
return Y0 - (v - RIGHT[0]) / (RIGHT[1] - RIGHT[0]) * (Y0 - Y1)
font = 'font-family="Microsoft YaHei, PingFang SC, Noto Sans CJK SC, sans-serif"'
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400" {font} font-size="13">',
'<rect width="640" height="400" fill="#ffffff"/>',
'<text x="320" y="28" text-anchor="middle" font-size="17">学习小组上半年练习情况(教学虚构)</text>',
'<text x="320" y="50" text-anchor="middle" fill="#555">柱:练习时长(小时,左轴)'
' 折线:完成练习数(个,右轴)</text>']
for i in range(7):
y = Y0 - i * (Y0 - Y1) / 6
parts.append(f'<line x1="{X0}" y1="{y:.1f}" x2="{X1}" y2="{y:.1f}" stroke="#ddd"/>')
parts.append(f'<text x="{X0 - 8}" y="{y + 4:.1f}" text-anchor="end">{LEFT[0] + i * 10}</text>')
parts.append(f'<text x="{X1 + 8}" y="{y + 4:.1f}">{RIGHT[0] + i * 5}</text>')
slot = (X1 - X0) / len(MONTHS)
points = []
for i, (month, h, e) in enumerate(zip(MONTHS, HOURS, EXERCISES, strict=True)):
cx = X0 + slot * (i + 0.5)
parts.append(f'<rect x="{cx - 20:.1f}" y="{y_left(h):.1f}" width="40" '
f'height="{Y0 - y_left(h):.1f}" fill="#5b8def"/>')
parts.append(f'<text x="{cx:.1f}" y="{Y0 + 20}" text-anchor="middle">{month}</text>')
if month in LABELED["hours"]:
parts.append(f'<text x="{cx:.1f}" y="{y_left(h) - 6:.1f}" text-anchor="middle">{h}</text>')
points.append((cx, y_right(e)))
if month in LABELED["exercises"]:
parts.append(f'<text x="{cx + 10:.1f}" y="{y_right(e) - 8:.1f}" fill="#d9480f">{e}</text>')
parts.append('<polyline fill="none" stroke="#d9480f" stroke-width="2.5" points="'
+ " ".join(f"{x:.1f},{y:.1f}" for x, y in points) + '"/>')
parts += [f'<circle cx="{x:.1f}" cy="{y:.1f}" r="4" fill="#d9480f"/>' for x, y in points]
parts.append("</svg>")
Path("chart.svg").write_text("\n".join(parts), encoding="utf-8")
answer = {"units": {"left": "小时", "right": "个"}, "left_axis": list(LEFT), "right_axis": list(RIGHT),
"months": MONTHS, "hours": HOURS, "exercises": EXERCISES, "labeled": LABELED}
Path("answer.json").write_text(json.dumps(answer, ensure_ascii=False, indent=2), encoding="utf-8")
print("已写出 chart.svg 和 answer.json")
print("图上印有数字的位置:", LABELED)
h3, h6 = HOURS[2], HOURS[5]
print(f"6月与3月练习时长:数值之比 {h6 / h3:.2f},柱子高度之比 {(h6 - LEFT[0]) / (h3 - LEFT[0]):.2f}")