logo

Computer Use:浏览器操作的状态机设计

Published on

Computer Use:浏览器操作的状态机设计

“帮我打开网页,填写表单,再点击提交。”这是 Computer Use Agent 最容易展示的任务。截图发给模型,模型返回一个坐标,浏览器执行点击,看起来就完成了。

但网页不是一张静态图片。页面会加载、跳转、弹窗、刷新,按钮可能因为权限或字段校验暂时不可用,窗口大小和缩放比例也会改变坐标。一个动作即使点中了位置,也不代表点中了正确的元素。

我第一次做浏览器 Agent 时,最初把重点放在“坐标预测准不准”。后来发现,真正的根因通常是系统不知道自己处于什么状态:页面还没加载完就点击了,提交以后没有重新确认结果,或者旧页面的动作晚到,覆盖了新任务。

Computer Use 的核心不是让模型学会更多点击,而是把浏览器操作设计成一个有状态、可观察、可恢复的系统。

一、把浏览器任务建模成状态机

先定义任务状态,而不是直接调用动作:

type BrowserState =
  | 'initializing'
  | 'page_loading'
  | 'ready'
  | 'form_filling'
  | 'waiting_confirmation'
  | 'submitting'
  | 'completed'
  | 'blocked'
  | 'error'

状态转换必须有条件:

page_loading → ready:目标元素存在且页面稳定
ready → form_filling:找到目标表单
form_filling → waiting_confirmation:参数已校验
waiting_confirmation → submitting:用户明确批准
submitting → completed:结果状态已验证

如果页面元素不存在,就不能假设它只是“晚一点出现”,应该进入等待或阻塞状态;如果提交结果没有验证,就不能直接标记完成。

二、观察结果要包含语义和版本

截图有视觉信息,但最好结合 URL、标题、可访问性树、DOM 角色和元素边界:

type PageObservation = {
  observationId: string
  url: string
  title: string
  screenshotHash: string
  viewport: { width: number; height: number; scale: number }
  elements: Array<{
    elementId: string
    role: string
    name: string
    visible: boolean
    enabled: boolean
    bounds?: [number, number, number, number]
  }>
  observedAt: string
}

observationId 用来防止旧动作作用在新页面上。动作执行前检查当前页面版本,如果 URL、截图哈希或目标元素状态已经变化,就重新观察,而不是继续使用旧坐标。

三、动作应该是受限的工具集合

不要让模型直接生成任意 JavaScript 或 Shell。定义有限动作:

type BrowserAction =
  | { type: 'click'; elementId: string }
  | { type: 'type'; elementId: string; text: string }
  | { type: 'select'; elementId: string; value: string }
  | { type: 'scroll'; direction: 'up' | 'down'; amount: number }
  | { type: 'wait'; condition: string; timeoutMs: number }

服务端检查元素是否存在、是否可见、是否启用、是否属于允许的域名和当前任务。elementId 由系统观察生成,不能由模型凭空写一个猜测 ID。

坐标只有在语义定位不可用时才作为候选,并且要转换设备像素和 CSS 像素:

function toCssPoint(x: number, y: number, scale: number) {
  return { x: x / scale, y: y / scale }
}

即使坐标落在按钮范围内,也要在执行前重新确认页面没有变化。

四、等待不能靠固定 sleep

await sleep(2000) 很容易写,却无法保证页面真的准备好了。网络快时浪费时间,网络慢时又不够。应该等待可观察条件:元素出现、按钮启用、加载指示消失、URL 改变或某个状态文本出现。

async function waitFor(condition: () => Promise<boolean>, timeoutMs: number) {
  const deadline = Date.now() + timeoutMs
  while (Date.now() < deadline) {
    if (await condition()) return
    await sleep(100)
  }
  throw new TimeoutError('页面未达到预期状态')
}

等待条件要和任务状态对应。看到页面加载完成,不代表数据请求完成;按钮出现,不代表它已经可点击。稳定性比“快点继续”更重要。

五、表单填写要分为填充和提交

输入框填写通常是可逆的,提交可能产生副作用。两者不要合成一个“填写并提交”工具。

读取字段和规则
填充候选值
重新读取并校验字段
展示摘要和影响
等待用户确认
提交

金额、收件人、日期、权限和附件要在提交前再次校验。用户确认的应该是最终值,而不是 Agent 几分钟前生成的草稿。

六、动作结果要重新观察

每次动作执行后,都要等待并读取新状态:

async function executeAndObserve(action: BrowserAction) {
  const before = await browser.observe()
  await browser.execute(action)
  const after = await waitForChangedObservation(before)
  return { before, after }
}

点击“下一步”以后,可能是页面跳转、错误提示、权限弹窗或按钮无响应。Agent 必须根据结果重新规划,不能直接假设动作成功。

状态变化还要记录时间和来源,方便回放:

observation-41:表单已填充,提交按钮可用
action-42:点击 submit
observation-43:出现二次确认弹窗
action-44:等待人工确认

七、导航和域名必须受限

浏览器 Agent 经常需要打开链接,但页面中的链接可能指向外部域名、钓鱼页面或包含敏感参数的 URL。建立允许列表:

type NavigationPolicy = {
  allowedOrigins: string[]
  allowDownloads: boolean
  allowPopups: boolean
  blockSensitiveQueryParams: boolean
}

跳转前解析目标 URL,检查协议、域名、端口和查询参数。不要因为页面文字说“请打开这个链接”就自动信任它。网页内容是输入,不是权限指令。

八、失败恢复要区分可重试和不可重试

元素暂时没出现,可以等待;网络暂时失败,可以退避重试;权限被拒绝、参数不合法和目标状态冲突,则不应该重复点击。

元素加载超时 → 重新观察 / 有限重试
网络 503 → 指数退避
权限不足 → 停止并提示用户
表单校验失败 → 读取错误并修正
提交结果未知 → 查询状态,不直接重提交

尤其是结果未知的提交。浏览器连接断开,不代表提交没有发生。重新点击可能产生重复订单、重复邮件或重复预约。需要使用业务状态查询或幂等键,而不是凭感觉重做。

九、高风险动作必须人工确认

删除、支付、发送、发布、权限变更和下载敏感文件,都应在动作前暂停:

type Confirmation = {
  actionHash: string
  target: string
  impact: string[]
  expiresAt: string
  approvedBy?: string
}

审批期间如果页面、参数或权限变化,actionHash 失效,需要重新确认。用户确认不能被 Agent 自己模拟,也不能因为任务超时就默认同意。

十、评测看任务结果和动作过程

浏览器 Agent 的基准集应包含页面变化、加载延迟、同名元素、弹窗、权限不足、用户改目标和高风险动作。指标至少包括:

  • 任务成功率;
  • 正确元素定位率;
  • 非法动作率;
  • 平均和 P95 步骤数;
  • 恢复成功率;
  • 重复副作用次数;
  • 越权导航次数;
  • 单次成功任务成本。

最终页面正确但中间发生越权,不能算成功;任务失败但安全停下,可以算一种可接受失败。指标要奖励谨慎和可恢复,而不是只奖励继续往下点。

总结:浏览器操作首先是状态管理

Computer Use 最容易被理解成“模型看截图,返回坐标”。真正做到可靠,需要状态机、语义观察、动作约束、条件等待、结果回读、导航策略、审批和恢复。坐标只是最后的执行细节,不能承担系统的全部判断。

我现在设计浏览器 Agent,会先问:当前页面是什么状态?这个动作针对的是哪一次观察?动作后如何证明结果发生?失败时能不能安全重试?如果提交结果未知,怎样查询而不是重复提交?

一个值得托付的浏览器 Agent,不是从不点错的 Agent,而是点错之前能发现状态不对,点错以后不会继续扩大,遇到不可逆动作会停下来让人确认。把浏览器当成一个会变化的外部世界,把每次操作当成可能产生后果的状态转换,Agent 才会从“会操作网页”走向“能够可靠地完成浏览器任务”。

🤪 您也可以编辑此页: