foril@blog ~
  __            _ _ 
 / _| ___  _ __(_) |
| |_ / _ \| '__| | |
|  _| (_) | |  | | |
|_|  \___/|_|  |_|_|
// developer & blogger
💻theme: auto
[░░░░░░░░░░░░░░░░░░░░] 0%

从低层浏览器原语到 Agent 级能力:browser-use retrofit 复盘

📅 2026-04-07|⏱ ~11 min read|#开发小记

这次 retrofit 的目标,不是再做一个“能跑浏览器任务”的演示层,而是把 browser-use 改造成一个可被 Agent 直接调用的浏览器子代理能力。换句话说,旧能力更像一个低层的浏览器任务原语,负责把一段浏览器操作跑起来;改造后的能力,则需要在控制平面、运行平面、事件协议、人工介入、恢复语义和可观测性上补齐完整链路,才能真正接入更上层的 Agent 系统。

这篇文章记录的是一次典型的“原语升级”。它没有引入一个通用工作流引擎,也没有重写浏览器执行内核,而是沿着最小边界,把原来的 playground 友好型执行能力,改造成了面向 Agent 的 browser sub-agent。

##一、旧能力的问题,不在“能不能跑”,而在“能不能被稳定调度”

最开始的 browser-use 更接近一个低层任务执行器。它擅长接收输入,然后在浏览器里逐步执行动作,但它缺少几个对上层系统很关键的能力。

第一,状态不够稳定。任务跑到哪一步、当前是否阻塞、是否进入等待人工介入,这些信息原本更偏向运行时日志,而不是可以被系统持续消费的状态模型。对 playground 来说,这通常够用,但对 Agent 来说不够,因为 Agent 需要知道任务能不能继续、要不要切换策略、什么时候可以安全恢复。

第二,恢复语义不清晰。旧模型更像“重新开跑”或“继续执行”,但没有明确的安全点边界。对于浏览器任务来说,任意时刻恢复并不安全,特别是涉及表单提交、页面跳转、cookie 变更或外部副作用时,盲目续跑很容易造成重复操作或状态错位。

第三,人工介入没有标准化。浏览器任务一旦需要人来点一下、确认一下、补一段信息,系统需要能明确表达“我现在在等人”,而不是只靠某种自定义日志来暗示。更进一步,人工输入回来后,系统要能验证这份输入是否对应当前安全点,是否还能继续。

第四,事件形状不统一。旧 playground 可以接受比较自由的输出格式,但一旦要接入更大的系统,事件就必须被规范成稳定协议,否则上层消费方会被不同版本、不同字段、不同命名拖慢。

所以这次 retrofit 的本质,不是加功能,而是把原本偏执行器的能力,升级成一个可调度、可恢复、可回放的浏览器子代理。

##二、为什么选择 sub-agent-first,再配一个 thin skill wrapper

这次方案里,最核心的选择是 sub-agent-first,加一个很薄的 skill wrapper。

原因很简单。浏览器任务天然是“带上下文的执行过程”,它不是一个纯函数,也不是一次性 API 调用。上层系统真正需要的,是一个有生命周期、可中断、可恢复、可观测的子代理单元。把 browser-use 作为 sub-agent,意味着它能以完整任务实体存在,而不是被压扁成一串零散动作。

thin skill wrapper 的作用,则是把这个能力暴露成上层容易调用的入口,同时尽量不把业务逻辑塞进 wrapper 里。这样做有两个好处。

一是边界清楚。wrapper 只负责参数入口、会话挂接和最基本的能力暴露,真正的执行、恢复、事件流和人工交互,都放在 sub-agent 和运行时里。这样能避免上层入口变成一个越来越胖的“万能适配层”。

二是兼容性好。原来的 playground 用户路径并没有被彻底推翻。thin wrapper 让旧的提交方式还能继续工作,至少在基础任务提交上不会立刻断掉,同时新能力可以通过 v1 session 协议逐步接入。这个过渡方式,比一次性切换所有消费方更稳。

说白了,这不是把 browser-use 包成一个更花哨的壳,而是让它成为 Agent 架构里的一个可管理节点。

##三、Node 负责控制平面,Python 负责运行平面

这次 retrofit 里最重要的架构拆分,是把 Node ecos-server 和 Python runtime 明确分工。

Node ecos-server 做控制平面

Node 侧不再参与具体浏览器动作执行,而是负责所有控制平面工作:

  • admission,决定一个运行请求是否可以进入系统
  • durable run registry,持久化每个运行实例的生命周期信息
  • command 和 status handling,接收命令、查询状态、协调状态流转
  • replay handling,提供回放与追踪入口
  • outward normalized v1 event protocol,对外输出统一事件协议
  • queue coordination,协调队列槽位的占用、释放和重新获取

这样做的好处是,Node 侧只需要维护“任务是什么、现在到哪了、还能不能继续、如何回放”,而不需要知道浏览器细节。控制面一旦收敛,就更适合做稳定协议、状态机和对外 API。

Python ecop_fe_ai 做运行平面

Python 侧则保留运行时的复杂性,负责真正的浏览器执行:

  • browser execution
  • cookie verify and merge
  • interrupt emission
  • resume validation
  • safe-point dedupe
  • cancel fast-path

这个拆分的价值在于,运行时的复杂性很高,但它不应该污染控制面。浏览器执行、页面状态、cookie 和人工中断都属于 runtime 关心的问题,把它们留在 Python 侧,能让控制平面保持更稳定的语义。

##四、实时进度不是日志,而是事件协议

为了让上层系统真正“看得懂” browser-use,retrofit 做了统一的实时事件输出。对外的公开事件 taxonomy 只有五类:

  • state
  • progress
  • interrupt
  • result
  • error
  • ping

这套 taxonomy 的重点不在于种类少,而在于语义稳定。state 用来表达会话级状态变化,progress 用来表达执行进度,interrupt 用来表达需要人工介入的阻塞点,result 表达最终结果,error 表达失败,ping 则用于连通性和心跳。

Node 侧负责把来自 runtime 的原始事件归一化成 v1 协议,再对外输出。这里的“归一化”很关键,因为 runtime 内部可以有很多细碎事件,但对外接口不能跟着内部实现一起抖动。上层系统只应该面对稳定的 session 视图,而不是每次升级都要重新解析一套新的日志字段。

这也是为什么事件不能只是“打日志”。日志适合人看,事件协议适合系统看。两者不是一回事。

##五、HITL 不是附加功能,而是运行时状态机的一部分

浏览器任务真正难的地方,往往不是自动化动作,而是中途需要人接手。比如需要登录确认、验证码处理、表单补全,或者页面信息不完整,需要用户补一段输入。

这次改造里,HITL 采用了 waiting_humanrespond / resume / cancel 的命令模型。这个设计有两个要点。

1. 中断必须显式

runtime 一旦检测到需要人类介入,就发出 waiting_human interrupt,而不是默默挂起。这样控制面能立刻把任务标记成等待状态,上层也知道此时任务不是失败,只是阻塞。

2. 恢复必须经过校验

v1 的 resume 不是“任意时刻续跑”,而是只允许 safe-point 恢复。这不是保守,而是必要。浏览器任务的中间状态经常不稳定,尤其是在点击、跳转、输入和 cookie 变化之后。只有在预定义的安全点,系统才知道当前状态足够稳定,继续执行不会重复副作用或破坏上下文。

这意味着 resume 不是一种“时间回溯”,而是一次“从安全点继续”。这套语义虽然更严格,但换来的是确定性。

##六、safe-point 语义和去重,是恢复链路能否可靠的关键

很多浏览器自动化系统的问题,不出在执行,而出在恢复。中断之后,系统到底能不能准确判断“我已经执行过什么,我现在该从哪继续”,这是恢复能力的核心。

这次 retrofit 里,Python runtime 实现了 safe-point dedupe。它的目的,是避免同一个安全点被重复消费,避免因为重放、重试或重复通知导致状态错乱。配合 v1 resume only safe-point 的规则,系统就能把恢复语义收束在有限边界里。

这里的 tradeoff 也很明确。我们放弃了“看起来更灵活”的任意中断续跑,换来的是更强的可预期性。对浏览器自动化来说,这通常是更正确的选择,因为副作用场景太多,灵活并不等于安全。

##七、队列槽位要释放,也要能重新获取

浏览器任务常常不是短命任务。一个会话进入 waiting_human 后,不能继续占着执行槽位不放,否则队列会被阻塞,后续任务也没法进入。

所以这次设计里加入了 queue slot release/reacquire 语义。简单说,任务在进入人工等待时,运行侧要释放占用的队列槽位,让系统资源回到可调度状态;当用户完成输入、任务准备恢复时,再重新获取槽位,继续后续执行。

这套机制解决的是资源公平和调度效率的问题。没有释放机制,长时间等待会拖住整个队列。没有重新获取机制,恢复时又无法保证任务能回到受控执行路径。两者一起,才能让 HITL 不把系统拖死。

##八、可观测性和 replay,不只是给排障用

这次 retrofit 里,另一个很重要的能力是 observability and replay。对浏览器任务来说,可观测性不只是为了“出了问题好查”,更是为了“知道任务为什么走到了这一步”。

Node 侧的 durable run registry 保存了运行的关键元数据,配合事件流,就能把一次任务的生命周期串起来。这样做的价值有三个:

第一,可以看状态。任务当前是 running、waiting_human、completed 还是 error,一目了然。

第二,可以回放。事件和状态都有统一入口后,排障不再依赖碎片化日志,而是可以沿着 session 去重建路径。

第三,可以做兼容判断。旧 playground 和新 v1 协议并行期间,回放机制能帮助确认哪些消费方还依赖旧事件形状,哪些已经适配了新协议。

这里不是引入一个重型审计系统,而是把“任务发生了什么”这件事结构化了。对 Agent 场景来说,这很重要,因为上层系统需要在失败、恢复、人工介入之间做策略判断,而不是只看到最终成功或失败。

##九、和旧 playground 的兼容,是过渡期最现实的要求

retrofit 完成后,旧 playground 大概率还能继续做基础任务提交,这一点很重要,因为它给了消费方一个缓冲期。但也要说清楚,依赖旧事件形状或旧 resume 假设的消费方,需要迁移到 v1 session 协议

这不是向后破坏,而是协议演进。旧 playground 适合“发任务、看结果”的简单路径,新能力则要求更严格的 session 视角。只要消费方还把 browser-use 当成一个“立刻返回的执行函数”,它就会和新的中断、恢复、事件协议产生摩擦。

所以兼容策略不是伪装成没变化,而是保留基础提交路径,同时明确新语义的边界。这样最容易让系统平稳过渡。

##十、这次实现里,真正值得保留的几个工程决策

回头看,这次 retrofit 里最有价值的,不是某一个具体接口,而是几项基础决策。

首先,把 browser-use 定义为 Agent 级子代理,而不是普通任务接口。这让它拥有了状态、恢复和事件边界。

其次,把控制平面和运行平面分开。Node 侧聚焦协议、注册、命令和调度,Python 侧聚焦执行、验证和中断,避免职责混杂。

再次,把事件协议收敛成稳定 taxonomy。上层消费方不再追着内部日志变动跑。

然后,把 HITL 做成状态机的一部分,而不是例外分支。等待人工、响应、恢复、取消,都是正常生命周期,不是补丁逻辑。

最后,把恢复限制在 safe-point。这条看起来保守,但它是保证一致性和避免重复副作用的关键。

这些决定的共同点,是都在收缩不确定性。浏览器自动化最怕的不是慢,而是状态不清、恢复不稳、协议乱跳。retrofit 的目标,就是把这些不确定性一个个收回来。

##十一、验证结果

这次改造完成后,验证结果是清楚的:

  • Node build PASS
  • Node 41/41 tests pass
  • Python 44/44 tests pass
  • Oracle final review PASS

这些结果说明,控制平面和运行平面的改动都已经通过了基础验证,协议、状态流转和运行时行为没有破坏现有可用路径。

##结语

这次 browser-use retrofit 的意义,不只是“让浏览器任务更像 Agent”。更准确地说,它把一个原本偏低层的浏览器原语,改造成了一个具备明确控制边界、稳定事件协议、人工介入能力和安全恢复语义的子代理能力。

如果说旧版本更适合 playground,那么新版本更适合进入真正的 Agent 调度体系。它不追求把所有问题一次性解决,而是把该收束的边界收束起来,把该稳定的协议稳定下来。对于浏览器自动化这种天然状态复杂的场景,这种工程取向,往往比“更聪明”的实现更重要。

$ tree --headings