跳到主要内容

Webwright 完成门禁与自我校验

本章讲什么: 工作区模式最有辨识度的设计——「模型说完成不算数,得让另一个模型看截图签字」。 以及配置系统怎么把这一切拼起来、怎么让裁判工具复用同一个模型。这是 Webwright 在基准上稳的关键。


1. 为什么需要门禁

问题: 模型很爱「幻觉式完成」——没真正应用筛选、没截到证据,就宣称任务做完了。基准分数会因此虚高、 实际失败。

Webwright 的答案:把「完成」变成一个可核对的磁盘事实。 模型想 done=true,必须先:

  1. 把任务拆成 critical points 写进 plan.md(每条一个可独立验证的约束/筛选/动作)。
  2. final_script.py,在 final_runs/run_<id>/ 里跑一次,产出逐 critical-point 的截图 + 动作日志。
  3. self_reflection 截图裁判,写出 self_reflect_result.json,其中 predicted_label 必须是 1

只有第 3 步的文件真的存在且判 success,01 章里的 _tool_gate_error (default.py:208)才放行。这套流程和完成门禁在 base.yaml 的 system/instance 提示词里写得极细 (config/base.yaml:289 起的「Completion Gate」)。


2. self_reflection:两阶段截图裁判

文件:src/webwright/tools/self_reflection.py。它是个独立 CLI,模型在某一步用一条 shell 命令调它 (python -m webwright.tools.self_reflection --config ... --output ...)。

它要解决的小问题: 怎么客观判断「这一堆截图 + 动作日志,到底满没满足全部 critical points」。

两阶段设计:

阶段 1(并行,每张截图一次调用)
┌─ 截图1 ─▶ (image_judge system+user prompt + 图) ─▶ 模型 ─▶ 解析 Score(1-5) + Reasoning
├─ 截图2 ─▶ ... (解析失败重试≤3,仍失败记 Score:0)
└─ 截图N ─▶ ...

▼ 收集所有 Reasoning
阶段 2(一次聚合调用)
把「每图 Reasoning 编号列表」+「final_script_log.txt 动作日志」注入 final 用户提示模板
+ 附上「全部截图」 ─▶ 模型 ─▶ 回复必须以 `Status: success` 或 `Status: failure` 结尾


解析末尾 Status 行 ─▶ predicted_label = 1(success) / 0(failure) / null(没解析出=当失败)

关键实现点:

  • 阶段 1 并行。 run_self_reflection_async(self_reflection.py:379)用 asyncio.gather 同时判所有图, 每张走 _judge_one_image(self_reflection.py:298)。
  • 解析很宽容。 _parse_image_judge_response(self_reflection.py:252)先用正则抠 Score/Reasoning 两行,抠不到再退回当 JSON 解析。裁判提示词被要求只返回这两行、别返回 JSON。
  • 阶段 2 的模板用 str.format 注入两个占位符:{image_reasonings}{action_history_log} (_render_final_verdict_user_prompt,self_reflection.py:186)。所以模型写裁判配置时,任何字面 花括号都得写成 {{ }}——base.yaml 提示词专门警告了这点(config/base.yaml:277)。
  • verdict 只认最后一个 Status: 行。 _parse_final_verdict(self_reflection.py:283)取最后一处 status: 后面的 success/failure;缺失或畸形一律当 FAIL,退出码 1。
  • 截图自动发现。 不传 images 时,--auto-latest-run(默认 final_runs)会找编号最大、且有截图的 run_<id>/screenshots/(_discover_latest_run_screenshots,self_reflection.py:107), agent 无需手动列图。

模型只写一次「四段提示词」。 agent 把 image_judge_system/user + final_verdict_system/user 四段 提示词写进 self_reflect_config.json(一次性),之后每次调裁判都复用同一份;并行打分、聚合、解析都由工具 代劳(config/base.yaml:206 的「Task Reflection Tool」明令别自己手搓 judge.py 循环 image_qa)。


3. image_qa:轻量视觉问答

文件:src/webwright/tools/image_qa.py。比 self_reflection 简单得多:给一张或多张图 + 一个问题, 返回 {answer, evidence[], unknown, confidence} 的 JSON(run_image_qa,image_qa.py:71)。

用途:探索阶段让模型核对 UI 状态——「BMW 筛选 chip 到底选中了没?」(config/base.yaml:184)。 它只做一次纯文本补全(base.py:565__call__),不参与完成门禁。工作区提示词禁止模型自己手搓 判图逻辑,探索用 image_qa、终审用 self_reflection,分工明确。


4. 门禁与截图裁判怎么接上

把 [01 章] 的 Agent 门禁和本章的裁判工具串起来看:

模型 done=true


Agent._tool_gate_error(default.py:208)
│ 读 workspace_dir/final_runs/ 里 id 最大的 run_<N>
│ 找 run_<N>/self_reflect_result.json
├─ 文件不存在 / 解析失败 / predicted_label != 1
│ └──▶ 返回一段“具体该怎么补救”的错误串 → Agent 注入为 user 消息 → 模型继续干
└─ predicted_label == 1
└──▶ None(放行) → Agent 造 exit 消息 → 循环结束,吐 final_response

注意错误串是可操作的:比如「predicted_label=0,去诊断 self_reflect_result.json 里的失败原因、 改 final_script.py、在新的 run_<N+1>/ 里重跑、再跑 self_reflection」(default.py:260)。 这把「失败」变成了「带修复指引的下一步」,而不是死胡同。


5. 配置系统:堆叠、合并、快照

上面这套流程能跑起来,全靠配置层把 Agent/Model/Environment 拼对。文件:src/webwright/config/__init__.py

5.1 堆叠 + 递归合并

命令行 -c base.yaml -c model_openai.yaml -c task_showcase.yaml 会依次加载、后者覆盖前者, 用 recursive_merge(utils/serialize.py:8)深度合并(嵌套 dict 递归合,非 dict 直接覆盖)。 还支持内联覆盖:-c agent.step_limit=200 这种 key=value 会被 get_config_from_spec (config/__init__.py:33)拆成嵌套 dict 合进去。合并时 UNSET 哨兵值会被跳过(serialize.py:16)—— CLI 用它表达「这个字段不覆盖,保持下层默认」(cli.py:71 里大量 ... or UNSET)。

所以「模式」本质就是配置组合:

想要叠哪些配置
OpenAI + 工作区模式base.yaml model_openai.yaml
Claude + 工作区模式base.yaml model_claude.yaml
实时浏览器 + OpenAIbase.yaml local_browser.yaml model_openai.yaml
工作区 + 产出可复用 CLI 工具/dashboardbase.yaml model_openai.yaml task_showcase.yaml

5.2 快照:为复现,也为工具复用

snapshot_config_specs(config/__init__.py:46)在每次 run 开始时,把用到的每个配置文件原样拷进 output_dir/config_snapshot/,并写一份 merged_config.yaml(合并后的最终配置)。这有两个用途:

  1. 复现: 事后知道这次 run 到底用了什么配置。
  2. 工具复用同一个模型(关键)。 self_reflection / image_qa 需要一个模型客户端。 load_tool_model(tools/_model_config.py:67)默认就去读 <workspace_dir>/config_snapshot/merged_config.yaml 里的 model: 块,造出和 agent 完全相同的 模型(_model_config.py:43 的解析顺序)。这就是为什么裁判用的是同一个 Claude/GPT,也是为什么 Anthropic 跑不需要额外 OpenAI key(README.md「Run」)。

6. 巧妙之处

  • 「完成」是磁盘事实,不是模型断言。 predicted_label==1 的 JSON 文件必须真存在 (default.py:259),模型骗不过去。这是抗幻觉的核心。
  • 裁判两阶段:先逐图冷静打分,再带着日志聚合表决。 逐图并行、聚合一次,兼顾细节与全局 (self_reflection.py:379)。
  • 失败=可操作的下一步。 门禁错误串直接告诉模型该建 run_<N+1>、改脚本、重跑(default.py:260)。
  • 配置快照一举两用:复现 + 让工具零配置拿到同一个模型(_model_config.py:57)。
  • 提示词把「别自己手搓 judge」写死。 强制走内置 self_reflection,保证裁判口径统一 (config/base.yaml:208)。

7. 边界与局限

  • 裁判也是模型,会看错。 self_reflection 用同一个模型看自己的截图,存在「自己批自己作业」的偏差; 提示词靠「harsh evaluator」的措辞和逐图打分来缓解,但非万能。
  • 门禁只保护工作区模式。 实时浏览器模式 require_self_reflection_success: false,没有这层保护 (config/local_browser.yaml:78)。
  • 强依赖截图证据。 没截到关键 critical point 的图,裁判就判不了 success——所以提示词反复强调 「多存证据截图」(config/base.yaml:203)。
  • 占位符转义是坑。 final 提示词里的字面花括号忘了写成 {{/}} 会让 str.formatKeyError(self_reflection.py:199 显式报「未知占位符」)。
  • report.json 只在叠了 task_showcase.yaml 时产出(README.md 的 Note),普通 run 只有 trajectory.json 和 debug 产物。

8. 代码地图(导航索引)

主题文件路径符号名
完成门禁src/webwright/agents/default.pyDefaultAgent._tool_gate_error
截图裁判编排src/webwright/tools/self_reflection.pyrun_self_reflection_async
逐图打分src/webwright/tools/self_reflection.py_judge_one_image
打分解析src/webwright/tools/self_reflection.py_parse_image_judge_response
终审解析src/webwright/tools/self_reflection.py_parse_final_verdict
截图自动发现src/webwright/tools/self_reflection.py_discover_latest_run_screenshots
视觉问答src/webwright/tools/image_qa.pyrun_image_qa
工具模型解析src/webwright/tools/_model_config.pyload_tool_model
配置堆叠/加载src/webwright/config/__init__.pyget_config_from_spec
配置快照src/webwright/config/__init__.pysnapshot_config_specs
递归合并 + UNSETsrc/webwright/utils/serialize.pyrecursive_merge / UNSET
入口:拼配置跑一次src/webwright/run/cli.pyrun_one

9. 横向对比(ai-agent-reference / browser-agents)

README.md 自带一张与同类的对比,提炼其架构取舍:

项目动作空间「状态」是什么循环形状
Stagehand(Browserbase)Playwright 代码 或 自然语言→翻译成 Playwright浏览器会话命令式,agent() 需要时多步
agent-browser(Vercel)离散子命令(open/click @e2/snapshot)浏览器会话(守护进程跨调用持有)每次 CLI 调用一个微步
browser-useLLM 选索引化的 click/type 动作浏览器会话观察→预测下一动作→执行→循环
Webwright自由 Python(自己写 Playwright 脚本)本地工作区:代码/截图/日志。浏览器可丢弃写代码→执行→看截图→修复

Webwright 的独特取舍: 别人把「浏览器会话」当状态,它把「工作区代码」当状态;别人枚举动作, 它用整段代码当动作。代价是每步更重(工作区模式还每步重开浏览器),回报是长任务更稳、产物可复跑可复用—— 这与 README.md「Performance」里长任务基准(Odysseys +15.6 分)的结论一致。

同 shelf 兄弟: 与 computer-use 类 agent(操作整台 GUI)、编码 agent(落到代码文件)相比, Webwright 属于「browser-agents」里把「code-as-action」推到极致的一支——它把浏览器任务直接退化成 「写并调试一个 Playwright 程序」的编码任务。