一段测试脚本,两种写法
传统 Playwright 脚本要点击一个按钮,得先给它找个稳定的 selector:
await page.locator('[data-testid="submit-btn"]').click();这行代码的前提是页面里真的有一个稳定的 data-testid。如果这个按钮是纯图标、放在 canvas 里绘制、或者藏在跨域 iframe 里,这条路直接走不通——没有语义化标注,selector 就无从下手。
字节跳动 web-infra 团队开源的 Midscene 给出的答案是换一条路:不找 selector,直接把整张截图交给视觉模型,用自然语言描述要做什么:
await agent.aiTap('提交按钮');这正好对应上一篇(02)建立的词汇表里的路线一:像素视觉定位——不依赖任何应用暴露的结构化接口,纯粹靠"看"。这篇要拆解的,是这条路线在一个真实落地的测试框架里具体长什么样。
四个核心 API,对应四种不同粒度的操作
Midscene 不是一个单一的"用自然语言操作页面"黑盒,而是把不同粒度的需求拆成了四个职责清晰的 API:
aiTap / aiInput —— 原子操作:精确控制单步"点击"或"输入"
↓
aiAct —— 自主流程:把一整段自然语言描述的多步任务交给 Agent
自己规划执行(比如"搜索并按价格从低到高排序")
↓
aiQuery —— 结构化提取:从页面里"看"出结构化数据
(比如提取当前购物车里所有商品的名称和价格)
↓
aiAssert —— 视觉断言:验证"用户真正看到的效果"是否符合预期
(比如判断某个按钮是否高亮、布局是否错位)这个拆分背后的设计考量是:测试脚本不应该把所有操作都交给 Agent 自主决策。 原子操作(aiTap/aiInput)保留了传统脚本"每一步做什么都由人明确指定"的确定性和可读性;自主流程(aiAct)则把探索性的、步骤数不固定的任务交给模型自己拆解。这跟纯 Computer Use 路线(一个模型端到端决定所有动作)有本质区别——Midscene 让测试工程师自己选择在哪一层引入不确定性,而不是整个测试流程都赌在模型的规划能力上。
aiAssert 值得单独说一句:它要解决的正是系列第 01 篇提到的 oracle 问题——很多视觉层面的正确性没法用像素 diff 或 DOM 属性判断,比如"这个按钮看起来是不是处于禁用状态"。aiAssert 把这类判断交给视觉模型做语义判断,而不是写死一个 CSS 类名检查。这个思路会在系列后面第 12 篇(视觉与语义回归测试)里进一步展开。
核心实现一:三种模型角色分工,而不是一个模型包打天下
上一节说 aiAct 把"多步任务交给 Agent 自己规划执行",但没说清楚的是:这个"规划"具体怎么落地成模型调用。Midscene 的做法是把一次自动化任务拆成三种可以分别指定模型的角色(Model Strategy):
Default 模型 —— 负责元素定位(Locate),以及没有被 Planning/Insight
接管的其余工作负载,是默认情况下扛住大部分自动化需求
的基础模型
Planning 模型 —— 增强复杂目标、多步任务、带分支场景下的规划能力
Insight 模型 —— 增强数据提取、断言判断、页面理解能力关键设计是这三个角色可以分别绑定不同的底层模型:官方文档的原话是"用户可以在 Default 模型基础上追加 Planning 和 Insight 模型",让每个模型在自己更擅长的角色上发挥优势——比如给"规划"配一个推理能力更强的模型,给"定位"配一个视觉定位更精准的模型,而不是强迫一个通用模型同时扛住这两种完全不同的能力需求。
但这不是没有代价的。官方文档明确提醒:"这种协作扩展了 Midscene 处理复杂任务的能力,但也会增加任务延迟和 token 消耗"。因此推荐的用法是渐进式引入:先用 Default 模型跑通,只有在遇到明确的能力瓶颈时才针对性引入 Planning 或 Insight 模型——不是默认三个模型一起上。
这个角色拆分本身就是对第 02 篇"定位精度不是唯一变量"结论的一次工程化回应:把"规划"和"定位"两种能力需求显式拆开、允许独立选型,比让一个模型同时兼顾两种能力更容易针对瓶颈精确优化,但要为这份灵活性多付出延迟和调用成本。
核心实现二:aiAct 的执行循环——什么时候重新规划,什么时候拆开定位
aiAct 不是"调用一次模型拿到完整步骤列表就执行到底"。官方文档的描述是:它会持续用 AI 从最新的界面状态规划下一步动作,直到目标完成——也就是每执行完一步,就重新观察一次当前界面再决定下一步,而不是死守着任务开始时生成的那份计划往下走。这样设计的好处是:如果中途弹出一个权限对话框或者加载延迟改变了界面,规划会自然地基于最新状态调整,而不需要额外写"如果出现意外弹窗怎么处理"这类分支逻辑。
这个循环不是无限重规划——有一个 replanningCycleLimit(重规划轮次上限)参数兜底,默认值随模型类型变化(标准模型 20 轮、UI-TARS 40 轮、AutoGLM 100 轮),超出上限或者 prompt 里嵌入的断言失败时,aiAct 会直接抛出错误,而不是继续无休止地重试。这跟第 03 篇讲的"保守失败"思路是同一种设计哲学:与其让一个跑不出结果的任务悄无声息地空转,不如让它尽快报错交给人判断。
默认情况下,aiAct 在同一次模型调用里**同时完成"规划下一步"和"定位目标元素"**这两件事。但复杂任务有时需要把这两步拆开成独立的模型调用——这就是 deepThink 参数的作用:开启后,规划和定位会被拆成两次独立调用,代价是更多的模型调用次数和延迟,换来的是复杂任务下更好的稳定性。
这里有一个容易混淆但值得说清楚的历史细节:deepThink 这个参数名在不同 API 里曾经指代过两件不同的事——在 aiAct() 里它一直是"规划模式"的开关;但在 aiTap 这类单步方法里,旧版本的 deepThink 指的是"增强元素定位"。为了消除这个歧义,Midscene 把"增强定位"的概念重新命名成了独立的 deepLocate 参数,deepThink 统一收敛为"规划模式"的开关——而在 aiAct() 内部,deepThink(是否拆开规划)和 deepLocate(是否增强定位精度)这两个参数是同时存在、各管一段的,不是互斥关系。
跟 Playwright 的关系:增强,不是替代
Midscene 没有重新发明一套浏览器控制协议,而是通过 PlaywrightAgent 包装现有的 Playwright page 对象——测试代码里 Playwright 原本的能力(导航、等待、传统 selector 断言)全部保留,Midscene 的四个 AI API 只是叠加在上面的一层。同样的接入方式也支持 Puppeteer。
这个设计的实际意义是:迁移成本是渐进式的,不是推倒重写。 一个已有的 Playwright 测试套件可以先在最脆弱、最容易因为 UI 改版而失效的那几个断言点换成 aiAssert,其余部分维持不变——不需要为了引入视觉定位把整套框架换掉。
更进一步,Midscene 把这套 Agent API 做成了跨平台统一接口:"同一套 API 覆盖 Web、Android、iOS、HarmonyOS 和桌面应用"。这意味着一个测试工程师如果已经掌握了 aiTap/aiQuery/aiAssert 这套心智模型,切换到移动端或桌面端自动化时不需要重新学一套 API——只是底层截图采集和事件注入的实现换了,调用方式不变。(移动端具体项目会在后续 05-09 篇详细拆解,那几个项目各自有更专门的移动端优化。)
缓存机制:视觉方案最大的痛点是怎么被缓解的
上一篇讲过路线一(视觉定位)的代价:每次定位都要过一次模型推理,比读结构化数据慢、贵。 如果测试套件里有几百个用例、每个用例几十步操作,每一步都要真实调用一次视觉模型,这个成本在 CI 里跑起来是不可接受的。
Midscene 的答案是分别缓存两种不同性质的产出,而不是笼统地说"缓存定位结果":
规划缓存(针对 ai / aiAct)
缓存键 = 用户写的自然语言指令原文
缓存值 = 模型返回的执行计划(一串具体动作步骤)
定位缓存(针对 aiLocate / aiTap 等单步定位操作)
缓存键 = 定位用的 prompt
缓存值 = 匹配到的元素 XPath查询类操作(aiQuery/aiBoolean/aiAssert)被显式排除在缓存范围之外——因为这类调用的目的就是"读取当前页面的真实状态",缓存一个过去的判断结果在语义上是错的。
定位缓存不是"缓存了 XPath 就直接信任它"。每次执行都会先验证这个 XPath 在当前页面上是否仍然有效,判定失效的条件是"相同 XPath 位置的新元素文本内容和缓存时不一样"或者"页面 DOM 结构相对缓存时发生了变化"——这是一个刻意收紧的匹配策略,宁可多失效几次触发重新定位,也不要在页面已经变了的情况下,错误地点到一个视觉上长得像、但语义已经不对的元素。规划缓存这一侧也有类似的保守设计:如果一份缓存的 aiAct 计划在运行时执行失败,Midscene 会退回到正常的 AI 规划流程重新走一遍,并清掉这条失效的缓存记录;但这次退回后重新生成的计划不会被写回原始 prompt 的缓存里,因为它未必能代表"从任务最初状态开始"的完整正确计划。
还有一类边界条件值得单独指出:定位缓存在结构不稳定的渲染场景里完全用不上——Canvas 内部绘制的图形没有对应的 DOM 节点,天然没有 XPath 可缓存;跨域 iframe 受浏览器安全策略限制访问不到内部结构;Closed Shadow DOM 从外部完全不可访问;WebGL/动态 SVG 的 DOM 结构本身就不稳定,缓存了也大概率下次就失效。这恰好呼应了本文开头的论点:canvas/iframe/自定义渲染正是纯视觉方案的核心适用场景,但也正是缓存机制天然失效、每次都要真实调用模型的场景——缓存能省下的成本和视觉方案的核心优势场景之间存在结构性冲突,这不是 Midscene 的设计缺陷,而是这类界面本身缺乏结构化信息导致的必然结果。
这个设计本质上是把"视觉定位"从"每次都要用"降级成"兜底手段":正常情况下测试跑的是缓存的、确定性的操作序列,跟传统 selector 脚本的速度和成本没有本质区别;只有 UI 真正发生变化,导致缓存失效时,才退回到视觉模型重新定位——但在 canvas/iframe 等结构缺失场景里,这个"兜底手段"实际上就是唯一手段。某种意义上,这已经带有第 10 篇要讲的"自愈定位"的味道——区别在于 10 篇讨论的自愈逻辑更聚焦在"定位失败后怎么恢复",这里的缓存机制解决的是"大部分时候根本不需要触发定位"的成本问题。
真实的 Benchmark 数字
Midscene 官网公开了三项 benchmark 成绩,可以用来跟系列第 02 篇讲的 ScreenSpot/OSWorld 数字做对照:
AndroidWorld Benchmark Pass@1 93.1% Pass@3 97.4%
MobileWorld Benchmark Pass@1 78.6%(92/117 通过)
AppControlBench Benchmark Pass@1 96.7%(58 项通过)这几个数字都是移动端基准(AndroidWorld 会在第 05 篇详细介绍),但放在这里有一个值得注意的对比:第 02 篇提到 OSWorld 上早期通用基线模型只有 12.24% 的任务成功率,而 Midscene 在 AndroidWorld 上做到 93.1% 的 Pass@1。这不是说 Midscene 的底层模型比 OSWorld 测的模型强多少,而是任务类型和评测范式不同——AndroidWorld/AppControlBench 这类专门为移动 GUI Agent 设计的基准,任务边界更明确,配合专门的规划+重试机制(Pass@3 比 Pass@1 高出 4 个百分点,说明允许重试确实能弥补一部分单次定位失败),能做到远高于 OSWorld 这种开放式桌面任务的完成率。这再次印证第 02 篇的结论:端到端任务成功率不只取决于定位精度,规划、重试、任务边界设计同样关键。
跟 Stagehand 的路线对比
Midscene 不是这个方向唯一的开源方案。Stagehand(Browserbase 开源)是另一个值得对照的项目,它同样在 Playwright 之上叠加了自然语言 API(act/extract/observe),但技术路线更偏向系列第 02 篇讲的路线二:DOM/无障碍树语义理解——优先用浏览器暴露的结构化信息做定位和操作,视觉能力是补充而非核心。
核心定位方式 适用场景 代价
────────────────────────────────────────────────────────────────
Midscene 纯视觉(截图+模型) canvas/iframe/纯图标 每次定位需要模型推理
等无语义标注场景 (靠缓存缓解)
Stagehand DOM/无障碍树优先 常规网页,结构信息可靠 结构缺失时的降级能力
视觉为辅 的场景 不如纯视觉方案这组对比呼应了第 02 篇的核心结论:两条路线不是互斥的技术优劣,而是针对不同场景的取舍。 如果测试目标是结构信息完整、语义标注规范的常规 Web 应用,DOM 优先的方案(如 Stagehand)通常更快更省;如果测试目标本身就包含大量 canvas 绘制、自定义渲染控件、或者需要跨越 Web/移动/桌面统一维护一套测试逻辑,纯视觉方案(如 Midscene)的通用性优势会更明显。
总结
- Midscene 走的是纯视觉定位路线——不依赖 selector 或语义标注,直接把截图交给视觉模型,因此能覆盖 canvas、纯图标、跨域 iframe 这类传统方案定位不到的场景
- 四个核心 API(
aiTap/aiInput原子操作、aiAct自主流程、aiQuery结构化提取、aiAssert视觉断言)把不同粒度的不确定性交给测试工程师自己选择引入的层级,不是整个流程都赌在模型规划上 - Default/Planning/Insight 三种模型角色可以分别绑定不同底层模型,让"规划"和"定位"两种能力需求可以针对性选型优化,但会增加延迟和 token 成本,官方建议按需渐进引入而非默认全开
aiAct的执行循环是每步都基于最新界面重新规划,而不是死守任务开始时生成的计划;deepThink参数决定"规划"和"定位"是合并在一次模型调用里还是拆成两次独立调用,用更多调用换取复杂任务下的稳定性- 缓存机制分别缓存规划结果(键=指令原文)和定位结果(键=定位 prompt,值=XPath),且定位缓存有严格的失效判定(文本变化或 DOM 结构变化即失效);canvas/iframe 等结构缺失场景里定位缓存天然用不上,这正是纯视觉方案的核心适用场景与缓存机制的边界重叠区
- 跟 Stagehand(DOM/无障碍树优先)对比,两条路线不是优劣关系,而是分别匹配"结构信息不可靠"和"结构信息完整可靠"两类不同场景
欢迎访问 PrimeSkills —— 一个精心策划的 AI Agent 与技能市场,所有内容均经过真实企业级工作流验证。没有噱头,只有真正有效的东西。
更多实用知识和有趣产品,欢迎访问我的个人主页