跳转至

browser-use v0.13 Browser Harness:薄抽象层设计哲学

📊 Level ⭐⭐⭐⭐ | 12.4KB

来源:数据STUDIO(云朵君)。browser-use v0.13.2 架构拆解——上万行 DOM 处理代码替换为约 600 行 CDP 直连的 Browser Harness,LLM 本来就懂 CDP 协议,厚封装反而阻碍其能力。 → 原文存档

核心洞察:薄抽象胜过厚封装

browser-use v0.13 的设计挑战了一个广泛假设:更好的 AI 工具 = 更完善的 API 封装。

实验结果:在 browser-use 范围内,抽象层越薄,LLM 表现越好。因为 LLM 的训练数据里有海量 CDP 协议文档(Page.navigate、DOM.querySelector、Runtime.evaluate、Input.dispatchMouseEvent),它"本来就懂"如何操控浏览器。给更厚的抽象层(Playwright、Selenium),LLM 反而要多一道"翻译",增加出错可能。

Browser Harness 架构(~600 行)

v0.13 将之前上万行 Python 代码(DOM 元素提取器、元素索引器、点击包装器、目标管理器、看门狗、跨域 iframe 处理器)替换为仅四文件:

文件 行数 职责
run.py ~13 入口点,预加载 helpers,执行用户代码
helpers.py ~192 薄 CDP 包装函数:goto_url、click_at_xy、type_text、capture_screenshot、js、new_tab——可在运行时被 Agent 编辑
daemon.py ~220 维护 CDP WebSocket 长连接,崩溃检测+重连+多实例命名空间
SKILL.md — LLM 运行时指令:怎么用 helpers、优先坐标点击、跨域 iframe 处理

关键设计特性

坐标点击穿透一切:CDP 的 Input.dispatchMouseEvent 在浏览器合成器层(compositor layer)执行——不关心元素在哪个 frame、哪个 shadow root、哪个跨域边界。传统工具需 switch_to.frame()、shadow_root.querySelector(),跨域 iframe 直接死胡同。

运行时自愈:Agent 发现缺 upload_file 函数时,读 helpers.py 源码 → 用 DOM.setFileInputFiles 写实现 → 保存 → 继续任务。不是预设容错,是 LLM + 薄抽象层的涌现行为。

四步循环:Observe(截图+页面信息)→ Decide(最多 3 个动作/步)→ Act(CDP 坐标点击)→ Verify(截图确认 + paint_order_filtering)

Benchmark 数据

browser-use 官方 WebVoyager 基准测试:

ChatBrowserUse (bu-ultra) 78.0% > OSS+BU Hybrid 63.3% > Claude Opus 4.6 62.0% > Gemini 3.1 Pro 59.3% > Claude Sonnet 4.6 59.0% > GPT-5 52.4% > GPT-5 Mini 37.0%

差距 14 个百分点在复杂任务里是"一次成功 vs 多次重试"的区别。

适用性

场景 推荐
复杂多步 Web 操作(跨页面填表/审批/数据提取) ✅ 该用
页面结构不确定、需适应改版 ✅ 该用
探索性调研(竞品/价格监控) ✅ 该用
简单爬虫/固定表单 ❌ 传统 Playwright 更可靠
低延迟要求 ❌ 每步 LLM 调用 1-5 秒
成本敏感批量任务(10 万次相同操作) ❌ 稳定脚本更便宜

深度分析

薄抽象的第一性依据:翻译层才是错误源

抽象层的厚度应由一个可检验的问题决定:模型是否已熟悉这一层协议。LLM 语料里沉淀了海量 CDP 文档,Page.navigate、DOM.querySelector、Runtime.evaluate、Input.dispatchMouseEvent 的命令名与参数格式高频共现,直连 CDP 因此落在模型先验分布之内。

厚封装的代价是插入了一次额外翻译:模型先把自己记得的协议翻成 page.click 这类框架词汇,框架再翻回 CDP。两步翻译间任一处不一致——选择器语法、等待语义、重排后失效的元素句柄——都会表现为"代码正确但结果错误"。翻译层不是保护层,它就是错误源,这也正是 Harness Engineering 所说的"抽象厚度即失败面积"。

适用边界同样清晰:任务越不需要判断力,翻译层的收益越可能反超损耗。固定表单、稳定页面、十万次相同操作的最优解仍是确定性脚本,因为每一次 LLM 调用都要付出延迟与 token 成本。判断力需求越高,越该让模型直面协议;确定性搬运越多,越该用厚封装把判断提前消掉。

600 行替换一万行:被移除的脆弱假设

旧版每一类组件都对应一条关于网页的脆弱假设:DOM 提取器假设"可交互元素能被启发式穷尽",索引器假设"索引在一次交互内稳定",点击包装器假设"语义锚点唯一映射到元素",目标管理器与看门狗假设"标签页生命周期可单线程观测",跨域 iframe 处理器假设"frame 树不会重排"。每条假设在演示页上都成立,在真实站点上都是等待触发的裂点。

四文件把实体压到 run.py(约 13 行入口)、helpers.py(约 192 行薄包装)、daemon.py(约 220 行连接管理)与 SKILL.md(运行时指令)。收益不在行数,而在需要跨组件同步的隐性状态变少:过去用代码堆出来的稳定性,改由"模型每步选对动作 + 观测足够可靠"承担——这是把确定性押注换成了能力押注。

坐标点击与四步循环:穿透能力最终由观测兜底

Input.dispatchMouseEvent 在合成器层执行,命中测试按视口坐标完成,发生在 DOM 树、frame 树与 shadow root 的层级归属之前。跨域 iframe、嵌套 shadow DOM、被 polyfill 包装的组件对它是同一件事:它不查询边界,所以边界不存在。语义路径则逐层叠加失败模式——switch_to.frame() 要先知道 frame 在第几层,shadow_root.querySelector() 要先穿透 shadow boundary,而跨域 iframe 直接让查询路径终止。这也是 Browser Harness 把坐标点击设为首选而非备选的原因。

代价是放弃语义锚点:它只保证"这个坐标收到了点击",不校验点到的是不是目标元素。可靠性因此被重新分配到三处——视口稳定性(滚动与懒加载让旧坐标失效)、截图对合成后图层顺序的忠实度、以及模型对坐标与意图的自检。paint_order_filtering 正是为第三处打的补丁:把被遮挡元素从"看起来可点"里剔除。

运行时自愈:代码即工位,工位可被 Agent 编辑

最说明问题的一份证据是个 git diff。Agent 做上传任务时扫描 helpers.py,发现没有 upload_file,它没有报错退出,而是按既有代码风格用 DOM.setFileInputFiles 写了实现、保存、调用、继续执行;另一次 12MB 文件撞上 CDP WebSocket 约 10MB 的消息上限,Agent 自行把上传逻辑改成块传输。两次都是团队 review diff 时才发现的,没有任何人预写过"缺函数就生成"的分支。

自愈只在薄抽象下可行,前提是"可读面"与"可修改面"是同一份东西且足够小:约 192 行的 helpers.py 能被完整读进上下文、按既有风格增补;上万行的组件体系做不到这一点,模型看不见全貌,任何局部补丁都可能踩到跨模块假设。推论是 harness 的目标从"提供正确的能力"位移为"提供 Agent 能读懂、能改、改完能立刻验证的工位";风险与之同源——自改代码等于在生产路径上打热补丁,可落地的控制是让改动 diff 可见、可回滚,并在 Verify 里复测被改过的能力。

Observe → Decide → Act → Verify 的瓶颈通常不在 Decide。模型选动作的能力已经很强,真正决定成败的是两端观测的保真度:截图漏掉被遮挡元素、paint order 与真实命中顺序不一致时,Decide 就是在错误前提上正确推理,Verify 还会给出假的确认信号。循环能否闭合是个观测问题,不是推理问题。

"每步最多 3 个动作"则是一条误差累积控制:一次输出大量动作意味着中间无法插入验证点,一旦第 2 个动作的前提被第 1 个动作的实际效果推翻,后续动作全部作废并污染上下文。它保护的是上下文信噪比。由此得到的排序是观测质量先于决策算法——同一模型换观测管道(只看 DOM 文本 / 每步截图 / 带 paint order 过滤)带来的差异,会大于换一个同代模型,这与 browser-use Runtime Harness 记录的结论一致。

WebVoyager 数字的条件性解读

官方基准序列为:ChatBrowserUse (bu-ultra) 78.0% > OSS + BU Hybrid 63.3% > Claude Opus 4.6 62.0% > Gemini 3.1 Pro 59.3% > Claude Sonnet 4.6 59.0% > GPT-5 52.4% > GPT-5 Mini 37.0%。

榜首并不是最强的通用模型,而是与这套 harness 配套训练/对齐的专有模型:同一份约 600 行的 harness,在 bu-ultra 上跑到 78.0%,在同代通用模型上只有 52%–62%。这说明 harness 与模型是耦合关系,而非单向的"harness 服务于任意模型"。

因此只按模型名读这张表会误判 harness 的贡献。这些数字共享同一个 harness、同一套观测管道、同一组任务;要分离两者,必须固定 harness 只变模型,或固定模型只变 harness。否则无法回答差距里有多少来自抽象层设计、多少来自模型训练——这正是 100 行 vs 托管 Harness 的权衡 所强调的读数纪律。

实践启示

  1. 先问"模型是否已熟悉这层协议",再定抽象厚度:CDP、HTTP、SQL 这类语料极厚的公开协议适合薄抽象直连,语料稀薄的私有 API 可能更需要厚封装的可读性。
  2. 把 harness 代码当作 Agent 可编辑的工位,保持"能被完整读进上下文并按既有风格增补"的规模,这直接决定运行时自愈能否发生。
  3. 观测质量先于决策算法:先投资截图保真、paint order 过滤与"到底点到了什么"的确认,再考虑换模型或调提示词。
  4. 用可回滚的小步修改配合自愈:让 Agent 对 helpers 的改动 diff 可见、可回滚,并在 Verify 阶段复测被修改过的能力。
  5. 基准对比必须固定 harness 只变模型(或反之),混着变会把抽象层贡献与模型贡献搅在一起,得出无法归因的排名结论。
  6. 先过一遍适用性门槛:确定性强、重复量大、延迟敏感的任务交给脚本,把薄 harness 留给需要判断力的多步操作与结构不确定的页面。参见 何时不该做 Harness Engineering。

与已有 wiki 实体关系