让大模型操作浏览器跑了两周,我发现瓶颈根本不在模型

11 月 10 号下午,我让一个 Agent 帮我填一张内部报销表单。十七个字段,它填了十四分钟,最后卡在一个下拉框上------那个框是自定义组件,真实结构是一层 div 加一个隐藏的 select,点击要落在那个 div 上。它试了七种方法,包括往隐藏 select 里塞值然后派发 change 事件,全都不行。同一时刻,我打开网页手动填完只用了 40 秒。

这件事让我重新想了一下:浏览器 Agent 到底难在哪。过去半年大家讨论的都是模型看得清不清屏幕、坐标点得准不准。跑两周之后我的结论不太一样:难点在页面的不确定性,而现在的解法几乎都是绕开它。

截图这条路线,天花板很低

纯视觉方案(截一张图,让模型输出点击坐标)是 computer use 带火的,后面各家都在跟。它的好处是通用:任何界面都能看,不用适配。

坏消息有三个,而且是叠在一起的。

坐标这件事本身不稳定。同一个按钮在不同窗口宽度、不同缩放比例下位置不一样,视口 960 和 1280 是两套坐标。模型给出的坐标偶尔会偏到相邻元素上,而相邻元素往往正好是删除。

截图很贵。一张图进上下文就是几千到上万个 token,而一次真实任务要几十张。两周里我最贵的一次消耗就来自一个纯视觉任务,烧掉的钱够我手动做完二十遍。

它没有稳定的状态。视觉模型看到提交按钮变灰,推断大概提交成功了。这个推断在很多系统里是错的,变灰也可能只是防抖。DOM 里一行 aria-disabled 就写清了的事,非要靠像素猜。

走 DOM 的三条路,各自的坑

现在主流的做法是别只看像素,把可交互元素喂给模型。实现上分三种。

无障碍树。浏览器能把 a11y 树导出来,role 加 name 加状态,比 HTML 干净得多,体积也小。这是我最推荐的起点。它的坑在于:页面作者没写语义的时候,导出来的树和截图一样没法用。一个 div 套 span 的按钮,a11y 树里就是一段无名文本。我那次卡住的报销表单正是这个情况。

给元素打引用标记。自动化工具先扫一遍页面,给每个可交互元素注入一个临时 ID,模型只回"点 e761",工具自己找元素。这样坐标彻底消失了,精度问题一次性解决。代价是每轮都要重新打标,页面一动 ID 就作废,而且模型偶尔会引用一个已经消失的 ID------这时不能猜,必须停下重扫。

直接把 HTML 塞给模型。省事,但一份现代页面的 HTML 动辄几十万字符,里面大部分是无用的样式表和埋点属性。真要用,得先做裁剪:只留交互标签,把样式、脚本、内联事件全删掉。

大厂现在押的方向不太一样

我观察了一圈,重心正在从"让模型更像人"转到"给模型更好的接口"。

一类是把浏览器本身改掉。市面上出现了几款给人机协同用的浏览器,页面内容对 Agent 开放结构化读取,而不是让它隔着截图猜。这思路是对的:与其优化眼睛,不如让界面自己说清楚。

另一类是 MCP 这条路,把工具能力标准化暴露出来。浏览器以 server 的形式提供导航、点击、执行这些原子动作,模型只负责编排。这样确定性就落在工具层,而不是模型的目测能力上。

还有一类干脆绕过界面,让模型写脚本在页面上下文里执行。这是最快也最脆的一条。快,是因为一次执行能顶几十次点击;脆,是因为它依赖选择器,而选择器是前端代码里最容易被重构掉的东西。我自己就吃过:某个组件库升级后类名从 submit-btn 变成 form-action,我的脚本静默失效了三天,期间跑出来的成功日志全是假的。

真正有效的三件小事

跑完两周,我留下的改动就三个,都不高级。

所有点击之后必须回读证据。不要相信"我点了",要相信"输入框现在为空"或者"页面里出现了这段文本"。这一条把假成功基本清空了。

一次点不中就不重试。这是最反直觉的。Agent 点了没生效,再点一次,很可能触发的是双击、或者把已经打开的弹层关掉。收藏、点赞这种开关型动作,二次点击等于撤销。我现在写死:动作失败就停下来报告,让编排层决定要不要换路径。

能读接口就不读界面。列表进度这种信息,DOM 里找比截图便宜十倍,而且不会有歧义。界面留给真需要人看的部分。

顺便说一句,这三条里没有一条跟模型能力有关。它们全是流程约束,写在编排层,写在工具层。这是我觉得最值得说的一点:过去半年我们习惯把 Agent 的问题归给模型,但真跑起来才知道,工程侧欠的账比模型侧多。

一个不太客气的结论

这两周最大的收获是:我对浏览器 Agent 的期待值往下调了一档,同时对它的可用性往上调了一档。

往下调,是因为模型再强也治不了页面的不确定性。一个不规范的前端组件能让最强模型在那儿耗十四分钟,这不是提示词能修的。上调,是因为只要把确定性做进工具层------引用标记、读证据、不重试------粗糙的模型也完全能干活,而且比我想象中稳。

反过来说,如果一个团队现在要做浏览器自动化,优先做的不是换更贵的模型,而是给自己加一层稳定的元素引用协议和一份强制回读的验收规则。前者让点击变成离散可寻址的动作,后者让看起来成功和真的成功分开。这两件事的边际收益,比换模型高得多。

那张贴不进去的报销单我后来手填了。不过我把那个自定义下拉框的结构抄进了工具说明里,现在同一条路径在这套系统上没再失败过。## 前端工程师要背的这半口锅

写到这里有个不太好听但必须说的判断:浏览器 Agent 的可靠性,一半是前端代码质量决定的。

同一套业务,做成什么样完全取决于写它的人有没有留下可寻址的接口。给按钮加上明确的 role、把 id 写稳、避免把交互挂在纯装饰性的 div 上,这几件事对人类的无障碍用户有用,对 Agent 同样有用。反过来,那些靠 onMouseDown 挂在 svg 上、真实结构是三层 span 的组件,人和机器都要挨冻。

我们团队上个月把一批老组件补了 data-testid 和 aria-label。补完之后同一段自动化脚本的失败率下降了一大半,而模型一个字没换。这件事给我的触动比任何一篇技术周报都大:Agent 时代的可测性,顺手就是可用性。

我现在的做法是把可寻址性当成验收项写进评审:新增交互组件必须有 name,必须有稳定标识,不允许只靠类名定位。听起来像给机器让路,其实给人也让了路。屏幕阅读器用户等的就是这套东西。

一次真实失败的复盘

上周有一次让我印象很深。任务是把一批文章按标签归档,Agent 点了收藏,弹层出现,它回了一句"收藏成功"。实际没成功------弹层里还要勾一个收藏夹再点确定,它把弹层出现当成了完成。

这个错误很典型,它不是没看见,而是把中间态当成了终态。视觉方案在这类多步弹层上尤其容易翻车,因为截图看起来"变化了",模型就认为任务推进了。而 DOM 判断能直接问一句:那个确认按钮还在不在?如果还在,说明流程没走完。

我后来给每一步都配一条"终态断言":这件事做完以后,页面上一定成立的一个条件。写断言的成本很低,一行文本,但它是假成功唯一的解药。没有终态断言的 Agent,它的成功率报表只能当小说看。

顺带一提,断言也不能由模型自己写。它会挑最容易满足的那条。断言得由人写死在任务描述里,或者从需求文档里带下来。这是编排层的责任,不该下放。

相关推荐
用户2986985301425 分钟前
用 Python 自动化把 JavaScript 代码转换为 Word 文档
javascript·python·api
学以智用30 分钟前
正则表达式完整参考手册(JavaScript 版,兼容绝大多数语言)
javascript
北城笑笑37 分钟前
Vue 109 ,从零搭建 Vue3 + Electron 工程:生态、安全桥接与打包选型( Vue + Electron )
前端·javascript·vue.js·安全·electron·vue
Highcharts.js1 小时前
Highcharts智能数据分析平台Orbit 多语言配置技术文档
java·前端·javascript·highcharts·orbit·智能数据分析·设置文档
非优秀程序员12 小时前
第07章 孙德良的阳谋|《互联网产品恩仇录》
javascript·后端
程序员大阳15 小时前
HTML+CSS+JS对话式实战(5)-活动管理页之查看报名名单的实现
javascript·css·html·查看报名名单
新*18 小时前
从一段 WebSocket 代码聊开:bytes 不是 zlib、binaryType 决定一切,15MB 大消息的零拷贝优化
javascript·python·websocket·性能优化·blob·zlib·arraybuffer
OpsEye19 小时前
别再挨个充会员了:多模型编码,最麻烦的从来不是选模型
javascript·ai编程
学以智用20 小时前
jsencrypt.js 完整详解
javascript