跳到主要内容

生产环境中的浏览器 Agent:DOM 脆弱性税

· 阅读需 14 分钟
Tian Pan
Software Engineer

一个日历日期选择器让一个生产环境浏览器 Agent 连续失效三天,无人察觉。设计师在一次小型 UI 改版中,将原生 <input type="date"> 替换为自定义 React 组件。没有 API 变化,没有内容移动,只是新布局中 24px 的单元格——而此前一直可靠点击正确日期的视觉模型,现在偏移了一格,悄悄地把预约订在了错误的日期。

这就是 DOM 脆弱性税:在从未为机器操作而设计的 Web 之上构建自动化 Agent,所持续付出的运营成本。与大多数基础设施税不同,它会复利累积。Web 在变化,反爬虫防御在进化,SPA 越来越动态,而你的 Agent 在悄然退化。

基准测试数据无法让你对此有所准备。顶级系统在精心策划的网页导航任务上宣称准确率超过 90%。而真实生产环境中,面对混乱多变的流量,落地结果接近 50–60%。这个差距不是测量误差——它反映的是受控基准测试无法暴露的结构性失败模式。

浏览器 Agent 到底如何失败

浏览器 Agent 的失败有三种截然不同的方式,理解失败模式才能决定如何修复。

坐标漂移。 基于截图的 Agent 将浏览器窗口转换为图像,通过位置识别目标元素,并在像素坐标处点击。当一个元素移动——哪怕只是因为字体变化、边距增加或相邻元素尺寸变化而偏移 10 个像素——点击就会落错位置。这对紧凑型 UI 尤为致命:日历网格、数据表格和多列表单。即使语义目标完全没变,视觉模型在这里依然举步维艰。

DOM 重构。 基于 CSS 类或 XPath 的选择器,在开发者重构 HTML 时会悄然失效。原来位于 div.sidebar > button.primary 的按钮,经过设计系统迁移后变成了 div.nav-panel > div.actions > button。功能没变,用户体验没变,但 Agent 的定位器停止工作了。这是最古老的 Web 自动化问题,AI 并未解决它——只是将其从显式故障(测试失败)转变为静默退化(Agent 做了错误的事)。

时序与状态假设。 大多数 Agent 隐式假设页面在初始渲染完成时就"就绪"了。SPA(React、Vue、Angular)彻底打破了这个假设。DOM 加载了——但组件还在获取用户数据。按钮渲染了——但点击处理程序还未绑定。搜索框可见了——但自动补全服务尚未初始化。不考虑异步状态的 Agent,要么过早操作未加载的内容,要么在永远不会到来的信号上超时等待。

最难调试的 Bug 会将三者叠加:元素改变了位置,组件发生了重构,页面还是异步加载的。没有任何单一防御策略能同时应对三者;你需要分层防御。

SPA 问题比看起来更糟

单页应用不只是更难抓取——它们在架构上与朴素的浏览器 Agent 假设根本不兼容。

传统 Web 自动化等待 DOMContentLoadedload 事件,然后行动。在 React 应用中,这两个事件在你的实际内容存在之前就触发了。真实数据通过 JavaScript 包执行后的异步 API 调用到达。组件基于这些数据渲染。交互状态在 effect 中初始化。这整个链条都在两个标准"就绪"信号之后运行。

朴素的修复方案——等更长时间——会引发另一个问题。许多 SPA 有持续的后台活动:分析 ping、WebSocket 心跳、周期性数据刷新。等待 networkidle 会无限挂起,因为网络永远不会空闲。你最终得到的 Agent,要么在健康页面上超时,要么与从未为其设计的动态内容赛跑。

正确的做法需要明确的状态验证:等待特定元素出现、特定 API 调用完成或特定 DOM 状态稳定——而不是模糊的网络信号。这需要对每个站点或每种组件类型单独设计 Agent 的等待逻辑,当你在数十个 Web 应用上自动化时,这根本无法规模化。

基于 Canvas 的应用更进一步加剧了这个问题。Google Sheets、Figma 和 Canva 在 HTML5 Canvas 上渲染。没有 DOM 树可检查,没有可访问性节点可查询,也没有 CSS 选择器可用。视觉方法是唯一的选择——但视觉坐标对缩放级别、窗口大小和像素密度极为敏感。在 1x 显示缩放下有效的方法,在 Retina 显示屏上会失效。在默认缩放下有效的方法,在用户放大时会失效。

反爬虫防御正在针对你

反爬虫系统的设计初衷是阻止爬虫和虚假账户。浏览器 Agent 看起来和爬虫一模一样。

Cloudflare 的检测栈结合了 TLS 指纹(验证浏览器的 TLS 实现是否符合真实浏览器模式)、浏览器指纹(Canvas API 渲染、WebGL 输出、音频上下文行为)、行为信号(鼠标移动轨迹、点击时序、滚动速度)和 IP 声誉评分。每个信号都贡献于一个信任分数,决定是放行、发起挑战还是拦截请求。

行为信号层是当前 AI Agent 最一致地失败的地方。人类表现出特征性的移动模式:平滑的光标轨迹、真实的停留时间、偶尔的回退操作。浏览器 Agent 以匀速直线移动,以毫秒精度点击,从不误触错误元素。这些模式在统计上与人类行为可区分,即使没有任何内容分析。

Cloudflare Turnstile 作为验证码的替代品被引入,运行一个非交互式挑战,分析浏览器环境、操作系统特征和交互模式。即使 Agent 成功渲染页面并执行 JavaScript,行为信号也会在任何任务逻辑运行之前触发挑战。

军备竞赛是真实的,而且在加速。规避插件(Selenium Stealth、Puppeteer Stealth、Playwright Stealth)曾有效多年。现在它们被主要反爬虫厂商专门针对和拦截了。新技术出现、被检测、被拦截,整个周期以月为单位。任何将隐蔽性作为主要可靠性策略的 Agent,都在一台永远赢不了的跑步机上。

实际含义:浏览器 Agent 架构需要将反爬虫干扰作为基线失败模式来假设,而不是边缘案例。为被检测设计,而不是为躲避检测设计。

元素定位策略:稳定性谱系

并非所有元素定位器的脆弱程度都相同。稳定性谱系从最脆弱到最稳健排列:

XPath 和 CSS 类选择器是最脆弱的。它们编码的是实现细节——元素层级、CSS 命名约定、DOM 结构——而不是意图。不改变行为的 UI 重构会让这些选择器悄然失效。它们写起来快、执行快,这就是被过度使用的原因。在必须使用这种方法时,优先使用 idnamedata-* 属性而不是结构性选择器。

可访问性树定位器要稳定得多。可访问性树是 DOM 的简化视图,暴露语义含义:元素角色(按钮、标题、文本框)、可访问名称(来自 aria-labelaria-labelledby 或可见文本)和 ARIA 属性。Playwright 的 getByRole() 查询这棵树。一个被新 div 包裹、获得了 CSS 类或改变了样式的按钮,在可访问性树中仍然显示为具有相同可访问名称的 Button。该树对实现变化是稳定的,但对可访问性质量敏感——实现不良的自定义组件通常没有有意义的可访问性语义。

语义定位器(Google 的开源方案)在可访问性的基础上构建了人类可读的语法:{button 'Create'} 定位可访问名称为"Create"的按钮。它们自动强制良好的可访问性标记,对用户不可见的变化有弹性。它们要求底层 HTML 具有语义结构,这排除了许多遗留应用和自定义 Canvas 组件。

带验证的视觉定位器对 HTML 重构最有弹性,但对布局变化最敏感。视觉模型可以识别"右下角的蓝色提交按钮",无论 DOM 结构如何,但当按钮移动、在设计更新中改变颜色或被模态框遮挡时,同样的描述就失效了。关键的补充是验证:点击后,确认预期的状态变化发生了。基于截图的确认——比较操作前后的状态——能捕获点击落点正确但无效的情况。

实践建议:不要选择单一策略。同时使用多种策略,在主要方法失败时沿层级向下回退。视觉识别用于初始元素发现,可访问性树用于稳定交互,当两者都失败时回退到语义匹配。

加载中…
Let's stay in touch and Follow me for more thoughts and updates