☰
大模型语义理解与Puppeteer协同:Midscene驱动的UI自动化架构实践
2026/10/20 13:55:08 网站建设 项目流程

1. 从一次改版引发的重构说起:传统UI脚本的寿命焦虑

先说个真实场景。三四年前我在某团队维护一条核心链路的UI回归用例,用Puppeteer写得挺开心——page.click、page.type、page.waitForSelector一气呵成。结果某次产品改版,把按钮从#submit-btn变成了.header__submit--primary,类名从驼峰换成了BEM。那一行waitForSelector('#submit-btn')当场失效,连带后面十来个依赖它的步骤全挂。最气人的是,排查了一上午,发现功能明明没变,只是前端工程师换了个CSS命名风格。

这种脆弱的根源不只是选择器本身,而是脚本把“用户的操作意图”和“页面的实现细节”死死绑在了一起。我想说“点击提交按钮”,但Puppeteer只听得懂“点击另一个节点”。你必须在DOM里精确描述目标,还得祈祷它下次改版别挪窝。

所以当我看到Midscene这类基于大模型语义理解的自动化方案时,第一反应不是“又一个轮子”,而是“这次换了个拧螺丝的方式”。Midscene的核心不是要替代Puppeteer,而是把“意图层”从“DOM层”里剥出来,让Puppeteer继续干它擅长的浏览器控制,让大模型去理解“用户到底想干嘛”。

这篇文章不是官方文档的复述,是我在实际项目里把Midscene和Puppeteer搭在一起跑通之后,对两者协同机制的复盘。我会从架构分层、职责边界、消息链路、工程方案、踩坑经验这几个角度来拆,尽量让你看完之后脑子里能形成一张完整的协同图。

适合谁看?已经在用Puppeteer、Playwright,被选择器维护折磨过的人;刚接触AI自动化,想搞明白“大模型怎么和真实浏览器对话”的人;以及准备在团队里引入语义化自动化,但不知道该怎么设计代码结构的人。

2. 按“输入流向”拆解Midscene的层次化架构

想理解Midscene和Puppeteer怎么协同,不能只看API拼凑,得先把Midscene的架构拆成一层一层看。我最开始犯的错就是拿Midscene当Puppeteer的平替,看文档时候满脑子“这东西怎么click”,结果越看越乱。后来意识到,这是个完全不同的抽象层级。

2.1 四层分工:意图层、编排层、控制层、执行层

我是按“输入流向”来理解的。一条自然语言指令从你嘴里说出来,到真正在浏览器里产生鼠标键盘事件,中间经历了四个角色完全不同的层次:

第一层叫意图层,也是Midscene特有的层。目标用户给了“打开首页,登录,点击工作台”,这一层要把它解析成三步明确的任务序列,每步包含“动作类型”和“动作对象”。比如“点击工作台”会被拆成{action: "click", target: "工作台入口"}。这里用的是大模型的语义理解能力,不涉及任何DOM。

第二层叫编排层,负责把意图层的任务序列转成可执行的动作计划。这一层会考虑页面状态、前置条件、动作依赖。比如“登录”要拆成“输入用户名、输入密码、点击登录按钮”三个原子操作,其中“输入用户名”依赖“用户名输入框已出现”这个前置条件。编排层做任务规划。

第三层叫控制层,这一层就是Puppeteer的地盘了。它负责建立浏览器连接、管理页面生命周期、发送CDP(Chrome DevTools Protocol)指令、处理网络请求等。Midscene在这里的回调接口拿到的是一个标准的Puppeteer Browser/Page实例。

第四层叫执行层,负责真正去碰DOM。选择器定位、事件触发、输入框赋值、等待逻辑,都发生在这一层。但注意,这个“碰DOM”的动作是Midscene生成的——Midscene负责“找谁”,Puppeteer负责“怎么碰”。

这个分层的意义在于:你可以在意图层享受大模型的语义理解,同时在执行层保留Puppeteer的底层控制力。二者不是竞争关系,是上下游关系。

2.2 一条指令在Midscene内部经历了什么

举个完整流程的例子。假设我对Midscene下指令:“打开示例网站,在搜索框输入Midscene,点击第一个搜索结果。”

第一步,意图层拿到这段自然语言,不做任何页面操作,纯靠大模型拆解成有序的动作清单:

[ { "action": "goto", "target": "https://example.com" }, { "action": "type", "target": "搜索框", "text": "Midscene" }, { "action": "click", "target": "第一个搜索结果" } ]

第二步,编排层拿到动作清单,逐个检查前置条件。比如执行“type”之前,得保证页面已经加载完、搜索框已经渲染出来。编排层会调用控制层的接口去“看一下”当前页面状态——注意,这里的“看”很关键,有两种路径,后面细说。

第三步,传给执行层。如果是type动作,执行层先定位到目标元素,然后调用Puppeteer的page.type()方法。如果是click动作,同样先定位,再调page.click()。

这个链条的关键点在于:Puppeteer全程不参与“元素该不该点”的决策,只负责“怎么点”的落实。而Midscene全程不直接操作浏览器,所有浏览器通信都通过Puppeteer的Page实例完成。

这种上下游关系决定了二者协同的本质:Puppeteer是Midscene的手和脚,Midscene是Puppeteer的大脑。手再灵活也不知道该往哪走,脑子再聪明也得通过手来触摸世界。

3. Puppeteer在协同中的“手感”边界:谁碰DOM、谁碰协议

一个常见误区是以为Midscene完全替代了Puppeteer,不需要再学习后者。真不是这样。要衡量两者协同得好不好,核心是分清职责边界——哪些事情该交给Midscene,哪些事情该直接拿Puppeteer的实例来,哪些事情必须绕过调度层自己写。

3.1 各管一段:浏览器管家与AI翻译官

我在代码里习惯把这条边界画成这么一张分工表:

事务类型交给谁原因
页面跳转、标签页管理Puppeteer生命周期操作的稳定性远高于模型推理
元素定位(语义层面)Midscene大模型擅长处理“按钮在哪个区块”的模糊描述
表单填写(纯文本)Puppeteertype()性能极佳,不需要模型参与
表单填写(有语义歧义)Midscene例如“填写收货人信息”,模型能推断出是姓名+电话
页面状态断言Puppeteer选Selector + Midscene选语义精确断言用前者,模糊校验用后者
网络监听、接口MockPuppeteer这属于协议层能力,AI帮不上什么忙
任务规划(先做什么后做什么)Midscene大模型的前置依赖判断能力

这个表看起来简单,但实际落地时我吃过亏。有一版方案我让Midscene做了所有事——包括页面跳转、等待网络空闲、甚至鼠标滚轮操作,结果发现两个问题:一是大模型推理每步要耗时几百毫秒到一秒,一个简单的五步操作链就要多等两三秒;二是模型偶尔会跳步、重复执行,导致链路不稳定。后来我把纯机械的操作全部下沉给Puppeteer,只把“需要理解才能决策”的部分保留给Midscene,整个链路的耗时和稳定性都改善了不少。

3.2 全权代理之外的混合模式

Midscene的常见用法是给一个整任务,然后它全权代理执行完。但真正的工程场景往往不需要这种“全权代理”,而是混合模式——有些步骤用Puppeteer精确控制,有些步骤用Midscene语义理解。我建议写脚本时这样组织:

import puppeteer from "puppeteer"; // 示意代码,实际API以对应版本为准 import { createAgent } from "midscene"; (async () => { const browser = await puppeteer.launch({ headless: false }); const page = await browser.newPage(); // 1. 协议层精细操作,直接走Puppeteer await page.setViewport({ width: 1280, height: 720 }); await page.goto("https://example.com", { waitUntil: "networkidle2" }); // 2. 语义层让Midscene理解“用户名输入框”到底是哪个 const agent = await createAgent({ page }); await agent.aiAction("在用户名输入框填入 test_user"); // 3. 纯机械步骤,回到Puppeteer await page.type("#password", "test_pass"); // 4. 复杂的任务规划,交给Midscene await agent.aiAction( "点击登录按钮,然后等待页面出现欢迎提示,如果出现弹窗则关闭它" ); await browser.close(); })();

这种混写模式在可维护性上有个明显好处:选择器的变化只影响第3步那一行,流程意图的变化只影响第4步那一句。改前端类和改产品逻辑被隔离在了不同区域。

4. 协同链路解剖:自然语言如何变成一次真实的点击

理解Midscene和Puppeteer的协同,最直观的方式是看一条链路上每个环节的输入输出。下面我用“点击目标按钮”这个最常见操作,把整条链路走一遍。

4.1 定位阶段的“看”:DOM快照与视觉快照

执行层要点击目标元素,第一步是“找到它”。这里Midscene定位目标元素有两种主流策略:

第一种是基于DOM结构定位。Midscene会通过Puppeteer拿到当前页面的可交互元素快照——大概长这个样子:

<button id="submit-btn" class="submit--primary">确认提交</button> <a href="/account/order">查看订单</a> <input placeholder="请输入手机号" />

然后把这份简化DOM(去掉了大量无关样式节点)丢给大模型,让它从里面找“确认提交”对应的是哪个节点。这种方式的优点是快、准、token消耗低;缺点是模型只能看到结构化文本,不能理解视觉布局——如果页面按钮文字没有明确语义,比如图标按钮,DOM方案就抓瞎。

第二种是基于视觉截图定位。Midscene调用Puppeteer的page.screenshot()拿到页面截图,把截图丢给多模态大模型,让模型在图上标注出目标元素的坐标。这种方式能理解视觉布局,标注完坐标之后由Puppeteer把坐标转成一次真实点击。缺点是token消耗高、延迟大。

我实际测试下来,大多数表单场景用DOM快照就够,图标按钮、图形化界面、Canvas场景才需要视觉定位。Midscene的实现通常会混合两种方式——第一轮先跑DOM快照,没命中或置信度不够再补视觉,类似“先文字理解、再视觉兜底”。

4.2 执行阶段的“点”:从意图到CDP指令

定位完成,Midscene拿到了目标元素。接下来它不自己“假装”发射点击事件,而是把目标信息交给Puppeteer执行真实验证过的鼠标事件。

Puppeteer的page.click()底层走的是Chrome DevTools Protocol的Input.dispatchMouseEvent——真实地派发鼠标按下、移动、松开三个事件,和人工操作的行为几乎一致。Midscene如果用的是DOM定位,那它会取到元素boundingBox,计算中心点坐标,然后把坐标传给Puppeteer:

// 示意代码:语义定位结果转成真实点击 const box = await elementHandle.boundingBox(); const x = box.x + box.width / 2; const y = box.y + box.height / 2; await page.mouse.click(x, y);

如果是视觉定位,则直接把模型返回的坐标交给page.mouse.click()。这条链路上Puppeteer的角色非常纯粹:给它坐标,它负责动手。

4.3 决策阶段的“等”:等待策略的真实分工

自动化脚本里最愁人的环节其实是等待。waitForSelector写短了容易网络慢导致失败,写长了又拖慢整个用例。

Midscene对等待的处理方式比较有意思——它把“是否准备好了”也变成了一个语义判断问题。比如执行点击之前,Midscene先调用Puppeteer的waitForSelector做基础轮询,等目标出现;如果目标迟迟不出现,Midscene会先看页面是不是弹了异常弹窗,判断是不是有东西挡住了点击,再决定是继续等还是先清理弹窗再点。

这个“先等等再判断”的节奏,比固定轮询聪明,但也带来了新的问题——大模型每次“判断”都要走一次推理,耗时明显偏长。我的经验是:能用Puppeteer的显式等待解决,就不要让Midscene反复判断。比如登录后跳转回首页这种确定性强的场景,直接await page.waitForSelector('.home-page'),比让模型猜“首页内容出现没有”快得多。

5. 工程落地里两类典型搭配方案

理论说完,聊点能直接抄的。我在实际项目里把Midscene和Puppeteer的搭配归纳成两种典型方案,不同项目选不同方案,别混着用。

5.1 方案A:Midscene主导,Puppeteer处理边界能力

适合“流程长、步骤复杂、页面变化频繁”的业务线。比如电商下单全流程、后台配置流程、跨系统数据核对。这类场景如果全用Puppeteer写选择器,每次改版都要返工;让Midscene主导流程推进,Puppeteer只在关键节点接管。

具体落地时,我习惯给Midscene一个相对明确的动作序列,而不是一个宏大任务。原因是大模型的长期规划能力还不算强,指令太宏观容易跑偏。我会把任务拆成多个agent.aiAction()的调用,每个调用只做一件事,中间夹着关键的Puppeteer校验:

// 示意代码:任务拆解风格 await agent.aiAction("输入用户名和密码,点击登录"); // 关键节点用Puppeteer做硬性校验 await page.waitForSelector(".main-dashboard", { timeout: 10000 }); await agent.aiAction("点击左侧菜单中的‘订单管理’"); await agent.aiAction("在筛选区域设置时间为最近7天,点击查询");

这种写法的好处是,每个aiAction都很短,单步推理的准确率远高于一次性做完十步。坏处是代码里aiAction调用多,看得有点啰嗦。但比起选择器修来修去,这个代价完全值得。

5.2 方案B:Puppeteer主导,Midscene负责易变部分

适合“稳定流程为主、少量环节频繁调整”的场景。比如数据爬取类任务——流程结构相对固定(列表翻页->进详情->抓字段),但页面细节经常变(字段位置调整、按钮文案变化)。

这种方案里Puppeteer是主心骨,负责全部流程;只有“找元素”这个环节用到Midscene。我会封装一个通用的语义定位函数:

// 示意代码:封装语义定位,返回目标元素 async function locate(page: any, description: string) { const agent = await createAgent({ page }); const result = await agent.aiLocate(description); return result.elementHandle; } // 使用:其他逻辑全是Puppeteer,只有定位交给Midscene const nextBtn = await locate(page, "列表底部的下一页按钮"); await nextBtn.click();

这种方案最大的收益是把变量的影响范围控制在了最小区域。列表翻页逻辑不会变,变的只是“下一页按钮长什么样”,那我只要改封装内部的描述词,不用动主流程代码。

5.3 方案选择的判断标准

我的经验是一句话:看你的业务是“步骤易变”还是“元素易变”。步骤易变(流程经常增删环节)选方案A;元素易变(流程固定但界面常改)选方案B。两个都变量很大,就先考虑重构测试策略,别急着上自动化——自动化工具解决不了已经失控的流程。

6. 实际使用中的几个坑,以及我最推荐的协作姿势

协同方案看着清晰,真正跑起来还是有不少坑。我把踩过的几个记录在这里,按“受伤程度”排序,最后一个最疼。

坑一:大模型单步推理延迟不可控。我用的时候最直观的感受是,每步语义操作在100ms到2s之间波动,高峰时段更明显。这在CI里很难接受——比如一个20步的核心链路,跑一次要额外多等20-30秒。解法是上面提到的:能用Puppeteer硬编码的绝不上模型。我甚至在团队里立了条规矩,“一个用例里aiAction不超过5个,超出就先怀疑设计有问题”。

坑二:DOM快照裁剪策略影响定位准确率。Midscene拿DOM快照时会做节点裁剪,只保留可交互元素,但这有个隐患:如果业务文案恰好不在裁剪的节点上,模型就找不到目标。比如一个按钮只有个图标,文案在旁边的<span>里,DOM快照可能只截了<i>标签。我遇到这个问题的解法是给Midscene提供更完整的上下文,或者用视觉定位兜底。总之别默认DOM快照一定够用。

坑三:视觉定位的Token消耗比想象中大。截图一次大概是几万Token级别,多模态模型收费又高,跑重试时钱包会疼。我的建议是视觉定位只作为“保险丝”用:先在DOM层找,找到就用;找不到再截图走视觉。别一条链路从头到尾都是截图,那成本没法看。

坑四:Midscene在长任务中偶尔“自作主张”。有次让它执行“等待页面加载完成”,它居然自己判断页面“差不多了”就往下走了,结果后续操作全挂在半路。这件事实在让我长记性。后来我在每个关键节点后面都加了Puppeteer的硬校验——waitForSelector或者waitForFunction。把Midscene当“理解者”用,别把Midscene当“可靠性保障”用,这是协同架构里最重要的一条原则。

坑五:混用模式下的异常传播链路不清晰。当一条链路里既有Puppeteer的抛错,又有Midscene的语义判断失败时,定位问题会比较痛苦。我自己的做法是给链路做了统一的错误分类——Puppeteer的错误统一转成ElementNotFoundError、TimeoutError,Midscene的错误统一转成SemanticUnderstandingError、AgentExecutionError。这样报错出来,看错误类型就知道是哪一层出的问题。

6.1 我最推荐的协作姿势

踩了上面这些坑之后,我现在的协作姿势可以总结成四句话:

第一句:架构上“意图-编排-控制-执行”四层各管一段,不在代码里跨层打转。写脚本时我会先画一条“哪些步骤需要理解、哪些步骤是机械操作”的分工线,再开工。

第二句:流程控制靠Puppeteer,语义判断靠Midscene。循环、条件、超时、重试,全部用Puppeteer/Node原生逻辑实现;只有“我要找什么”“这一步是否达成目标”这类问题,才问Midscene。

第三句:任务尽量拆短,单步语义操作控制在2-3个动作之内。大模型是很好的语义解析器,但不是可靠的状态机。

第四句:关键节点必须加Puppeteer硬校验。如果你看到哪个环节非用aiAssert判断“页面出现预期内容”,建议优先改成waitForSelector加自定义校验,只有当选择器实在写不出来的时候再退回语义校验。

6.2 收尾前的一个建议

最后想说,Midscene和Puppeteer的协同关系,本质上不是“二选一”,而是“心智模型的切换”。Puppeteer给你的是“确定性的精细控制”,Midscene给你的是“模糊性的语义理解”。把两者放在同一条流水线上,让确定的部分走确定的路线,让模糊的部分走理解的路线,这套架构才真正立得住。

我这套方案里踩过的坑、试过的姿势也不算多,不同项目实际遇到的边界情况肯定更多。如果你在自己项目里跑出了不一样的坑,欢迎来回聊——这类工具的工程实践更新太快,一个人闭门造车肯定不行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询