跳到主要内容

引擎家族:DOM 规划器 vs 计算机使用(CUA)

30 秒导读: Skyvern 的「大脑」是可换的。同一个单步循环 execute_step,既能用 Skyvern 自研的 DOM 规划器(看元素树、点元素 ID),也能挂上 OpenAI、Anthropic、UI-TARS、Yutori 的 CUA 模型(看截图、点坐标)。换引擎只改 agent_step 里的一个 elif 分支——感知(截图/爬页)和执行(把动作落到浏览器)这两层原封不动地复用。


1. 这是什么(零基础也能懂)

前面几章讲的是 Skyvern 的三层身体:感知层把网页变成 LLM 能读的东西(01)、决策层单步循环规划动作(02)、执行层把动作精确落到真实元素(03)。

这一章讲大脑——也就是「决定下一步做什么」的那个模型。关键洞察:

大脑是可插拔的。 感知层和执行层是固定的骨架,而「用哪个模型、模型怎么看页面、输出什么形状的动作」是可以整块替换的模块。Skyvern 把这个模块叫 engine(引擎)

一句话类比:引擎就像电钻的钻头。钻机(感知+执行)不变,你可以换木工钻头,也可以换金属钻头——每种钻头擅长的材料不同。Skyvern 提供了 5 种「钻头」。

两条根本不同的路线

所有引擎干的是同一件事:看一眼页面 → 说出下一个动作。但「怎么看」「怎么指」分成两大流派:

流派页面长什么样(给模型看的)动作怎么指目标代表引擎
DOM 规划器元素树(每个可交互元素带一个 ID)「点 元素 ID = 42skyvern_v1
CUA(计算机使用)一张截图(像人看屏幕)「点 坐标 (830, 210)openai_cua / anthropic_cua / ui_tars / yutori_navigator

CUA = Computer Use Agent(计算机使用智能体),指那类「像人一样看屏幕截图、用鼠标坐标操作」的多模态模型。这是 OpenAI/Anthropic 等厂商近两年推出的新能力,Skyvern 把它们接了进来,作为自研规划器之外的备选大脑。

本章不重复 v1 规划器的内部细节(那是 02/03 的活),只讲引擎作为一个可替换插件:有哪几种、切换点在哪、两条路线的取舍。


2. 顶层全景(引擎是怎么插进循环的)

引擎的注册表:两个枚举

所有引擎的「名册」就是一个枚举 RunEngine,外加一个常量把 4 个 CUA 引擎圈起来(skyvern/schemas/run_enums.py:14,RunEngine;:23,CUA_ENGINES):

class RunEngine(StrEnum):
skyvern_v1 = "skyvern-1.0" # DOM 规划器(自研)
skyvern_v2 = "skyvern-2.0" # 2.0 编排器,见 ch06
openai_cua = "openai-cua" # ↓ 以下四个都是 CUA
anthropic_cua = "anthropic-cua"
ui_tars = "ui-tars"
yutori_navigator = "yutori-navigator"

# 一个元组,把「哪些引擎属于 CUA」变成一处真相
CUA_ENGINES = (RunEngine.openai_cua, RunEngine.anthropic_cua, RunEngine.ui_tars, RunEngine.yutori_navigator)

CUA_ENGINES 是全篇的枢纽:代码里凡是要区分「两条路线」的地方,都写 if engine in CUA_ENGINES:。想知道 CUA 和 v1 到底有哪些行为差异?grep 这一个常量就能列全。这个枚举被 schemas/runs.py:53 重新导出,作为运行请求的 engine 字段(默认 skyvern_v1,runs.py:95)。

skyvern_v2 不在 CUA_ENGINES 里——它是更上层的编排器(把一句话目标拆成多个子任务),每个子任务底下仍然跑 skyvern_v1 这套单步循环。本章不展开,见 06

切换点:agent_step 里的一组 elif

引擎在哪里生效?就一个地方——单步循环 agent_step 决定「这一步的动作从哪来」时的分支(skyvern/forge/agent.py:1449-1485)。这是整章最该记住的一张图:

agent_step 一步

scraped_page(截图 + 可能的元素树)已就绪

┌─────────────────┴──────────────────┐
│ 按 engine 选「动作来源」(分支) │
└─────────────────┬──────────────────┘
┌──────────┬────────┼─────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
openai_cua anthropic ui_tars yutori else(skyvern_v1)
_generate_ _generate_ _generate_ _generate_ 建 extract_action
cua_actions anthropic_ ui_tars_ yutori_nav _prompt → LLM →
actions actions _actions parse 元素ID动作
│ │ │ │ │
└──────────┴────────┴─────────┴──────────────┘

得到 list[Action]——形状统一


执行层(ch03)照常处理,不关心动作来自哪个引擎

怎么读这张图: 上游(爬页/截图)和下游(执行动作)都只有一条路;只有中间「产生动作」这一格按引擎岔开五路。五个生成器返回的都是同一种 list[Action],所以下游执行层根本不需要知道大脑是谁。这就是「可插拔」的全部含义。

五个动作生成器一览

引擎生成器函数(agent.py)模型/客户端动作解析器靠什么定位目标
skyvern_v1(走 else 分支,:1487)配置的主 LLM元素树 prompt → JSON元素 ID
openai_cua_generate_cua_actions :2130OpenAI Responses APIparse_cua_actions截图坐标
anthropic_cua_generate_anthropic_actions :2297LLMCaller(Anthropic)parse_anthropic_actions截图坐标
ui_tars_generate_ui_tars_actions :2397UITarsLLMCallerparse_ui_tars_actions截图坐标
yutori_navigator_generate_yutori_navigator_actions :2443YutoriNavigatorLLMCallerparse_navigator_response_to_actions坐标(或 ref → 坐标)

四个 CUA 解析器都在 skyvern/webeye/actions/parse_actions.py(Yutori 的在 sdk/api/llm/yutori_navigator_response.py:77)。注意 skyvern_v1 没有独立生成器函数——它就是那条 else,老老实实建 prompt、调 LLM、解析元素 ID 动作(ch02 详述)。


3. 两条路线的根本差异(核心对比)

这是本章的心脏。DOM 规划器和 CUA 不是「同一件事的两种口味」,而是两种世界观

3.1 世界观差异:元素 ID vs 坐标

DOM 规划器(skyvern_v1)的世界: 页面 = 一棵带 ID 的元素树。感知层把每个可交互元素编号,模型说「点元素 42」,执行层查表找到那个真实 DOM 节点去点。目标是符号化的。

CUA 的世界: 页面 = 一张截图。模型像人一样看图,说「点像素 (830, 210)」。目标是几何化的。

差异直接体现在解析出的动作里。看 CUA 的点击解析(parse_actions.py:454-462,parse_cua_actions):

case "click":
...
action = ClickAction(
element_id="", # ← 注意:没有元素 ID,留空!
x=cua_action.x, # ← 用模型给的像素坐标
y=cua_action.y,
button=button,
...
)

element_id="" 是决定性的一行:CUA 动作根本不携带元素 ID,纯靠 x/y 坐标。而 v1 的 ClickAction 一定带一个真实 element_id(见 ch03 执行层如何用它定位)。执行层对这两种都能处理——有 ID 走 DOM 定位,没 ID 走坐标点击。

3.2 感知层的差异:CUA 只要一张截图

既然 CUA 靠看图,那它不需要费劲爬整棵元素树、也不需要滚动拼接多张截图。感知层因此按引擎降配(agent.py:3253-3256):

if engine in CUA_ENGINES:
max_screenshot_number = 1 # 只截一张(v1 会截多张滚动图)
scroll = False # 不滚动页面

对应地,截图后处理时 CUA 的滚动次数也清零(agent.py:3055-3056,scrolling_number = 0)。而元素树 prompt 只为非 CUA 引擎构建(agent.py:3421):

prompt_name = EXTRACT_ACTION_PROMPT_NAME
if engine not in CUA_ENGINES:
# 只有 v1 才组装「元素树 + 动作指令」这个大 prompt
extract_action_prompt, use_caching, prompt_name = await self._build_extract_action_prompt(...)

所以 CUA 走的是一条更轻的感知路径:一张截图喂给模型,没有元素树 prompt,没有滚动拼图。爬页拿到的元素树对 CUA 基本闲置(Yutori 例外,它用元素树做 ref → 坐标解析,见 3.4)。

3.3 执行节奏的差异:CUA 一次只出一个动作,且关闭完成校验

这是 CUA 最容易被忽视、却很关键的两个行为改动,都由 engine in CUA_ENGINES 把守。

(1) 关闭「完成校验」。 v1 的模型有时会谎报「任务完成了」或忘了报完成,所以 Skyvern 会额外验一道。CUA 不需要——因为它一步只出一个动作,谎报完成的概率低,而多验一道会显著拖慢。代码直接强制关闭(agent.py:656-657,注释说得很明白):

# do not need to do complete verification when it's a CUA task
# 1. CUA executes only one action step by step -- 谎报完成的概率很低
# 2. It will significantly slow down CUA tasks
if engine in CUA_ENGINES:
complete_verification = False

于是当动作是「完成」时,CUA 直接标记为已验证、跳过校验(agent.py:1796-1799,action.verified = True)。

(2) 一次一个动作。 v1 可以在一步里返回一批动作(填完用户名接着填密码接着点登录),一步执行多个。CUA 模型的交互范式是「看一张图 → 出一个动作 → 再看新图」的严格回合制:每个生成器每步实质只产出一个动作,循环再截新图喂回去。这也是为什么 CUA 任务通常步数更多、更慢,但对动态页面(动作后布局大变)更稳。

附带的小优化:openai_cua 连「预取动作异步操作」都跳过(agent.py:1793,if engine != RunEngine.openai_cua:),CUA 也不预取数据抽取摘要(agent.py:1416)、不做投机式下一步规划(agent.py:2525,_speculate_next_step_plan 对 CUA 直接返回)——因为回合制下这些「抢跑」优化没有意义。

3.4 各 CUA 引擎的接入差异

四个 CUA 引擎「都是截图+坐标」,但接厂商 API 的方式各不相同。

openai_cua —— 用 OpenAI Responses API 的原生 computer 工具。 它不走 Skyvern 的 LLMCaller,而是直接调 app.OPENAI_CLIENT.responses.create(...),挂上 computer_use_preview 工具、告诉它浏览器尺寸(agent.py:2141-2162):

first_response = await app.OPENAI_CLIENT.responses.create(
model=cua_model,
tools=[{"type": "computer_use_preview",
"display_width": settings.BROWSER_WIDTH,
"display_height": settings.BROWSER_HEIGHT,
"environment": "browser"}],
input=[{"role": "user", "content": task.navigation_goal}],
...
)

它是有状态的:靠 previous_response_id 串起多轮(agent.py:2262),把上一步的截图作为 computer_call_output 回传(:2246-2255)。还处理 OpenAI 特有的两个东西——安全检查(pending_safety_checks,:2256)和「模型反问」(当模型没给动作、而是提问时,用 cua-answer-question prompt 让 Skyvern 主 LLM 帮它回答,:2207-2227)。

anthropic_cua —— 走 LLMCaller + Anthropic computer 工具。 它按模型版本挑 computer 工具的 beta 版本(agent.py:2317-2329,新模型用 computer_20251124),带 thinking 预算,自己维护 message_history(:2385-2386)。解析时要做坐标缩放:截图可能被缩过,得按窗口尺寸把坐标映射回真实像素(:2388-2394,parse_anthropic_actions 传入 window_dimension)。

ui_tars —— 字节的 Seed1.5-VL 模型,走 UITarsLLMCaller 最简单:把当前截图加进对话、生成响应、解析(agent.py:2416-2434)。它多一道特性开关——DISABLE_UI_TARS_CUA 打开时这个分支直接不走(agent.py:1466-1470),说明它还带实验性质。

yutori_navigator —— 唯一会用元素树的 CUA。 Navigator 模型可以用元素 ref(而非坐标)指目标;Skyvern 在解析前先把 ref 就地解析成坐标,让下游解析器永远只看到坐标(agent.py:2479-2487):

for tc in nav_resp.tool_calls:
args = json.loads(tc["function"]["arguments"])
if args.get("ref") and not args.get("coordinates"):
# 用页面脚本按 ref 反查坐标
result = await evaluate_tool_script(page, GET_ELEMENT_BY_REF_SCRIPT, args["ref"])
if result.get("success"):
args["coordinates"] = result["coordinates"]

它还在最后一步发「停止并总结」信号(:2467-2468),而不是普通的截图回传。这算 CUA 里的混血:主体是截图坐标,但保留了 ref 这条通向元素树的退路。

CUA 引擎的接入方式对比

引擎客户端状态怎么维持特有处理
openai_cuaOPENAI_CLIENT(直调)previous_response_id 串轮安全检查、模型反问
anthropic_cuaLLMCallermessage_history坐标缩放、thinking 预算、按模型选 beta
ui_tarsUITarsLLMCaller对话内累积截图特性开关可禁用
yutori_navigatorYutoriNavigatorLLMCallermessage_historyref → 坐标解析、末步停止总结

三个非 OpenAI 的引擎都通过 LLMCaller 家族接入,而且在 execute_step懒创建并缓存LLMCallerManager(agent.py:797-828),这样多步之间共享同一个会话上下文。openai_cua 是例外——它不用 LLMCaller,而是靠传递 cua_response 对象在步与步之间接力。


4. 巧妙之处(可借鉴的技术)

  • 用一个常量元组承载「路线」概念。 CUA_ENGINES(run_enums.py:23)把「四个引擎共享的一套行为差异」收敛成一处真相。全代码库十几处 if engine in CUA_ENGINES: 的判断,想改动路线边界只改这一个元组。这比在每处硬编码四个引擎名健壮得多。

  • 动作形状统一,把「大脑差异」挡在生成器里。 五个生成器无论内部多不同,出口都是 list[Action](agent.py:1437)。执行层(ch03)因此对引擎一无所知。想加第六种引擎?写个新生成器、加个 elif,下游零改动。这是依赖倒置教科书级的落地。

  • 让 CUA 复用感知层,但按需降配而非另起炉灶。 CUA 明明不太需要元素树,Skyvern 没有为它写一套独立爬页,而是复用同一个 scrape_website、只把 max_screenshot_number=1scroll=False(agent.py:3253)。省代码,又保留了 Yutori 那种「偶尔要用元素树」的可能性。

  • openai_cua 的「反问」回路。 CUA 模型卡住时会提问(如「要用哪个收货地址?」),Skyvern 不是硬塞坐标,而是调自己的主 LLM 结合页面上下文替它回答(agent.py:2207-2227,cua-answer-question prompt)。等于给外部黑箱大脑配了个「本地参谋」。


5. 边界与局限(诚实)

  • CUA 更慢、步数更多。 回合制「一步一动作」+ 关闭批量,注定比 v1 的「一步多动作」交互轮数多。代码注释自己承认完成校验会「significantly slow down」(agent.py:654),所以才关掉。

  • 坐标定位天生比元素 ID 脆。 截图缩放、DPI、动态布局都可能让坐标点偏。anthropic_cua 要专门做坐标缩放(:2393)就是这问题的税。v1 的元素 ID 定位在这点上更稳。

  • ui_tars 带实验开关,yutori 靠外部包。 ui_tars 可被 DISABLE_UI_TARS_CUA 整体关掉(:1466);yutori_navigator 依赖 from yutori.navigator.tools import ...(agent.py:22)这个外部库。两者成熟度不及 v1 与前两个 CUA。

  • CUA 的成本硬编码在生成器里。 openai_cua 把 token 单价直接写死(agent.py:2169/2283,3.0/1e6 输入、12.0/1e6 输出),厂商改价时得手动跟。


6. 横向对比(与兄弟章 / 兄弟库)

  • 引擎切换点建立在 02 决策层的单步循环之上——本章讲的就是那个循环里「动作从哪来」这一格如何岔路。
  • 无论哪个引擎产出的 Action,都交给 03 执行层统一落地;执行层对引擎无感,正是「可插拔」成立的前提。
  • skyvern_v2(06)是更上层的编排器,与本章的引擎是正交维度:v2 负责把一句话拆成多个子任务,每个子任务底下仍可选任一 engine 来跑单步循环。

ai-agent-reference 货架里,「同一套执行层挂多种大脑」是 browser-agent 的通用模式:DOM 规划器代表符号化定位(准、但依赖能爬到干净元素树),CUA 代表视觉化定位(通用、像人,但对坐标脆弱)。Skyvern 的取舍是默认符号化(v1),按需切视觉化(CUA),而不是二选一。


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

主题文件路径符号名
引擎名册枚举skyvern/schemas/run_enums.pyRunEngine
CUA 引擎集合(路线枢纽常量)skyvern/schemas/run_enums.pyCUA_ENGINES
运行请求的 engine 字段(默认 v1)skyvern/schemas/runs.pyRunEngine(re-export)
按 engine 选动作来源(切换点)skyvern/forge/agent.py:1449-1485agent_step
CUA 强制关闭完成校验skyvern/forge/agent.py:656-657execute_step
CUA 感知降配(单截图/不滚动)skyvern/forge/agent.py:3253-3256_scrape_with_type
仅非 CUA 建元素树 promptskyvern/forge/agent.py:3421build_and_record_step_prompt
CUA 的 LLMCaller 懒创建/缓存skyvern/forge/agent.py:797-828execute_step
OpenAI CUA 生成器skyvern/forge/agent.py:2130_generate_cua_actions
Anthropic CUA 生成器skyvern/forge/agent.py:2297_generate_anthropic_actions
UI-TARS 生成器skyvern/forge/agent.py:2397_generate_ui_tars_actions
Yutori Navigator 生成器skyvern/forge/agent.py:2443_generate_yutori_navigator_actions
CUA 动作解析(坐标,element_id 空)skyvern/webeye/actions/parse_actions.py:422parse_cua_actions
Anthropic 动作解析(带坐标缩放)skyvern/webeye/actions/parse_actions.py:575parse_anthropic_actions
Yutori ref → 坐标解析skyvern/forge/sdk/api/llm/yutori_navigator_response.py:77parse_navigator_response_to_actions
CUA 反问回路 promptskyvern/forge/prompts/skyvern/cua-answer-question.j2