Webwright 完成门禁与自我校验
本章讲什么: 工作区模式最有辨识度的设计——「模型说完成不算数,得让另一个模型看截图签字」。 以及配置系统怎么把这一切拼起来、怎么让裁判工具复用同一个模型。这是 Webwright 在基准上稳的关键。
1. 为什么需要门禁
问题: 模型很爱「幻觉式完成」——没真正应用筛选、没截到证据,就宣称任务做完了。基准分数会因此虚高、 实际失败。
Webwright 的答案:把「完成」变成一个可核对的磁盘事实。 模型想 done=true,必须先:
- 把任务拆成 critical points 写进
plan.md(每条一个可独立验证的约束/筛选/动作)。 - 写
final_script.py,在final_runs/run_<id>/里跑一次,产出逐 critical-point 的截图 + 动作日志。 - 跑
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 |
| 实时浏览器 + OpenAI | base.yaml local_browser.yaml model_openai.yaml |
| 工作区 + 产出可复用 CLI 工具/dashboard | base.yaml model_openai.yaml task_showcase.yaml |
5.2 快照:为复现,也为工具复用
snapshot_config_specs(config/__init__.py:46)在每次 run 开始时,把用到的每个配置文件原样拷进
output_dir/config_snapshot/,并写一份 merged_config.yaml(合并后的最终配置)。这有两个用途:
- 复现: 事后知道这次 run 到底用了什么配置。
- 工具复用同一个模型(关键)。
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.format抛KeyError(self_reflection.py:199显式报「未知占位符」)。 report.json只在叠了task_showcase.yaml时产出(README.md的 Note),普通 run 只有trajectory.json和 debug 产物。
8. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 完成门禁 | src/webwright/agents/default.py | DefaultAgent._tool_gate_error |
| 截图裁判编排 | src/webwright/tools/self_reflection.py | run_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.py | run_image_qa |
| 工具模型解析 | src/webwright/tools/_model_config.py | load_tool_model |
| 配置堆叠/加载 | src/webwright/config/__init__.py | get_config_from_spec |
| 配置快照 | src/webwright/config/__init__.py | snapshot_config_specs |
| 递归合并 + UNSET | src/webwright/utils/serialize.py | recursive_merge / UNSET |
| 入口:拼配置跑一次 | src/webwright/run/cli.py | run_one |
9. 横向对比(ai-agent-reference / browser-agents)
README.md 自带一张与同类的对比,提炼其架构取舍:
| 项目 | 动作空间 | 「状态」是什么 | 循环形状 |
|---|---|---|---|
| Stagehand(Browserbase) | Playwright 代码 或 自然语言→翻译成 Playwright | 浏览器会话 | 命令式,agent() 需要时多步 |
| agent-browser(Vercel) | 离散子命令(open/click @e2/snapshot) | 浏览器会话(守护进程跨调用持有) | 每次 CLI 调用一个微步 |
| browser-use | LLM 选索引化的 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 程序」的编码任务。