跳到主要内容

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:53Environment),所以 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(localbrowserbase)和 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 浏览器上下文,无 cookiechromium.launch() + new_context(viewport=...)
local_persistentPlaywright 持久上下文(带 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_cwdrelative_to 做沙箱,模型的 shell 命令跑不出去。

6. 边界与局限

  • exec 模型代码 = 高信任执行。 实时模式直接 exec 模型产出的 Python;工作区模式 shell=True 跑 模型的命令。两者都假设「模型不是对手方」,没有强沙箱(工作区只做了 cwd 限制)。
  • 工作区模式的无状态是刻意的负担。 每步重开浏览器更慢,但换来可复跑脚本——是有意的取舍。
  • 实时模式没有工作区/自校验。 没有 plan.mdfinal_script.py、截图裁判,完成与否全凭模型 (config/local_browser.yaml:78)。
  • local_workspace 还支持 Browserbase 云浏览器,但那需要 BROWSERBASE_API_KEY 等,且靠模型 生成的脚本自己连(local_workspace.py:167);本地 browser_mode: local 是更常用的默认。

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

主题文件路径符号名
环境协议src/webwright/__init__.pyEnvironment
环境工厂src/webwright/environments/__init__.pyget_environment
工作区:执行一条 shellsrc/webwright/environments/local_workspace.pyLocalWorkspaceEnvironment.execute
工作区:cwd 沙箱src/webwright/environments/local_workspace.pyLocalWorkspaceEnvironment._resolve_cwd
工作区:观察快照src/webwright/environments/local_workspace.pyLocalWorkspaceEnvironment._capture_observation
工作区:浏览器模式env varsrc/webwright/environments/local_workspace.pyLocalWorkspaceEnvironment._browser_env
实时:启动/三模式src/webwright/environments/local_browser.pyLocalBrowserEnvironment._prepare_async
实时:CDP 自启src/webwright/environments/local_browser.pyLocalBrowserEnvironment._ensure_local_cdp_browser
实时:执行一步src/webwright/environments/local_browser.pyLocalBrowserEnvironment._execute_async
实时:包协程 execsrc/webwright/environments/local_browser.pyLocalBrowserEnvironment._run_python_code
实时:观察捕获src/webwright/environments/local_browser.pyLocalBrowserEnvironment._capture_observation
浏览器模式常量src/webwright/environments/local_browser.py_BROWSER_MODES

下一步: 模型那一段严格 JSON 是怎么被逼出来、解析出来的?看 03-model-backends.md