Webwright 两种环境与 code-as-action
本章讲什么: Webwright 最核心的思想——「动作 = 代码」——以及它落地成的两个环境类。 看完你能说清「为什么浏览器是一次性的」,以及两种模式各自怎么执行、怎么观察。
1. 先讲思想:动作为什么是「代码」
别的浏览器 agent 的动作空间通常是一小撮枚举:click(元素#3)、type("...")、scroll。
每一步模型只能从里面挑一个。好处是简单,坏处是啰嗦、脆弱、长任务里错误累积。
Webwright 的动作空间 = 自由代码。 模型每turn写一段程序:
| 模式 | 一个「动作」是 | 落到哪 |
|---|---|---|
| 工作区模式 | 一条 shell 命令(常是 python - <<'PY' ... PY 内联脚本) | 交给 /bin/bash 跑 |
| 实时浏览器模式 | 一步异步 Playwright 代码(await page.click(...)) | exec 进活的 page |
这带来 README.md「Motivation」里说的几个好处:循环、函数、抽象都能用,选日期/填表这类多步交互变成
一段紧凑程序;脚本能复跑、能改、能跨任务复用。工作区模式更把它推到极致:整个浏览历史 = 一份
final_script.py。
两个环境都实现同一套接口(prepare / execute / get_template_vars / serialize / close,
协议见 src/webwright/__init__.py:53 的 Environment),所以 Agent 换环境不用改一行循环代码。
环境由 get_environment(environments/__init__.py:21)按配置里的 environment_class 造出来。
2. 工作区环境:把浏览器当一次性进程
文件:src/webwright/environments/local_workspace.py,类 LocalWorkspaceEnvironment。
它要解决的小问题: 给模型一个隔离的工作目录 + 一个能跑任意 shell 的执行器,浏览器的事让模型
在脚本里自己 launch。
2.1 一步是怎么跑的
execute(local_workspace.py:176)拿到动作后:
① 取出 command = action.bash_command
② 落盘:steps/step_NNNN.sh + 追加到 command_history.sh
③ 解析 cwd(必须留在工作区内,否则 ValueError —— local_workspace.py:80 _resolve_cwd)
④ 拼环境变量:WORKSPACE_DIR / BROWSER_MODE / 凭据 / TMPDIR ...
⑤ subprocess.run(command, shell=True, cwd=..., timeout=command_timeout_seconds)
⑥ 采集观察:截断后的输出 + final_script.py 预览 + 最近截图/文件清单
几个要点:
- cwd 沙箱。
_resolve_cwd(local_workspace.py:80)把目标路径resolve()后强制relative_to(workspace_dir),越界直接抛错——模型的命令跑不出工作区。 - 浏览器模式靠环境变量传进子进程。
_browser_env(local_workspace.py:167)注入BROWSER_MODE(local或browserbase)和 Browserbase 凭据,模型生成的脚本据此分支: 本地playwright.chromium.launch(...)还是连 Browserbase 云会话(base.yaml默认browser_mode: local,config/base.yaml:65)。 - 观察是「工作区快照」。
_capture_observation(local_workspace.py:230)不看浏览器, 它给模型看:命令输出(截断到output_truncation_chars)、final_script.py是否存在及其预览、 最近改动的截图和文件列表(_recent_workspace_files,按 mtime 排序)。模型据此知道「我这步产出了什么」。
2.2 无状态是重点
工作区模式没有持久浏览器。base.yaml 的 system 提示词明说:「There is NO persistent browser state.
Every Playwright run must create a fresh browser session」(config/base.yaml:109)。每条命令都是独立子进程,
上一条 launch 的浏览器早关了。状态靠什么续?靠代码和文件:模型把逻辑写进 final_script.py,
下一步 sed -n 看它、增量改它、再跑它。这正是「workspace-as-state」。
3. 实时浏览器环境:持有一个活的 page
文件:src/webwright/environments/local_browser.py,类 LocalBrowserEnvironment(≈568 行)。
它要解决的小问题: 有些任务(尤其要人工登录)需要一个长活的真实浏览器,模型每步直接操控它, 而不是每步重开。
3.1 启动:三种浏览器模式
_prepare_async(local_browser.py:310)按 browser_mode 三选一(常量 _BROWSER_MODES,
local_browser.py:21):
| 模式 | 含义 | 关键行为 |
|---|---|---|
local_launch | 干净的 Playwright 浏览器上下文,无 cookie | chromium.launch() + new_context(viewport=...) |
local_persistent | Playwright 持久上下文(带 profile) | launch_persistent_context(user_data_dir=...) |
local_cdp | 连真实的 Edge/Chrome(CDP 远程调试) | connect_over_cdp(...),推荐用于手动 Google 登录 |
local_cdp 最巧:若目标 CDP 端点(默认 http://127.0.0.1:9222)连不上且允许自启,
_ensure_local_cdp_browser(local_browser.py:262)会去候选路径找 Edge/Chrome 可执行文件
(_CHROMIUM_EXECUTABLE_CANDIDATES,local_browser.py:24),用 --remote-debugging-port=...
- 独立 user-data-dir 把它拉起来,轮询等端点就绪。macOS 上还会用
open -na走应用包启动 (_macos_open_app_name,local_browser.py:145)。local_browser.yaml默认就是这个模式 (config/local_browser.yaml:71)。
3.2 一步是怎么跑的
_execute_async(local_browser.py:391)是这套的核心。它把模型给的 python_code 包进一个协程再 exec:
# 示意,非源码:_run_python_code 的关键手法(真实见 local_browser.py:432)
wrapped = "async def __agent_step__(page, context, browser, playwright, task):\n"
wrapped += textwrap.indent(python_code, " ") # 把模型代码缩进成函数体
exec(wrapped, globals_dict, locals_dict) # 定义出这个协程
await locals_dict["__agent_step__"](page, context, browser, playwright, task) # 用活对象调用它
于是模型写的每步代码里,page / context / browser / playwright / task 都是已经就绪的活对象,
直接 await page.goto(...)、await page.get_by_role(...).click() 即可,无需自己 import 或 launch
(提示词也明令禁止自己 launch/close,config/local_browser.yaml:125)。stdout 被 redirect_stdout
捕获成 python_output 回传(local_browser.py:441)。整步有 step_execution_timeout_ms 超时保护
(local_browser.py:402)。
每步还会:落盘 steps/step_NNNN.py,并把这段代码追加到 script.py(_persist_step_code,
local_browser.py:422)——所以实时模式跑完,script.py 也是一份把所有步骤拼起来的可读记录。
3.3 观察:URL + 标题 + ARIA + 截图
执行完,_capture_observation(local_browser.py:463)从活 page 抓:URL、标题、
body 的 ARIA 快照(模型主要视力)、一张视口截图(存 screenshots/step_NNNN.png)、
本步和最近的控制台输出。每一步都真的操作同一个页面,所以状态天然跨步累积——和工作区模式正相反。
3.4 关闭策略很讲究
close(local_browser.py:521)分情况:prompt_before_close 会等你按回车;keep_open_on_exit 直接
不关(调试用)。CDP 模式下默认不关那个真实浏览器——只有显式开 local_cdp_close_started_browser_on_exit
才 terminate 它(local_browser.py:556),避免把用户自己的浏览器窗口给关了。
4. 两种环境对照(一图记住)
工作区模式 实时浏览器模式
┌───────────────────────────┐ ┌───────────────────────────┐
│ 动作 = bash_command │ │ 动作 = python_code │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ subprocess.run(shell) │ │ exec 成协程,await 调用 │
│ │ │ │ │ (page 活着) │
│ 脚本自己 launch 一次性 │ │ 操作同一个持久 page │
│ 浏览器,跑完就关 │ │ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ 观察 = 工作区快照 │ │ 观察 = URL/标题/ARIA/截图 │
│ (输出/脚本预览/文件清单) │ │ │
│ │ │ │
│ 状态 = 磁盘上的代码文件 │ │ 状态 = 浏览器会话本身 │
└───────────────────────────┘ └───────────────────────────┘
5. 巧妙之处
- 「包成协程再 exec」让模型代码零样板。 模型不用管事件循环、不用 import、不用 launch——
harness 把活对象当函数参数递进去(
local_browser.py:439)。这是实时模式好用的关键。 - 每步都拼出一份
script.py。 两个环境都把逐步代码追加成一个文件,天然产出「可复跑的浏览历史」。 - CDP 自启 + 不乱关。 能替你把真实 Edge/Chrome 拉起来连上,又默认不关它——照顾了「人工登录后 让 agent 接手」的真实工作流。
- cwd 强制留在工作区。
_resolve_cwd用relative_to做沙箱,模型的 shell 命令跑不出去。
6. 边界与局限
exec模型代码 = 高信任执行。 实时模式直接 exec 模型产出的 Python;工作区模式shell=True跑 模型的命令。两者都假设「模型不是对手方」,没有强沙箱(工作区只做了 cwd 限制)。- 工作区模式的无状态是刻意的负担。 每步重开浏览器更慢,但换来可复跑脚本——是有意的取舍。
- 实时模式没有工作区/自校验。 没有
plan.md、final_script.py、截图裁判,完成与否全凭模型 (config/local_browser.yaml:78)。 local_workspace还支持 Browserbase 云浏览器,但那需要BROWSERBASE_API_KEY等,且靠模型 生成的脚本自己连(local_workspace.py:167);本地browser_mode: local是更常用的默认。
7. 代码地图(导航索引)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 环境协议 | src/webwright/__init__.py | Environment |
| 环境工厂 | src/webwright/environments/__init__.py | get_environment |
| 工作区:执行一条 shell | src/webwright/environments/local_workspace.py | LocalWorkspaceEnvironment.execute |
| 工作区:cwd 沙箱 | src/webwright/environments/local_workspace.py | LocalWorkspaceEnvironment._resolve_cwd |
| 工作区:观察快照 | src/webwright/environments/local_workspace.py | LocalWorkspaceEnvironment._capture_observation |
| 工作区:浏览器模式env var | src/webwright/environments/local_workspace.py | LocalWorkspaceEnvironment._browser_env |
| 实时:启动/三模式 | src/webwright/environments/local_browser.py | LocalBrowserEnvironment._prepare_async |
| 实时:CDP 自启 | src/webwright/environments/local_browser.py | LocalBrowserEnvironment._ensure_local_cdp_browser |
| 实时:执行一步 | src/webwright/environments/local_browser.py | LocalBrowserEnvironment._execute_async |
| 实时:包协程 exec | src/webwright/environments/local_browser.py | LocalBrowserEnvironment._run_python_code |
| 实时:观察捕获 | src/webwright/environments/local_browser.py | LocalBrowserEnvironment._capture_observation |
| 浏览器模式常量 | src/webwright/environments/local_browser.py | _BROWSER_MODES |
下一步: 模型那一段严格 JSON 是怎么被逼出来、解析出来的?看 03-model-backends.md。