多模态结构化输出
模型可能用错误的理由答对视觉问题,例如利用文本线索,而不是读取图像。本教程沿着 examples/multimodal_structured_outputs.py,比较视觉语言回答与纯文本回答,并判断图像是否帮助或干扰了回答。
评估流程会:
- 加载合成颜色问题,或从 The Cauldron 读取少量 AI2D 样例。
- 带图像向模型询问每个选择题。
- 不带图像再次询问同一问题。
- 解析两个答案、计算准确率并分配诊断象限。
- 可选地让视觉语言模型诊断失败数据行。
默认数据集是合成的,但推理仍然使用 OpenAI 兼容的模型端点。默认端点是 Hugging Face 路由服务,API key 从 worker 环境中的 OPENAI_API_KEY 读取。
1. 定义结构化结果契约
选择题响应包含一个答案字母。诊断响应把详细推理、简短假设和粗粒度归因分开。所选端点支持 SDK 解析时,Pydantic 模型会提供结构化输出契约。请从 pydantic 导入 BaseModel、ConfigDict 和 Field;禁止额外字段会为 OpenAI 结构化输出关闭每个 object。
from pydantic import BaseModel, ConfigDict, Field class ChoiceResponse(BaseModel): """Structured answer for a multiple-choice question.""" model_config = ConfigDict(extra="forbid") choice: str = Field( ..., description="The selected answer letter, such as A, B, C, or D.", ) class JudgeResponse(BaseModel): """Structured diagnostic feedback for a failed example.""" model_config = ConfigDict(extra="forbid") reasoning: str = Field(..., description="Why the model likely answered that way.") hypothesis: str = Field(..., description="A concise cause of the error.") attribution: str = Field( ..., description="One of: question, image, model, or other.", )
仅提示词模式会省略这些 Python 返回类型,只依靠相同的系统消息直接请求 JSON。这条路径可以支持没有实现结构化解析的 OpenAI 兼容服务器。
2. 从可以审计的视觉数据集开始
合成数据源会创建纯色 PNG 和一道四选一问题。由于每个答案都确实依赖图像,它很适合快速验证消融逻辑。
def synthetic_rows(limit: int) -> list[dict[str, Any]]: """Return a tiny local vision QA set that needs the image to answer.""" color_rows = [ ("red", "A", (220, 32, 32)), ("blue", "B", (32, 96, 220)), ("green", "C", (32, 160, 80)), ("yellow", "D", (230, 200, 32)), ] rows: list[dict[str, Any]] = [] choices = "A. red\nB. blue\nC. green\nD. yellow" for color, answer, rgb in color_rows: rows.append( { "id": f"synthetic-{color}", "source": "synthetic", "question": (f"Which color fills the attached square?\n{choices}\nReturn the answer letter."), "answer": answer, "image": solid_png(96, 96, rgb), } ) if len(rows) >= limit: break return rows
AI2D 输入从 Hugging Face 流式读取数据行,取得第一轮用户和助手消息,把助手答案规范化为 A–D,将第一张图转换为字节,并且只保留完整样例。两个数据源都会变成包含 id、source、question、answer 和二进制 image 列的 Vane Relation。
3. 带图像运行推理
通用 provider 选项只组装一次,使多模态、纯文本和诊断调用共享模型、端点与采样设置。
common_prompt_options = { "provider": "openai", "model": args.model, "use_chat_completions": True, "temperature": args.temperature, "max_output_tokens": args.max_tokens, } if base_url: common_prompt_options["base_url"] = base_url choice_return_format = ChoiceResponse if args.structured_output_mode == "parse" else None judge_return_format = JudgeResponse if args.structured_output_mode == "parse" else None with_image = prompt( rel, [vane.col("question"), vane.col("image")], system_message=VISION_SYSTEM_PROMPT, return_format=choice_return_format, output_column="response_with_image", **common_prompt_options, ) eval_with_image = add_choice_eval( with_image, "response_with_image", "predicted_with_image", "is_correct_with_image", )
Relation 形式的 prompt 会保留所有源列并新增 response_with_image,因此评估阶段可以直接使用结构化响应,不需要再按位置接回数据行。
4. 不带图像重复提问
消融阶段使用已经扩充的 Relation,但只传入问题 Expression。它写入单独的响应列,并应用相同的答案规范化逻辑。
without_image = prompt( eval_with_image, vane.col("question"), system_message=TEXT_ONLY_SYSTEM_PROMPT, return_format=choice_return_format, output_column="response_without_image", **common_prompt_options, ) evaluated = add_choice_eval( without_image, "response_without_image", "predicted_without_image", "is_correct_without_image", ) classified = classify_quadrants(evaluated)
答案规范化会先尝试结构化的 choice 字段,再尝试类似 JSON 的模式,最后寻找独立的 A–D 字符。它同时记录规范化选项和布尔型正确性列。
5. 划分诊断象限
两个正确性值产生四个互斥结果:
- Both Correct:有无视觉证据都能回答正确。
- Image Helped:只有多模态推理回答正确。
- Image Hurt:只有纯文本运行回答正确。
- Both Incorrect:两次都回答错误。
分类由 Relation 上的 SQL 完成。
def classify_quadrants(rel: Any) -> Any: alias = relation_alias("quadrant", rel) return rel.query( alias, f""" select {alias}.*, case when is_correct_with_image and is_correct_without_image then 'Both Correct' when is_correct_with_image and not is_correct_without_image then 'Image Helped' when not is_correct_with_image and is_correct_without_image then 'Image Hurt' else 'Both Incorrect' end as quadrant from {alias} """, )
脚本会打印带图准确率、不带图准确率以及差值,再展示逐行预测和聚合象限计数。对于合成数据集,图像感知正常的模型应得到 Image Helped,因为问题文本里没有关于生成色块的线索。
6. 让 VLM 诊断失败
除非跳过 judge 诊断阶段,否则只有 Image Hurt 和 Both Incorrect 行会继续。脚本构造一个包含问题、正确答案以及两次模型选项的诊断提示,再次把图像提供给诊断模型。
judged = prompt( judge_input, [vane.col("judge_prompt"), vane.col("image")], system_message=JUDGE_SYSTEM_PROMPT, return_format=judge_return_format, output_column="judge_response", max_output_tokens=args.judge_max_tokens, **{key: value for key, value in common_prompt_options.items() if key != "max_output_tokens"}, )
最终预览会从诊断 JSON 中提取 attribution、hypothesis 和 reasoning。这个阶段产生的是诊断模型输出,而不是真值;应当用它组织评审,而不是取代人工检查。
模型服务与执行方式
默认通过 Hugging Face 的 OpenAI 兼容路由服务访问 Qwen/Qwen3-VL-8B-Instruct。基础 URL、模型、temperature、token 上限和结构化输出模式都可配置;凭据保存在 OPENAI_API_KEY 中,不会被序列化进 plan。Parse 模式要求视觉模型及其 endpoint 支持 OpenAI 风格的 JSON Schema 结构化输出。执行方式由 Vane 配置的 runner 决定;使用 Ray 时,执行节点需要能访问模型端点,并安装相同的 Vane AI 依赖。
图像规范化、AI2D 加载、答案解析、指标查询以及全部参数请查看完整源码。