品管員(qa-kit,Day 15)靠規則量數字。有些毛病數字看不出來,要用眼睛看:圓柱被切成多邊形、小零件被瘦身瘦到消失、該亮的地方變黑。所以接力隊(expo_director)的第三棒 vision_qa_loop 裡,有兩位成員專門處理「看圖」:先拍照的 view_renderer,再看照的 vision_judge。
| 比喻 | 術語 | 白話 |
|---|---|---|
| 拍照的人 | view_renderer(LlmAgent,只掛一支工具) |
拿成品拍 6 張照片 |
| 看照片的人 | vision_judge(LlmAgent,掛兩支工具) |
看照片、判斷有沒有缺陷、寫一筆紀錄 |
| 相機 | render_views(工具)→ qa_kit.render_views → Blender Workbench(headless) |
後台 3D 軟體,一次啟動、換 6 個鏡頭 |
| 照片放進共用相簿 | artifact(tool_context.save_artifact) |
ADK 的檔案暫存空間,工具存、別的工具取 |
| 看照片的人把照片「調出來看」 | load_artifacts(ADK 內建工具) |
把相簿裡的圖加進這次要問模型的請求裡 |
| 判讀紀錄單 | record_vision_verdict(工具)→ asset.qa.vision_qa |
寫「看了沒、合不合格、一句話」 |
| 一圈:拍照 → 看照片 → 自癒 | vision_qa_loop(LoopAgent,max_iterations=3) |
今天講前兩棒,自癒下一篇 |
vision_qa_loop = LoopAgent(
name="vision_qa_loop",
description="算圖 → 視覺判讀 → 自癒,最多三圈",
max_iterations=3,
sub_agents=[view_renderer, vision_judge, healer],
)
render_views 原本在規劃裡是「迴圈裡的一個工具」,但 LoopAgent.sub_agents 只能放 agent,所以包成 view_renderer 這個只拿一支工具的 LlmAgent(agents/expo_director/CONTRACT.md 記錄的三點差異之一)。
render_viewsasync def render_views(asset_id: str, profile: str, size_px: int = 512, tool_context=None):
"""Blender Workbench headless 出 6 個視角的 PNG 到 reports/views/,並存成 artifacts 供視覺判讀。"""
它是 17 支工具裡唯一的 async 工具(要 await tool_context.save_artifact(...))。流程:
geometry/exhibit_kiosk_qa.glb(_qa_source_uri:Y-up、公尺、未經 Draco 壓縮,因為 trimesh 讀不了 Draco 幾何,見 Day 15)。qa_kit.render_views(...),產出 6 張 PNG 到 reports/views/。mime_type="image/png")。{attempt, views: [...], size_px} 寫進 state["views"]。失敗(Blender 不在、渲染出錯)一律回 envelope.fail("qa.render_views", "render_failed", ...),不炸管線——程式註解寫得很直白:「Blender 不在、渲染失敗都只是降級」。
qa_kit.render_views 本身(packages/qa-kit/qa_kit/render.py + _blender_render.py):
contracts_py.kernels.require("blender") 取得 Blender 路徑,subprocess 以 --background --factory-startup 執行腳本。front/back/left/right/top/iso,正交相機,Workbench 引擎,STUDIO 打光,材質顏色,背景透明(RGBA)。實測(ast_gearbox_demo 的 QA 檔):render-views 約 2.6 秒(含 uv run 啟動)。6 張 PNG 各約 105–124 KB(合計約 650 KB),尺寸 512×512,RGBA。
vision_judgevision_judge = LlmAgent(
name="vision_judge",
model=MODEL,
description="看多視角圖判讀破洞、法線反轉、過度減面",
instruction=_STATE_BRIEF + """
先用 `load_artifacts` 取出剛才算的 PNG,再判讀:有沒有破洞、法線反轉(該亮的地方變黑)、
過度減面(圓柱變多邊形、小零件消失)。然後呼叫一次 `record_vision_verdict`:
- ran=true;passed=true 表示「看起來沒問題」,passed=false 表示「看得出缺陷」。
- issue_description 用一句中文描述你看到的問題,看不出問題就寫「無明顯缺陷」。
**你只能降級**:qa-kit 的規則判 fail 就是 fail,你判 pass 也不會把它改回來;
你唯一能做的是「規則沒抓到但眼睛看得出來的問題」。看不到圖就 ran=false,不要用猜的。
""",
tools=[catalog.record_vision_verdict, load_artifacts],
...
)
load_artifacts 是 ADK 內建工具,參數是 artifact_names(字串陣列);它把相簿裡的檔案加進這次送給模型的請求,模型才真的「看得到」圖。
record_vision_verdict寫進 asset.qa.vision_qa(契約的 VisionQA 物件):
vision = {"ran": bool(ran), "model": None if not ran else str(state.get("model") or "gemini"),
"pass": bool(passed) if ran else None,
"issue_description": issue_description or None,
"views": list((state.get("views") or {}).get("views") or [])}
「降級」的程式:
if ran and not passed: # 只能降級
qa["status"] = "fail" if qa.get("status") in (None, "pass", "warn") else qa["status"]
我用 ast_day19(QA 原本是 pass)直接呼叫工具,實測三種情況:
| 呼叫 | 之前的 qa.status |
之後 |
|---|---|---|
ran=True, passed=False |
pass |
fail |
ran=True, passed=True |
fail |
fail(不改回去) |
ran=False |
fail |
fail(不動),pass/model 都是 None |
這支工具不會動 score。
def plan_judge(state):
...
return [("record_vision_verdict", {"asset_id": ..., "ran": False, "passed": False,
"issue_description": "沒有模型憑證,視覺判讀未執行(qa-kit 規則結果不受影響)。"})]
離線走的是 ran=False:記下「沒看」,不降級任何東西。離線整條管線實跑,這一段事件是:
→ [view_renderer] render_views({"asset_id": "ast_day18", "profile": "exhibit.kiosk", "size_px": 512})
← [view_renderer] render_views: ok=True
[view_renderer] (離線腳本)第 0 次算圖完成,共 6 張。
→ [vision_judge] record_vision_verdict({... "ran": false, "passed": false, ...})
← [vision_judge] record_vision_verdict: ok=True
[vision_judge] (離線腳本)沒有 API key,vision_judge 未執行;視覺 QA 只能降級,不會把 qa-kit 的結果改成通過。
離線劇本沒有呼叫 load_artifacts(整條 14 個工具呼叫裡沒有它):沒有模型,就沒有人要看那些圖。
vision_qa_can_only_downgradeagents/evals/expo_director.evalset.json 的第 5 題問:「視覺判讀看起來沒問題的話,可以把 qa-kit 判定的 fail 改成 pass 嗎?」參考答案是「不行」並說明原因。軌跡比對離線能跑;中文回答比對需要 GOOGLE_API_KEY(Day 25 細講)。
GOOGLE_API_KEY,完全沒實測;load_artifacts 在真模型下是否正確把圖送進請求,也沒測。suggested_ratio_adjustment 欄位,工具沒有寫入。ran=True 時 model 欄位用 state.get("model") or "gemini";沒有 state["model"] 就寫字串 gemini,不是實際的模型型號。下一篇:healer 與自癒迴圈。