Tabbit CLI 如何为 Agent 构建浏览器工具:设计与实践
为 Agent 构建浏览器工具:Tabbit CLI 的设计与实践
美团技术团队发布 Tabbit CLI 设计与实现详解,让 Codex、Claude Code、WorkBuddy 等外部 Agent 通过命令行驱动用户的 Tabbit 浏览器完成网页任务,复用已有登录状态。
薄佑思 2026-10-30 10:24 北京
本文介绍工具的设计选择、实现过程中遇到的问题,以及测试结果。
很多日常工作发生在网站上:查资料、修改后台记录、提交表单。这些页面为人设计,依赖用户的登录状态,而且在操作过程中随时可能变化。Tabbit CLI 让外部 Agent(如 Codex、Claude Code、WorkBuddy)通过命令行,在用户自己的 Tabbit 浏览器里完成这类任务。
在内部对比测试中,Tabbit CLI 模型总消耗比 Codex Chrome Control 少 47.2%。本文介绍这套工具的设计选择、实现过程中遇到的问题,以及测试结果。
本文面向正在开发 Agent、对浏览器有一定了解的工程师。对 Tabbit CLI 实现思路感兴趣的一般用户,可以先读《我们是如何打造 Tabbit CLI 的:更快,更省 Token 的 Agent 浏览器操作方案》,该文侧重介绍背景与设计思路,阅读门槛更低。
全文目录如下,可选自己感兴趣的部分跳读:
1. 背景与问题
1.1 为什么做 Tabbit CLI
1.2 Tabbit CLI 概览
2 操作层:复用并适配 Playwright
2.1 为什么选择 Playwright、CDP 和 JavaScript
2.2 复用的部分:三种操作路径
2.3 如何将 Playwright 适配 Agent
2.4 动作之前:定位与就绪
2.5 动作之后:断言与判定依据
2.6 错误诊断与恢复
3 浏览器侧:访问边界与后台执行
3.1 合成浏览器:由真实页面支撑
3.2 跨 frame 与文档变化的页面归属
3.3 后台执行与用户控制
4 程序执行与结果反馈
4.1 以程序为执行单元
4.2 数据处理与结果选择
4.3 回执:执行结果与页面变化
4.4 Skill 设计
5 运行时生命周期与故障恢复
5.1 由浏览器托管的运行时
5.2 为可恢复而设计:保留结果,避免重复执行
6 评测:分别衡量接口与 Agent 行为
6.1 完整配对对比
6.2 衡量设计想要改变的行为
7 经验总结
附录:内部测试与证据
▼
作者:薄佑思,Tabbit 工程师
1. 背景与问题
1.1 为什么做 Tabbit CLI
Tabbit 是基于 Chromium 的 AI 浏览器,内置的 Agent 可以处理网页、本地和云端任务。不过,不少用户已经习惯了其他 Agent 的模型、工具或工作环境。Tabbit CLI 的出发点是:规划仍交给用户选择的 Agent;任务直接复用浏览器里已有的登录状态;浏览器程序在本机执行,不再额外叠加一层模型推理。
接入用户的浏览器后,Agent 面对的是仍在变化的页面:按钮可能移动,提交可能延迟,连接也可能在操作完成前中断。工具需要帮助 Agent 找到正确的控件、核对结果,并在中断后查明已完成的操作。同时,自动化不能操作任务之外的页面,也不能打断用户。我们还希望减少模型往返和不必要的页面读取,让这些检查尽量在程序中完成。
1.2 Tabbit CLI 概览
从 Selenium 等工具开始,开发者已经可以用脚本驱动页面。Agent 驱动的 computer use 则让模型根据页面状态决定下一步。我们沿用已有的自动化能力,由浏览器限制操作范围,并保存多次调用之间的执行状态:
Playwright 负责操作层。通过定位器、自动等待、可操作性检查和断言,确保动作作用于预期目标,并验证结果。
浏览器负责访问控制。浏览器在进程内附着到任务所属的页面,不开放调试端口,并让自动化在后台运行。
由浏览器托管的运行时服务(Runtime Service)负责执行生命周期。它在多次命令之间保持任务的执行状态,并为每次提交记录回执,使丢失的响应可以找回。
我们选择 CLI + Skill,是为了让用户已有的 Agent 直接使用浏览器能力。 CLI 提供程序提交、结果查询和故障诊断的执行入口;Skill 说明如何使用这些入口,以及何时观察、验证和恢复。 这样,规划仍由用户选择的 Agent 负责,Tabbit 无需为 CLI 额外维护一套模型调用循环,也能复用编码 Agent 已有的命令执行和编程能力。
接入时,Tabbit 会检测支持的 Agent 并安装 Skill,让不同宿主复用同一套说明。面对不同版本或多个浏览器实例,tabbit-cli 的启动入口(launcher)统一选择目标实例并调用对应客户端,Agent 无需管理客户端路径、底层连接,也无需另行启动 Node 运行时。
2. 操作层:复用并适配 Playwright
Agent 决定做什么,工具负责在页面上执行。例如,「点击提交」需要找到对应按钮、等待它可点击,再执行点击;操作后还需要检查预期结果。本文把提供定位、等待、动作和验证能力的工具层称为操作层。
早期以 Browser Use 为代表的浏览器 Agent,通常把点击、输入、滚动等动作分别封装成工具,由模型逐次选择调用。随着模型编程能力增强,这类接口开始向代码执行演进:模型可以用编程语言组合多个动作、编写循环,并保留中间结果。Browser Use 在《The Bitter Lesson of Browser Agents》中回顾了从固定动作集合转向模型编写操作和观察代码的过程。Tabbit CLI 也采用程序化的入口,由 Agent 编写 JavaScript,并通过 Playwright 提供操作层能力。
2.1 为什么选择 Playwright、CDP 和 JavaScript
在 Chromium 上,Playwright、Puppeteer 和 ChromeDriver 的底层都是 CDP(Chrome DevTools Protocol)。Playwright 在协议之上提供定位器、等待、frame 处理和错误诊断。选用 Playwright,就能直接复用这些成熟的操作语义,不必让 Agent 再学一套新的浏览器动作实现。
传输层把命令送到浏览器;操作层负责查找目标、等待就绪和报告错误。
在选择操作层时,我们也尝试过 Chromium 的 Actor 路径。Actor 是 Chromium 内部用于组织和执行 Agent 浏览器操作的框架。但在这条路径上,要达到我们需要的操作语义,仍需重新实现定位、可操作性检查、frame 处理和错误诊断。权衡之后,我们保留了 Playwright 的操作层及其底层的 CDP 通信路径:由 Playwright 提供定位、等待和验证能力,通过 CDP 与 Chromium 交互;浏览器侧的工作则集中在页面归属和生命周期上。
选择 JavaScript,是为了让浏览器操作、异步协调、断言和结果处理都在同一个运行时中完成。传给page.evaluate()的函数使用同一种语言,但在页面的执行上下文中运行。
2.2 复用的部分:三种操作路径
复用 Playwright 之后,Agent 可以在同一段 JavaScript 程序中使用三种互补的操作路径:用定位器操作有名称的控件,用原生的指针和键盘输入操作视觉目标,用页面内 JavaScript 做数据提取和计算。
这里的「脚本」指页面内的查询和数据处理;三种路径都可以由同一段 JavaScript 编排。观察和输入也可以走不同的路径:先用截图找到相关的对话框,再用定位器操作其中的按钮;或者用脚本选出符合条件的行,再用定位器逐行更新。
读取数据与注入动作要区分清楚。在 page.evaluate() 中调用元素的 click() 会绕过 Playwright 的可操作性检查。这种用法只能是有意为之,不应成为控件被遮挡或匹配不唯一时的通用补救手段。
以一个确认对话框为例:对话框里有一个Confirm reshipment(确认补发)按钮,背后的表格里还有十个同名按钮。操作必须带上对话框这一上下文:
点击时,Playwright 会重新在这个对话框里查找该按钮,再检查它是否可点击(§2.4)。
2.3 如何将 Playwright 适配 Agent
操作本身沿用原生 Playwright;我们修改了 Agent 常用的 Playwright 中观察页面和接收结果的方式。开发过程中,我们选择不修改 Playwright 的语法与格式,只增补返回内容,移除了会改变常用输入行为的包装层,保留了以下适配:
以 React 首页的截图为例,一张251,071 字节的 PNG,若以文本返回,需要334,764 个 base64 字符。返回文件回执后,这些编码不必进入文本响应;图片由宿主通过其图像输入路径加载,那部分成本另计。
返回类型的变化在接口文档中明确说明。Playwright 底层的浏览器访问适配见 §3.1。
2.4 动作之前:定位与就绪
我们测试了页面让一次预期点击失效的三种方式:移动按钮、复制它的标签、用遮罩层盖住它。
按钮被移走,原位置放置了一个诱饵按钮。
坐标保留的是旧位置,因此点中了诱饵;定位器针对当前文档解析,点中了目标按钮。这就是延迟绑定:定位器保存的是查询而不是元素或坐标,在执行时刻才解析。标签重复时,严格模式拒绝执行动作;把查询限定到预期区域之后,动作又能正常执行。
给按钮盖上一层持续存在的遮罩后,click({force: true}) 正常返回,计数器却显示 目标:0,遮罩层:1。普通点击则超时,计数为 目标:0,遮罩层:0,并报告:
普通点击没有发送输入,避免了误点遮罩层;强制点击跳过了检查,遮挡仍然存在。完整的配对结果见下一节。
2.5 动作之后:断言与判定依据
我们提供 expect,让 Agent 在程序中明确写出操作后的预期结果。点击正常返回,并不意味着保存或提交已经成功;页面可能仍在处理,也可能随后报错。expect 的页面断言会在限定时间内自动重试检查,条件满足后才继续执行,否则抛出错误。这样,Agent 可以根据验证结果判断是否完成,减少固定等待和反复回到模型检查页面的需要。
为了检查点击返回是否意味着保存成功,我们让点击先把页面状态改为 Saving;经过一段受控延迟后,测试页面再把状态改为 Saved 或 Failed。两种情况下点击都正常返回。加上等待 Saved 的断言后,24 次成功保存都通过了检查,24 次失败保存都没有通过。
移动按钮、重复标签、遮挡按钮和延迟保存四种场景各使用 48 对测试页面:共192 对、384 次初次尝试、32 次限定范围后的恢复。判定依据是对计数器和状态的直接回读。这些测试衡量的是特定机制的行为,而不是任意网站上的成功率。
2.6 错误诊断与恢复
一次填写表单的程序因 getByRole('textbox', {name: 'Address *'}) 匹配到多个控件而停止。Agent 检查页面后发现,Email 已填好,邮寄地址仍为空。它保留已填写的字段,为地址定位器加上 exact: true,然后填完剩余字段并提交,表单显示成功。
在33 条轨迹中的 38 次不同的严格模式失败之后,有 20 次的下一步是收紧查询,11 次选择了按位置匹配:
38 次后续处理的分布:12 次加上精确匹配,8 次其他查询修正,11 次按位置选择,7 次其他处理。拒绝执行避免了一次有歧义的动作;Agent 能否恢复,取决于下一段程序。
§2.3 中对错误截断的改动会保留长错误的首尾两端,同时保留出错的调用和最后报告的阻塞原因,例如拦截了按钮点击的遮罩层。Agent 由此可以对症处理:修改定位器、处理遮挡,或者先检查之前的操作产生了哪些效果。
3. 浏览器侧:访问边界与后台执行
Playwright 期望连接的是一个完整的浏览器,而一个任务只应看到属于自己的页面。我们需要在保留 Playwright 接口的同时,由浏览器限制页面访问,并处理导航、弹窗和后台执行带来的问题。
3.1 合成浏览器:由真实页面支撑
Playwright 连接的是一个「浏览器形态」的 CDP 端点,而 Tabbit 只能向它暴露任务自己的页面。浏览器通过 Chromium 的 DevTools 机制在进程内附着页面;独立的运行时服务通过私有传输通道与浏览器通信,不监听任何调试端口。
路由器的测试展示了一条页面命令的处理过程。Playwright 发出:
路由器把 tabbit-page-1 解析为测试中的任务所属页面,去掉合成的 sessionId,再通过该页面的 DevTools 客户端发出 {"id":2,"method":"Page.enable"}。对于跨进程 iframe,路由器保留真实的子会话 oopif-1,让命令到达 iframe 所在的渲染进程。而对测试中不属于该任务的目标 other 发起附着请求,返回的是 -32000: Unknown targetId。
其他浏览器命令按四种方式处理:
Playwright 照常收到其浏览器模型需要的目标和 frame 事件;命令实际能作用于哪些页面和 frame,则由浏览器检查。
3.2 跨 frame 与文档变化的页面归属
只在任务开始时过滤一次标签页,形成不了持久的边界。frame 可能被移到独立的渲染进程,弹窗可能继承上下文,已有的 frame 也可能提交一个新文档。归属检查必须跟着这些变化走。
保持 frame 会话的结构。同进程的 frame 可以与页面共用一个目标,各自有独立的执行上下文;跨进程 iframe 则有自己的 CDP 子会话。Tabbit 记录这些子会话,把它们关联到所属页面,并保留真实的会话标识。主页面的命令在转发前去掉合成的会话外壳;iframe 的命令保留子会话,以便到达正确的渲染进程。把两条路径合并成一个页面会话,会破坏 Playwright 的 frame 行为。
附着时检查真实的浏览器对象。 受限客户端会检查每个 RenderFrameHost,包括它所在的 WebContents、已提交的文档、是否为 WebUI 以及安全主体。归属属于页面,附着检查则针对实际的 frame。这样,普通的任务页面中嵌入的扩展子 frame 可以正常使用,而顶层的扩展文档和浏览器内部的 WebUI 会被拒绝。
任务归属在真实页面、frame 会话、弹窗和文档提交之间的传递;资源仍是同一个标签页,但其中的文档和渲染会话在变化,访问检查必须跟随这些变化。
弹窗的例子说明了为什么只在附着时检查是不够的:
→ 1. 任务所属页面或其中的某个 frame 打开了一个弹窗。Tabbit 接管这个弹窗,并记录它的打开者。
→ 2. 弹窗最初是一个空白文档。如果打开者是扩展,这个空白文档可能继承扩展的安全主体,因此一律拒绝会破坏原本合法的流程。
→ 3. 弹窗随后导航到目标地址。此时由目标地址决定是否允许顶层自动化。
→ 4. 这次导航可能复用同一个 frame host,DevTools 不一定会重新执行最初的附着检查。因此 Tabbit 在新文档提交之前还会再检查一次目标地址。
→ 5. 如果目标地址受限,自动化会解除附着并释放归属,用户的标签页保持打开。
因此,除了检查目标和子会话的归属,我们还在后续版本中加入了文档提交时的检查。页面曾经获准访问,并不意味着它导航后的文档也能继续被操作。这符合完全中介原则:访问检查需要覆盖所有路径,并在权限条件变化后重新执行。
同样的规则适用于底层通道。 增加 newCDPSession(page) 相当于多了一条控制路径。Tabbit 为它创建独立的受限客户端,重新检查归属,并使用同样的 frame 附着策略。原始会话不能发出不受限制的浏览器管理或目标管理命令;目前支持的目标信息查询也限定在任务范围内。独立的 DevTools 附着隔离了协议订阅,调用方的修改不会影响 Playwright 自己的订阅;但两个客户端操作的是同一个页面,效果是共享的。
任务归属限制的是页面控制范围。任务仍运行在用户现有的配置文件中,Cookie 和已登录账号是共享的;对于页面内容中的提示注入(prompt injection),敏感操作仍需由 Agent 宿主和用户授权。
3.3 后台执行与用户控制
后台执行涉及浏览器中的多条路径,拦截一条显式的激活命令只覆盖了其中一条:
焦点模拟让后台页面获得所需的文档焦点状态,同时不切换用户的前台标签页。
4. 程序执行与结果反馈
浏览器任务围绕三个环节反复推进:观察 → 执行 → 核对。核对结果决定任务是否结束;尚未完成时,当前结果成为下一轮观察的起点。
→观察:确定当前状态和操作目标。 读取相关的页面内容、截图或上一段程序的结果,判断下一步需要做什么。例如,预订前先确认日历当前显示的月份。
→执行:用程序完成已明确的步骤。 将定位、点击、输入和数据处理组织成 JavaScript;连续步骤之间不需要新判断时,就在程序中继续执行。例如,翻到目标月份、选择日期并设置人数。
→核对:检查是否得到预期结果。 用断言或业务记录确认日期、人数和预订结果。满足任务要求才结束;仍在处理就等待,出现错误或意外页面则返回相关信息,由 Agent 决定下一步。
这三个环节不要求分别调用一次模型。状态检查和预期结果明确时,程序可以连续执行并核对;需要理解新页面或作出新的业务判断时,再把观察交回模型。本节依次说明如何组织这样的程序(§4.1)、选择返回的信息(§4.2)、通过回执反馈执行状态(§4.3),以及用 Skill 引导 Agent 完成这个循环(§4.4)。
4.1 以程序为执行单元
Agent 可以通过 shell 的 heredoc(here-document),把多行 JavaScript 一次提交给 Tabbit CLI。程序可以包含连续动作、循环、断言和结果处理,执行到需要新判断的位置再把结果交还模型。下面以读取页面为例;17 表示已选定的任务页面编号,使用时替换为实际编号。
<<'JS' 将直到结束标记 JS 之间的内容作为标准输入传给 CLI;给标记加单引号,可以避免 shell 展开代码中的变量和命令替换。heredoc 负责传入多行代码,减少模型往返则依靠把已知步骤及其检查组织在同一次程序执行中。
在日期选择器测试中,事先的观察确定需要向后翻六个月。随后一次提交连续完成翻月、检查显示的月份、选择指定日期、把人数从一人增加到五人,然后预订。回执报告耗时 366 ms,评测器确认了日期和人数。下面的代码把原程序中固定的 100 ms 等待替换成了对确认信息的断言,展示如何把执行和核对放在同一段程序中;改写后的版本没有重新计时。
上述程序的执行顺序。
每个动作之后都回到模型,会重复输入固定指令和保留的对话历史。对于n次模型请求,总输入可以写成:
输入 = Σₖ₌₁ⁿ (S + Hₖ + Oₖ)
其中 S 是每次请求都会重复的固定指令,Hₖ 是保留的历史,Oₖ 是新的观察。压缩观察只减少 Oₖ;省掉一次模型轮次,则连同那一轮的 S 和 Hₖ 一起省掉。这里统计的是包含缓存在内的总输入,计费和延迟还取决于缓存命中情况。所以,已知的步骤连同对应的检查应放在同一段程序中执行,遇到需要新判断的情况再交还模型。
我们也在富表单任务中使用了这种方式:一段程序填写富文本编辑器、国家、出生日期、联系方式并上传文件,然后回读隐藏的 email 字段,返回 form.checkValidity(): true; 下一段程序提交表单并读取成功提示。
六次尝试均被判定为成功。在这几次运行中,Tabbit 的输入少65.3%,累计耗时少49.3%。更大范围任务上的完整对比见 §6。
4.2 数据处理与结果选择
一道字母方格题读取了3,864,805 个字符的词典。提交的程序在方格上枚举相邻且不重复使用格子的路径,检查每个候选词是否在词典中,再按长度和字母顺序排序。返回值是四个八字母单词、它们的路径和词典来源,压缩后的 JSON 只有319 字节:
Agent 选择了按字母顺序排在最前的最长候选词 briniest。返回路径后可以核对答案,词典本身则留在执行环境中,不必进入模型上下文。
词典在页面内完成搜索,只有压缩后的结果经运行时返回给模型。
另一次内部对比提高了内联结果的上限,资源读取次数从92 次降到 14 次,总输入却只下降了 1.5%。一次多返回一些原始内容,可以减少读取次数;先计算再返回,可以省去不需要进入上下文的中间数据。
4.3 回执:执行结果与页面变化
回执(receipt)是运行时为每次程序提交保存的结构化记录。CLI 以 JSON 返回执行摘要,也提供命令查询已保存的回执。程序的返回值只是其中一部分;回执还记录执行状态、耗时,以及执行期间观察到的页面变化和错误,让 Agent 判断应该继续等待、读取结果,还是检查失败原因。
运行时收到程序后,先按请求 ID 登记回执,再排队执行。状态从queued(排队)或running(运行中)更新为succeeded(成功)、failed(失败)或interrupted(中断)。如果 CLI 先返回,Agent 可以用原任务和请求 ID 查询,例如 §4.1 的请求可以通过tabbit-cli receipt --task inspect --request-id inspect-01取回结果。只要原任务中的回执仍可用,重复提交同一请求 ID 会返回已有回执,不会再次执行程序;修改后的程序需要使用新的请求 ID。回执的保留期限和重启后的处理见 §5。
下面按含义列出回执和 CLI 执行摘要中的主要信息。字段名为举例,不同命令的返回结构不同,部分字段只在相应状态或事件发生时出现:
status: succeeded 表示程序正常结束,不代表保存、下单等业务操作已经成功。mutationState: possible 表示不能排除副作用,并不意味着检测到了修改。例如,程序可以在点击「提交」后正常返回,而服务器仍在处理请求;是否完成仍需通过 expect 或业务记录核对。
在一次延迟提交的探测中,程序在网站的 POST 请求完成之前就已返回。随后出现的 HTTP 504 错误直到下一次回执才返回给 Agent:
如果在 p7 返回时就移除诊断监听,就会错过后来的失败。Tabbit 跨程序调用保留页面上的请求诊断,并在下一次回执中返回新发现的问题。两段程序都完成了各自的工作,而这次 POST 的业务结果仍需要另行检查。观察弹窗和导航时,也需要先安装监听,再触发动作,以免漏掉执行期间发生的事件。
4.4 Skill 设计
Skill 主要说明 Tabbit 的任务边界、返回结果和恢复方法,并用完整程序展示如何观察页面、执行操作和验证结果。截图示例还包含宿主加载图片的步骤。Skill 不再完整复述 Playwright 的 API 参考。
当前的主 Skill 有13,137 个字符,两份 Markdown 参考文档合计40,613 个字符,后者在任务需要时才读取:
我们比较过两个完整的 Skill 版本。篇幅更长、以文字说明为主的版本只有 4%的运行使用了并发;更短、把可运行示例放在显眼位置的版本,这一比例为63%。这促使我们更多地用程序展示预期的执行方式:并行提取只处理有限个相互独立的来源,保留成功和失败结果,并在 finally 中关闭临时页面;有依赖的修改按顺序执行,报告成功前检查应用结果。
在 75 对的内部对比中,缓存 token 占 Tabbit 输入的 91.9%,占 Codex Chrome Control 输入的 91.2%。我们保持常用指令稳定,以便复用缓存。
对于页面观察,Skill 说明具体的触发条件:遇到不熟悉的页面时读取相关内容,文档变化后重新检查,业务操作之后验证结果。已知步骤中的状态检查留在程序内部。
具体的恢复提示随运行结果返回:定位有歧义时列出匹配的控件,程序仍在运行时提示查询同一请求,修改结果不确定时提示核对应用状态。Skill 也要求 Agent 在程序中明确停止条件;需要新的判断时,返回当前状态,再决定下一步。
5. 运行时生命周期与故障恢复
CLI、Agent 宿主和浏览器的运行时间并不一致。CLI 退出时,页面操作可能仍在继续;Agent 没有收到回复时,操作也可能已经完成。运行时需要保存这些状态,供后续查询和恢复。
5.1 由浏览器托管的运行时
我们曾尝试复用 Codex 的浏览器客户端。客户端能够连上 Tabbit,但在当时接入的版本中,页面对象和变量依赖 Codex 宿主提供的持久 JavaScript 内核。换到其他 Coding Agent 后,这个内核并不存在;我们搭建的桥接层只能执行彼此独立的调用,无法保留跨调用的变量。
宿主的生命周期也是一个限制:有些 Agent 会在命令结束时终止其子进程。如果常驻执行器由 CLI 启动,任务就可能随命令退出而中断。对于要服务多个 Agent 的浏览器工具,持久状态不宜依赖某个宿主的专有内核或进程管理方式。
因此,Tabbit 由浏览器启动并托管运行时服务,让 CLI 只负责提交程序和查询结果。在任务有效且运行时未重启的前提下,页面对象、中间数据和执行进度可以跨调用复用;CLI 断开或 Agent 重启,也不必从头建立执行环境。浏览器本身持有用户的页面和登录态,由它同时管理任务状态,能够让不同 Agent 共用同一套执行与恢复机制。
每个任务运行在独立的 Worker 中,Worker 可以被终止而不关闭它的标签页:
运行时最多同时允许八个活动任务。表中最后一行是保留标签页的恢复路径;显式关闭浏览器会结束运行时。重新获取保留的标签页之后,程序需要根据当前页面状态重建变量和 Playwright 句柄。
5.2 为可恢复而设计:保留结果,避免重复执行
Agent 提交一段读取多页表格的程序后,CLI 可能等待超时,而浏览器里的程序仍在运行。此时重新提交会重复读取;查询原请求的回执,则能继续等待或取回已完成的结果。为此,Tabbit 把执行状态保存在运行时中,不依赖 CLI 连接持续存在。任务仍可用时复用原回执,回执丢失时则重新获取页面并核对当前状态。
恢复流程本身也可能丢失结果。有一次,finish 在回复前请求重启运行时,重启随即关闭了回复通道,调用方最终收到的是连接失败。我们把顺序改为先写完响应,再允许重启;如果调用方已经断开,也允许继续清理。这样可以保留本可送达的回复,也不会因等待已断开的连接而卡住恢复。
程序停止后,还需要检查已经完成了哪些操作。一次表格汇总在等待页面时超时,但「下一页」已经点击成功,页面停在第三页。Agent 检查当前位置后继续收集,再补回遗漏的第二页,最终得到正确总计。恢复时保留这部分进度,避免了从错误的起始页重新执行。
提交订单时需要更谨慎:假设点击后没有收到确认,服务器可能已经创建了订单,直接重试就可能重复下单。Tabbit 因此在支持的中断修改和传输丢失场景中返回 OUTCOME_UNKNOWN,不自动重放。Agent 应先查询回执,再核对订单记录;应用支持幂等键时复用原键,否则只有确认上次修改未生效且不会再生效,才重复提交。
6. 评测:分别衡量接口与 Agent 行为
前文的机制规定了工具应有的行为;内部对比要回答的是,Agent 是否借助这些机制,以更少的交互和模型输入完成了任务。
6.1 完整配对对比
主要的内部对比使用 25 道有固定答案的题目,每题重复三次:每种集成方式 75 次配对尝试,使用相同的模型和推理设置,没有用重试结果替换计分。
总输入低 47.2%,未缓存输入低 51.0%。配对输入比值的几何均值为 0.641,按任务聚类的 95% 区间为[0.503, 0.819]。前者描述总消耗,后者描述典型的配对变化,两者都支持「输入消耗更低」这一结论。
工具调用次数只少6.7%,汇总后每次调用的输入却少43.5%:约 41.2K 对72.9K token。调用次数的汇总比值为 0.933,配对几何均值为 0.917,其区间跨过 1(见附录)。换言之,输入的降幅远大于调用次数的降幅。工具调用不等于模型请求,因此这种拆分无法把差异分别归因到 Skill 大小、保留的历史或观察内容上。
按任务多数票计的正确率为 17/25 对 15/25,差异不显著。配对耗时的区间也跨过 1,输出消耗则更高。整体比较中,Tabbit 的优势主要是输入消耗较低;§4.1 中富表单的耗时改善只适用于那个具体流程。
6.2 衡量设计想要改变的行为
集成对比衡量的是初始化、指令、API、执行方式以及模型所写程序的综合效果。要分离单个设计选择的作用,需要受控测试;要了解 Agent 怎样使用这些机制,需要分析执行轨迹。
前文通过移动按钮、重复标签、遮挡按钮和延迟保存,检查了定位和完成判定。恢复机制也需要单独测试:在修改可能已经生效后丢弃响应,检查 Agent 能否找回结果,以及是否重复提交。附录列出了可用于后续集成验证的测试条件。
比较操作层时,应保持初始化条件一致。在这组配对对比中,第一次提交 goto 或 navigate 调用之前的工具调用次数,中位数在 Tabbit 中为 2,在 Codex Chrome Control 中为 3。这个数字包括初始化和探索,也包括浏览器操作;75 条 Tabbit 轨迹和 73 条 Chrome 轨迹包含相应的导航调用,另有两条 Chrome 轨迹未计入这一统计。
7. 经验总结
我们最终保留了 Playwright 的定位、等待和输入行为,把浏览器侧的工作集中在页面访问和运行时管理上。程序负责连续执行已知步骤并检查结果,出现新的业务判断时再交回 Agent。这个分工减少了自研操作语义的工作,也让批量执行不必省略验证。
中断后的处理同样影响任务能否正确完成:没有收到回复时,要查明程序是否仍在运行、哪些修改已经生效,再决定如何继续。性能评测因此需要同时记录输入消耗、错误操作、虚假成功和恢复结果。
下一步,我们希望将常用网站上已经验证过的操作说明、程序和结果检查方法保存为 Playbook,在相关任务中按需加载,减少重复查找入口和编写定位器的工作。WebMCP 由网站直接声明查询订单、添加商品等结构化工具,可以与 Playbook 配合使用。已有页面的操作经验和网站提供的业务接口都需要持续验证,并随网站变化维护。
附录:内部测试与证据
本文的证据分为三类:完整的配对集成对比、针对定位、遮挡和保存状态的受控测试,以及由实现历史佐证的执行轨迹。
配对对比。 完整的测试组包含 25 道题 × 三次重复 × 三种方案,共 225 次尝试。各方案按事先确定的种子和循环顺序依次运行,单次上限 1,800 秒。答案经匿名化、乱序后,由两个模型家族各给出两次评判。由于跨家族评分只覆盖了 225 次中的 222 次,事先设定的正确率判定门槛没有通过,正确率数字仅作描述用途。输入包含缓存部分,未缓存输入为输入减去缓存部分。工具调用指 Agent 发出的工具调用,不是 CDP 消息,也不一定对应一次模型轮次。耗时为单次尝试的实际耗时。
受控测试方法。移动按钮、重复标签、遮挡按钮和延迟保存四种场景各使用 48 对测试页面,每组都从新文档开始,先运行对照组,结果通过直接回读计数器和状态获得。按钮位置与文档变化的测试平均分为四种情况:文档不变、原位替换、移动、替换加移动。标签匹配的测试包含 16 个唯一匹配和 32 个重复匹配的例子,文档顺序保持平衡,限定范围后的恢复单独记录。遮挡测试包含 24 个无遮挡和 24 个持续遮挡的控件,点击超时为 500 ms。保存状态的测试包含 24 次延迟成功和 24 次延迟失败,延迟为 120、180 或 240 ms,断言期限为 700 ms。测试页面模拟的是内存中的应用状态,而不是持久化的服务器提交。
执行与实现证据。日期选择器的结果和表格恢复使用确定性评测器判定;富表单的结果依据保存下来的成功证据和评判。历史提交记录了实现上的变化,回归测试覆盖具体的契约。
有序恢复审计。 33 条轨迹包含 44 次严格模式错误输出,去掉 6 次重复的回执输出后,剩下 38 次不同的失败操作。每次失败按随后相关的程序分类,中间的检查步骤也计入;错误信息中 Playwright 给出的建议代码不计入。这些轨迹跨越多个内部版本和任务集。
其他测量。 截图示例是对 React 首页的一次新截图,视口为 951 × 893 CSS 像素,生成 1902 × 1786 的 PNG。早先对原始文本的扫描发现,1,067 条 Tabbit 轨迹中有 550 条出现了expect,包括指令和工具输出中的出现,不代表实际执行的断言次数。这批语料与配对对比不同。这些测量没有合并成一个总体的性能结果。
后续集成的测试建议。 下表列出需要检查的行为及测试条件;前文已经报告定位和保存结果检查,其余项目可用于后续验证。
如果你还没有尝试过 Tabbit CLI,可在 Codex、WorkBuddy 等 Agent 工具中输入 /tabbit ,直接驱动 Tabbit 接管真实浏览器操作。
未安装 Tabbit 浏览器的朋友,可以访问官网 www.tabbit.com或点击「阅读原文」免费下载电脑端试用。
体验手机端 Tabbit,可在各应用商店搜索「Tabbit」下载。登录同一账号,你的书签、标签页以及其他浏览数据都可在云端同步。
来源:美团技术团队 · 微信公众号 · mp.weixin.qq.com