agent-browser
天下兴亡,匹夫有责。——顾炎武
一次一条命令:agent-browser 把浏览器拆成一小步一小步
先算一下「用浏览器」这件事的成本
让 agent 上网,通常有三种做法。
一种是给它一个浏览器自动化框架的 API,让它写脚本;一种是给它一个云端的浏览器实例,让它通过协议操作;还有一种是给它截图,让它看着图猜。
三种做法都绕不开同一个问题:上下文。浏览器是一个巨大的状态机,一次把所有东西塞给模型,它会被淹没;每次只给一点点,它又不知道自己站在哪。
agent-browser 的答案是把这件事拆到最小:一次一条命令。
它的描述只有一行:给 AI agent 用的浏览器自动化 CLI。
项目地址:https://github.com/vercel-labs/agent-browser
正文说得更清楚:一个用 Rust 写的原生 CLI,给 agent 一个可以一次一条命令驱动的浏览器。snapshot 把页面返回成紧凑的文本,每个元素带一个像 @e1 这样的 ref。agent 对着 ref 操作,再读一次页面确认,如此往复。
而浏览器在命令之间是开着的,所以每一步都只是一次很短的 shell 调用。
这句话是整套设计的支点。
短调用,长状态
传统自动化脚本是「一个脚本干完所有事」:打开、点击、等待、断言、关闭,全在一次执行里。
这种模式对人是友好的——写完就跑。但对 agent 不友好:agent 需要在中途看结果、改主意、处理意外。如果每一步都要重启一个浏览器、重新登录、重新导航,那「看一眼再决定」这件事的成本会高到没法做。
agent-browser 把状态留在浏览器里,把控制权交给一次次短调用。
于是循环变成了这样:
1 | agent-browser open example.com |
五条命令,对应五个决策点。中间任何一步发现不对,agent 都可以停下来、换个思路、再试。
这不是「更快的自动化」,这是把自动化变成对话。
那个 snapshot 长什么样
example.com 的快照只有一行:
1 | - link "Learn more" [ref=e1] |
一行。一个链接,一个名字,一个引用。
这就是它能工作的原因。「把页面转成紧凑文本」这句描述里,「紧凑」两个字是核心。一个页面的 DOM 可能有几千个节点,截图可能有一千多像素高,而一段可读文本加引用,可能只有几十个 token。
顺便说,那个 - 前缀很讲究。它让输出天然像一个缩进列表,模型读起来跟读大纲一样。这不是装饰,这是给模型设计的排版。
那条我最喜欢的规则
文档里有一段话,我认为是整个项目的价值观所在:
当目标被隐藏、被禁用、在视口之外,或者被另一个元素——比如一个同意横幅——遮住时,点击会失败,而不是报告成功。
后面跟着三步处理:关掉遮挡的元素,重新拍快照,重试。
这句话值得停下来想。
在浏览器自动化里,「点击」这个动作有一个历史悠久的谎言:很多工具会告诉你点成功了,实际上它只是把事件派发到了一个坐标上,而那个坐标下面可能是横幅、可能是空白、可能是另一个按钮。
对脚本来说这没什么,因为脚本会自己去断言。但对 agent 来说,这就是灾难:它会认为这一步完成了,然后基于一个错误的前提继续往下走,十步之后你才发现它一直在操作一个从来没打开过的页面。
agent-browser 的选择是让点击诚实地失败。宁可让 agent 多走一轮,也不让它带着幻觉前进。
这和它另一条规矩是一套的:ref 描述的是快照那一刻的页面,导航或 DOM 变了之后,必须重新拍快照再用 ref。
两条规则合起来,是一句话:这个工具不允许你拿着旧地图找新路。
背后:一个 daemon,一小时
架构那一段写得很清楚。
CLI 把每条命令发给一个后台 daemon,daemon 在第一条命令时启动,通过 Chrome DevTools Protocol 控制浏览器。每个 --session 有自己的 daemon 和浏览器。默认情况下,daemon 会在一小时没有命令之后关掉无头浏览器并退出;如果配置了会话持久化,会先保存状态。
这几个决定的组合挺妙。
「第一条命令时启动」意味着你不需要预热,不用 agent-browser start。装完直接用。
「每个 session 一个 daemon 和一个浏览器」意味着并行不打架。你可以让一个 agent 在 session A 里查文档,另一个在 session B 里填表,它们不会互相抢焦点。
「一小时闲置退出」是一个很现实的保洁约定。它承认了:agent 会忘记关浏览器。与其让几十个 Chrome 进程躺在那儿吃内存,不如设一个超时,把垃圾收走。
而「退出前先保存状态」——这一句说明它知道 agent 的工作不能随便丢。
不止一种浏览器
默认是 Chrome for Testing,通过 agent-browser install 下载。
但文档列出的其他选项很值得看:
用 --cdp 或 --auto-connect 接管一个正在运行的 Chrome;用 --engine 切到 Lightpanda 或 Obscura;在 iOS 模拟器里驱动 Mobile Safari;或者接一家云浏览器:Browserbase、Browserless、Browser Use、Kernel、AgentCore。
六种来源,覆盖六种场景。
接管已运行的 Chrome,是给「我这个站点需要我已登录的账号」准备的。云浏览器,是给「本机跑不动或者要并发很多个」准备的。iOS 模拟器,是给「移动端布局又崩了」准备的。
而两个非 Chromium 引擎的存在,说明它没有把 CDP 当成唯一真理——这句话写得很克制,只提了名字,没吹性能。
一个只有几句话的 skill
「给 AI agent 用」那一节,有个设计我很欣赏。
CLI 自带和版本匹配的 skills:
1 | agent-browser skills get core # 核心循环、ref、等待与常见流程 |
而装进 agent harness 的那个 skill,是一个很薄的 stub——它只告诉 agent:去运行 agent-browser skills get core,然后照做。
这个设计解决了一个很烦的问题:文档和版本不同步。
如果 skill 里塞满命令说明,那 CLI 一升级,skill 就成了过期地图。而如果 skill 只是一个跳板,那 agent 每次读到的都是手上这个二进制自己带的说明。
同一个思路也出现在 AGENTS.md 的建议里:第一次用之前,先跑那条命令,然后跟着它走。
给 agent 写文档,和给人写文档不是一回事。人读一次就够了,agent 每次都要重新读;所以文档必须和它手上的工具是同一份。
MCP 客户端也有对应的入口:一条命令起一个 stdio 的 MCP 服务器。
命令表:能点,也能看穿
命令表按组排,粗看有十组。
导航与读取、交互、等待、捕获、页面与网络状态、标签与弹窗、调试与性能、会话与认证、agent 集成、观察、安装。
其中有一组最能说明它的野心:调试与性能。
console、errors、highlight、inspect、trace、profiler、vitals、react、a11y。
一个「给 agent 用」的浏览器工具,为什么要带 profiler 和 vitals?
因为 agent 要干的活不只是「点一下」。「这个页面为什么慢」「这个组件为什么没渲染」「这个按钮的对比度够不够」——这些问题的答案不在 DOM 结构里,在性能轨道和无障碍树里。
而 react 这条命令很有意思:能读 React 的内部状态,意味着它能回答「这个列表到底拿到了几条数据」这种问题,而不只是「屏幕上有几个 li」。
另一组是捕获:截图、PDF、录屏、下载、diff。
diff 值得单独说。它是给「这次改动到底改了哪儿」准备的——agent 改完样式,拍两张图做差,比让它描述「我觉得看起来好多了」靠谱得多。
平台:原生二进制,五个目标
macOS 的 ARM64 和 x64、Linux 的 ARM64 和 x64 各两种 C 库、Windows x64。
npm 包负责挑对的那一个。
这解释了两件事:为什么它用 Rust 写,因为要发原生二进制;为什么它敢说自己「一次命令一次调用」却不担心启动开销,因为每次启动的是个原生程序,不是整个 Node 运行时。
从源码构建要 Node 24 以上、pnpm 11 以上,再加上 Rust。检查也很标准:cargo fmt、cargo clippy、cargo test,以及一个会真的启动无头 Chrome 的端到端测试。
最后那条要单线程跑,也不奇怪。开浏览器的测试,天然不适合并行。
最后
回到开头那个成本问题。
agent 用浏览器,最贵的从来不是「看见」,是「下一步该干什么」——而这件事需要它在中途有机会停下来、看一眼、改主意。
一次性脚本给不了这个机会,全量截图又太贵。
agent-browser 选的是中间那条窄路:把浏览器留在后台,把控制权拆成一条条短命令,每条命令只做一件事,做完就回来汇报。
而它最难得的地方,是那些不允许自己撒谎的规则:点击失败就报失败,ref 过期就要重拍,遮挡物必须先处理。
一个会骗人的自动化工具,你只能用一次。一个会说「这一步没成,你自己看看」的工具,才敢让它跑一整个下午。
