
做过几年UI自动化测试的人大概都有一种感觉脚本写了多久维护的时间就差不多是多久。项目里最讽刺的画面往往是开发迭代一个页面测试跟着改一个星期的定位符。哪怕有录制工具、哪怕有 Page Object 模式本质上也还是在用人的时间堆机器的时间。我从去年开始认真琢磨一个问题如果让智能体自己来看需求、自己写 Playwright 脚本、自己跑一遍、自己修错这条链路能不能真正转起来答案是能但一开始的方向容易想偏。市面上很多“AI 生成自动化脚本”的方案本质上只是接一个对话窗口让模型给你一段代码然后你继续当人肉执行器。真正有用的是把 Playwright 的能力包装成 Agent 可以直接调用的技能Skill让智能体不仅能生成脚本还能去读页面结构、执行脚本、解析报错、再改脚本直到跑通为止。这篇文章就把我搭这套东西的过程和踩过的坑完整写出来适合正在做测试自动化的开发、QA 或者对 Agent 应用感兴趣的朋友参考。1. 让 AI 写自动化脚本卡住的从来不是“写”这一步1.1 脚本维护的真正痛点需求永远是新的脚本永远要改先说我接手过的某个内部管理后台。页面倒不复杂无非是登录、查询列表、新增数据、修改状态、导出文件这些操作。但问题在于这个后台两周一个小迭代一个月一次大改版。今天按钮叫“确认提交”下周就改叫“提交申请”昨天列表里的状态还是一个文本今天变成了可点击的标签组件。每改一次我就要在几十个脚本里把对应的定位符一个一个换掉。最开始我也用过录制回放工具。但录制回放有个天生问题它记下来的是屏幕上的坐标和固定的 DOM 路径页面一变化就全废。后来上了 Page Object把元素定位集中到一个文件里确实好维护了一些但依然需要人工去读懂需求变更再手工同步对象库。你会发现自己每天花大量时间做的事情根本不是“写代码”而是“对着页面找元素改定位试跑再改”。真正消耗人的是这条链路的重复性。每一条用例都要经历理解需求、分析页面结构、选定定位方式、写操作步骤、写断言、试运行、看报错、修脚本。这个循环跑了多少次你的时间就被吞噬了多少次。自动化测试的初衷本来是解放人力结果做得越深入维护成本越像滚雪球。1.2 需求到脚本的“翻译”比敲代码本身更消耗人后来我试着用大语言模型帮我写脚本也就是最常见的“AI 辅助编程”。把需求贴进去让模型生成一段代码飞快。可你仔细想想这省掉的只是最后那一步“敲代码”前面最耗时的需求理解、结构探测、定位符确认、试运行反馈一步都没省。举几个真实场景你就明白了。模型生成page.locator(text新增用户).click()这种写法看起来没问题但页面上可能同时存在“新增用户”按钮和“新增用户导出”菜单运行时直接报strict mode violation因为有两个元素匹配。再比如模型喜欢生成page.click(button:has-text(确认))但页面上这个确认按钮在弹窗里而弹窗是异步渲染的直接 click 就会说元素不存在。这些问题靠“生成代码”是解决不了的。它们需要的是对页面的实际感知真实的 DOM 结构是什么样的、哪些元素当前可交互、点击之后页面发生了什么变化。而这些信息恰恰是传统对话式生成流程里模型拿不到的。1.3 为什么用 Agent而不是简单的对话式生成所以问题的核心就变成了怎么让模型不只是一个“代码生成器”而是一个“会动手的测试工程师”。这就要引入 Agent 的设计思路——给智能体挂载一组可调用的工具Skills让它自己决定什么时候去探测页面、什么时候生成脚本、什么时候执行脚本、什么时候根据报错修改代码。一个合格的人类测试工程师是新需求来了打开页面先看看元素长什么样再写脚本跑挂了看报错改脚本跑通了再整理用例。Agent 要复现的是这条路而不只是最后一步写代码的动作。这正是 Agent Skills Playwright 这套方案的核心让 Playwright 变成一个 Agent 能用、会用的工具箱而这个工具箱里装的都是浏览器自动化的标准动作。我自己实际跑下来的感觉是链路一旦通了新需求的自动化覆盖率提升很快而且不用像以前那样逐条手写。接下来我先把 Skill 的机制讲透再拆解整条链路。2. Agent Skills 机制的本质给 Agent 挂一套可以实际操作浏览器的工具链2.1 先拆一下“Skill”到底是什么Skill 这个概念听起来高大上但它做的事很像给一个新手配工具箱。想象你请了一个聪明但没经验的助手你当然可以直接口头告诉他“去把那个页面的登录功能测一下”但他很快就懵了——他不知道怎么打开浏览器、不知道从哪里看页面元素、不会写脚本、报错了他也看不懂。可如果你先给他一套工具每样工具还配了说明书和注意事项他就能按图索骥地把活儿干下来。Agent 里的 Skill 就是这套“工具加说明书”的封装。一个典型的 Skill 至少包含三个部分第一是它的名称和描述说明这个技能是做什么的、适合在什么场景下使用第二是它对外暴露的工具列表每个工具都有明确的输入输出定义第三是工具内部的实际执行逻辑也就是真正调用 Playwright 完成操作的那段代码。以我搭的ui_test_agent为例在 Agent 框架里注册的时候大概长这样{ name: ui_test_agent, description: 根据自然语言描述的测试需求生成并执行 Playwright UI 自动化脚本能读取页面结构、运行脚本、解析报错并自动修复。, tools: [ { name: probe_page, description: 打开指定页面并提取当前可见的核心可交互元素清单。, parameters: { url: 待探测的页面地址, keywords: 需要重点关注的按钮或输入框关键词 } }, { name: generate_script, description: 根据测试需求与页面结构信息生成或修改 Playwright 脚本。, parameters: { task_description: 用户提出的测试需求描述, page_snapshot: probe_page 返回的页面结构快照, existing_script_path: 需要修改的脚本路径可选 } }, { name: run_script, description: 在隔离环境中执行指定的 Playwright 脚本并返回运行结果。, parameters: { script_path: 要执行的脚本文件路径 } }, { name: parse_failure, description: 读取脚本执行失败的错误输出给出原因判断和修复建议。, parameters: { run_output: 上一轮 run_script 的完整输出, script_path: 当时执行的脚本路径 } } ] }注意这里的描述写得越具体越好。Agent 模型的工具选择能力完全依赖这段描述来匹配当前任务如果你的描述写成含糊的“执行浏览器操作”它根本不知道什么场景该调哪个工具。2.2 Skill 内部串联的四个标准动作探测、生成、执行、反馈我这边最终收敛成了四个核心动作基本能覆盖日常 UI 自动化测试的绝大多数需求。第一个是probe_page。干什么用的让 Agent 先打开目标页面把当前页面里能看见的输入框、按钮、链接、表单元素全部捞出来。这个动作特别关键因为它给模型提供了它写代码时最缺的东西真实的页面结构。很多 AI 生成的脚本跑不通就是因为它瞎猜元素而这一步直接消灭了猜测。第二个是generate_script。基于需求描述和页面结构快照生成一段可以直接运行的 Playwright 脚本。在 Skill 内部这个工具会把用户需求、页面快照、约束规则拼成一段结构化提示词再调用模型生成代码最后落到指定目录。第三个是run_script。生成完不能只看代码必须真的跑起来。这一步我封装的是子进程调用 Python 脚本采集控制台输出、异常堆栈和运行耗时。第四个是parse_failure。脚本跑挂了Agent 需要看懂为什么挂。这里我会把报错信息做一层清洗把冗长的堆栈变成模型容易理解的结构化信息再让它判断是定位符问题、等待时长问题还是页面结构变了。这四个动作串起来就形成了一个完整的循环探测 → 生成 → 执行 → 反馈 → 修复 → 再执行。Agent 每次拿到需求后会自动按这个链路执行直到跑通或者到达迭代上限。2.3 最小可运行的环境准备说点实际的这套东西要跑起来的环境并不复杂。我用的主力组合是 Python 3.10 及以上版本、Playwright 库、一个支持工具调用的模型接口以及一台装了 Chromium 内核的机器。创建虚拟环境并按需安装依赖即可python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install playwright playwright install chromium选 Playwright 而不是其他浏览器自动化框架我这边主要有三个考虑。一是它内置了自动等待机制大多数情况下元素还没出现时不会立刻报错会先等一段时间这对 AI 生成的脚本来说太救命了。二是它的定位语法更语义化get_by_role(button, name登录)、get_by_label(用户名)这类方式比一堆xpath//div[class...]更稳定模型也更容易写对。三是它有 trace 查看器Agent 跑挂了我可以在本地打开录制的 trace 文件看到底是哪一步出的问题。跑脚本时我用 headless 模式也就是无头浏览器节省资源也适合在 CI 环境里跑。调试阶段会临时打开有头模式方便看浏览器到底做了什么。3. 把“需求”变成“脚本”核心链路逐段拆解3.1 第一步先让 Agent 认识页面而不是凭空猜整条链路里我最看重的一步就是页面探测因为它直接决定后续脚本的质量。我自己写的probe_page工具会启动浏览器打开目标页面等待页面稳定之后抓取一组关键信息页面标题、当前 URL、可见的输入框的 label 和 placeholder、按钮的可见文本、链接名称、下拉框的选项等。比如探测一个登录页返回的结构大概是{ page_title: 后台管理系统-登录, url: https://demo.example.com/login, inputs: [ {type: text, label: 用户名, placeholder: 请输入工号}, {type: password, label: 密码, placeholder: 请输入密码} ], buttons: [ {text: 登 录, role: button} ] }这段结构信息会原样传给generate_script模型基于“页面真实有什么”来写代码而不是凭训练数据里的记忆瞎编。实际效果差别非常大最直观的表现就是生成的脚本从“十次跑通一次”变成了“第一次成功概率明显提高”。另外一个细节探测的时候要主动过滤掉隐藏元素和非交互元素。很多页面源码里藏着大量display:none的元素模型看到这些会被误导以为用户能看到并点击。所以我只输出视口内的可见元素这个过滤逻辑虽然不起眼但对脚本生成的准确率帮助极大。3.2 生成脚本时的约束设计光给模型页面结构还不够还得给它一套清晰的约束规则。我的做法是把以下几点写进生成脚本提示词里每次调用generate_script都带上优先使用get_by_role、get_by_label、get_by_placeholder这类语义化定位如果页面有多个匹配元素必须加上name参数限定所有点击和输入操作前先调用wait_for_selector确保元素就绪不加人为的time.sleep()交给 Playwright 的自动等待脚本结构保持简单一个用例对应一个函数断言要明确不能只是“页面没有报错”。给一个登录加新增用户流程的生成示例最终产出的脚本大概是这样的from playwright.sync_api import sync_playwright, expect def test_login_and_create_user(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://demo.example.com/login) page.wait_for_selector(input[nameusername]) page.get_by_label(用户名).fill(tester01) page.get_by_label(密码).fill(pssw0rd) page.get_by_role(button, name登 录).click() page.wait_for_selector(text欢迎回来) expect(page).to_have_url(https://demo.example.com/dashboard) browser.close()这些规则不是拍脑袋定的。之前没有任何约束的时候模型确实会生成page.locator(xpath//div[contains(text(),登录)])这类脆弱脚本页面结构一动就废。强制语义化定位之后脚本的耐用性明显上来了。3.3 从“生成脚本”到“执行验证”的效率关键生成脚本只是中途站真正考验人的是把执行环节设计得稳、快、结果可解析。我自己在run_script里用子进程跑不要放在 Agent 主进程里这样即使脚本把浏览器搞崩了也不会拖垮 Agent 服务。核心逻辑简化后大致如下import subprocess import sys def run_script(script_path: str, timeout: int 120) - dict: cmd [sys.executable, script_path] try: result subprocess.run( cmd, capture_outputTrue, textTrue, timeouttimeout, cwdscript_dir ) return { returncode: result.returncode, stdout: result.stdout[-3000:], stderr: result.stderr[-3000:], } except subprocess.TimeoutExpired: return { returncode: -1, stdout: , stderr: 脚本执行超时超过 {} 秒.format(timeout), }为什么截取输出末尾的 3000 个字符因为 Playwright 报错往往很长前面一大半是调用栈真正有用的错误原因在最后几行。如果全量塞给模型既浪费上下文窗口又容易让模型被无关堆栈干扰。截断后的输出既清晰又够用模型判断错误的准确率反而更高。还有一个效率细节每个脚本跑完如果失败我会顺手抓一张页面截图保存到固定目录。截图不是给模型细看的而是留给我自己排模用的。我打开截图一眼就能看出 Agent 是点错了按钮还是页面根本没跳转排障效率高很多。4. 从“能跑”到“能修”闭环迭代的设计细节4.1 失败信息要结构化否则 Agent 跟你一样一头雾水链路跑到这里真正的分水岭来了脚本跑挂了之后Agent 能不能自己修得对。刚开始我直接把原始报错丢给模型结果它有时会误判把“页面元素加载慢”当成“定位符写错了”修了半天还在原地转圈。后来我加了parse_failure这层清洗逻辑把原始错误归类成几个高频原因给模型一个更明确的方向。比如 Playwright 报TimeoutError: locator.wait_for() timed out我清洗后输出的是错误类型元素等待超时 可能原因 1. 选择器没有匹配到任何元素页面结构变化 2. 选择器匹配到元素但一直不可见被弹窗遮挡或处于加载中 3. 页面跳转失败停留在了上一页 建议操作先调用 probe_page 获取当前最新的页面结构再对比脚本中的定位符是否仍然存在。这么一处理模型再接手时思路就清楚多了。它不用大海捞针地从堆栈里猜原因而是拿到结论去验证和修复。我把清洗规则做成了一个规则引擎适配最常见的几类 Playwright 错误——超时、严格模式冲突、元素不可见、框架内元素查找失败等。4.2 设置迭代上限与任务终止条件闭环循环很容易踩一个坑Agent 在同一个问题上绕圈。我一开始没设置迭代上限结果某次一个脚本连续改了十几次每次都是改完一个错又出来一个新问题最后还是没跑通白白浪费了一堆调用额度。后来我加了两条硬约束。第一是最大迭代次数默认 3 轮3 轮跑不通就直接退出标记为“需要人工介入”。第二是重复错误检测如果连续两次报的错误属于同一类型且定位符没有发生变化就判定 Agent 在无效修复立刻终止。这套终止逻辑等于给 Agent 划了一条安全线让它不至于在同一个泥潭里无限挣扎。你要相信有些问题它就是需要人工看一眼才知道怎么处理强 AI 不是万能的加终止条件反而是负责任的设计。4.3 把修过的脚本沉淀成回归资产Agent 跑通一个脚本之后这个脚本不应该只是临时产物。我的习惯是把它按测试目录结构存档命名规则是test_页面_场景.py方便后续纳入回归集。这个步骤和我平时写用例的习惯完全一致Agent 产出的脚本也是用例资产的一部分必须能被人看懂、能被人维护。存档的同时我还会把生成这个脚本时的需求描述、页面快照、失败修复记录一起存到一个 markdown 文件里。这样做的好处是下次这个页面再改版时我可以把旧的脚本路径连同新的页面结构快照丢给 Agent让它基于原来的脚本做增量修改而不是从零再写一遍。改存量脚本这件事后面会提到是整套 Agent 最出彩的场景。这里有一个很容易被忽略的细节脚本命名要“人工友好”。我曾经让 Agent 自动命名结果它生成了一堆test_copy_2_final_new.py之类的名字文件多了根本没法管理。后来我改了约束要求命名必须体现页面和场景比如test_login_success.py、test_create_user_validation.py。整理的时候才知道什么叫清爽。5. 落地过程中的坑以及它目前的真实效果边界5.1 定位符不稳定页面改版和动态渲染是绕不开的问题先说这个最大的坑。即使 Agent 已经用语义化定位了页面改版后元素文本发生变化或者前端重构之后整个 DOM 结构重写脚本照样挂。尤其是单页应用路由切换时页面会有一段“假死”状态旧的交互元素还在但点击已经无效了。Agent 生成的脚本如果没做显式等待在这种页面上就会时好时坏。我现在给 Agent 的生成规则里有一条硬性要求所有关键操作之前必须wait_for_selector并且等待的是目标元素的可见状态。这个规则在大多数场景下能避免动态渲染带来的不稳定。但遇到前端整体重构的情况脚本必然会挂这是合理的——页面都没了脚本凭什么还活着。这时候probe_page重新采集一次页面结构让 Agent 对照新结构修脚本属于正常操作。我列了一个常见问题对照表贴在这里给读者参考现象常见原因应对方式元素等待超时页面结构变化或元素不可见重新探测页面比对定位符是否存在strict mode violation定位符匹配到多个元素补充name或其他限定条件点击后无响应动态渲染导致事件绑定未生效点击前强制等待目标状态出现登录后 URL 不符跳转逻辑变化更新断言目标确认业务含义5.2 安全边界不能让它随便点生产环境的按钮Agent 能自主执行脚本能力越强越要划清楚它可以干什么、不可以干什么。我这边锁死了几个约束脚本运行环境只能访问测试环境域名代码里会显式校验页面 URL一旦发现跳转到外部域名立即终止环境的测试账号只会配置只读或低权限的专用账号比如只能查看列表、只允许在独立子域创建数据禁止 Agent 在未确认的情况下触发删除、生产外部导出这类高危操作。这点我必须多说一句。Agent 的优势是自动执行但风险也在自动执行上。脚本出错了点错一个按钮后果可比人手动点错大得多——人还知道及时收手Agent 只会继续跑下去。所以安全边界不是可选项是所有这类系统的底线。凡是 Agent 可能要执行的脚本我都会加一层“高危操作确认清单”里面匹配删除、提交、转账这类关键字一旦命中强制要求人工确认才能继续。5.3 与现有测试体系的对接思路Agent 产出的是标准 Python 脚本所以和现有测试体系对接并不难。我的做法是脚本全部生成到项目测试目录下统一用 Pytest 风格编写然后打上自定义标记比如pytest.mark.agent_generated。这样可以在 CI 流水线里单独拉一个 job 来跑这批 Agent 生成的用例不污染人工用例的稳定性指标。对接时间长了之后我的感受是这套东西最舒服的使用场景是“日常冒烟回归”和“需求变更后的快速验证”。每次页面改版我把改动说明和新页面结构丢给 Agent让它把相关脚本更新一遍再跑一轮省下的时间非常可观。但要实话实说它的能力边界也明显跨系统数据流转的复杂链路用例涉及图片像素对比或者复杂文件断言的用例以及那些需要业务知识深度判断的断言逻辑目前仍然需要人来兜底。我一直觉得判断一个工具好不好用不是看它能不能 100% 替代人而是看它能不能把人的时间从 60% 的机械重复里解脱出来。这套方案目前能达到的准确率已经足够把“维护脚本”这件事从我的日常优先级清单里往后挪一大截了。6. 下一步扩展方向和最后一个实用小技巧6.1 把元素记忆库做成 Skill 的记忆现在整套系统跑顺了我开始琢磨怎么让它更“长记性”。目前的做法是每次脚本跑通后把稳定可靠的定位符回收到一个元素库文件里存成 JSON 记录关联到页面和操作场景。下次生成脚本时我会在提示词里把相关元素库中的候选定位方式一起传给模型让它优先复用而不是每次都重新猜。这个思路其实就是人工维护 Page Object 的一个自动化变体。元素库积累得越多Agent 生成的脚本就越接近团队沉淀的高质量写法。我目前跑了一两个月登录相关的元素库已经覆盖了绝大多数页面的通用导航新页面再生成脚本时初始成功率明显比冷启动阶段高。6.2 一个最实用的小技巧从改存量脚本开始最后分享一个对我帮助最大的落地策略。如果你刚开始搞这套东西别一上来就让 Agent 从零生成几十个脚本那样的挫败感会很重。我的建议是先拿存量脚本喂给它挑一个需要小改的脚本比如按钮文案变了、某个输入框加了一个校验提示把新页面结构快照丢给它让它基于旧脚本做最小修改。改造存量脚本这件事天然就是一个语义清晰的“修改任务”模型的成功率特别高而且产出质量一眼就能看出来对不对。等你对链路的稳定性有底了再逐步让它从零生成新页面脚本那时候你会发现Agent 对你们项目代码风格的把握已经和最开始完全不是一个水平了。我在实际使用中还有一个体会Agent 写脚本的过程反过来会帮你发现测试体系本身的混乱。比如很多页面存在多个相似按钮过去人手工写脚本都用模糊定位蒙混过关Agent 跑的时候会把这类歧义问题暴露出来逼着开发把可访问性属性做得更规范。从这个角度看它不只是一个自动化工具还是一个推动前端可测试性改进的探针。