上个月我做了一次完整的UI回归测试,结束之后干的第一件事,是把花了两周时间精心调教的那个“万能Skill”整个删掉了。不是它不聪明,恰恰是它太“全能”,反而成了整个测试流程里最难用的一环。这篇文章想给你一套正好相反的思路——不搞万能Skill,而是用5个职责单一的Agent Skill,把UI自动化从脚本生成、驱动配置、断言编写、失败诊断到报告生成这段最耗体力的链路完整串起来。适合正在用或准备用Agent做测试提效的测试工程师,也适合想戒掉“宏大Skill瘾”的Agent爱好者。
1. 先泼一盆冷水:我为什么把“万能Skill”扔进了回收站
那会儿我刚接触Agent不久,和很多测试同学一样,第一反应是“既然AI这么强,干脆做一个全流程通吃的Skill”。我给它取了个名字叫QA-Master,听起来就很厉害。里面塞了录制脚本转换、驱动配置、断言生成、失败重跑、报告生成,甚至还有测试数据造数,几乎把UI自动化团队的活全包了。实际用起来,就像把整个测试团队的活压在一个实习生身上:什么都接,什么都做得不够专业。
第一个问题是上下文被打爆。Skill的描述文件越来越长,System Prompt加到了几千字,每次Agent调用时模型都得把整套指令重新读一遍,响应速度从几秒拖到十几秒。第二个问题是不可控。某个环节出了问题,比如断言写得不稳,你得判断到底是用户给的信息不够,还是Skill里的指令写岔了,排查链路很长。第三个问题最致命:复用性极差。换一个项目,录制的轨迹格式不同、页面元素规则不同,整个万能Skill几乎要推倒重来。
后来我换了一个思路,把QA-Master拆成了几个各管一段的专用Skill。效果立刻不一样:每个Skill指令短、定位清晰,AI执行起来又快又准,换项目时只需要调整其中一个Skill,其他完全不动。这也让我重新理解了Skill和Agent的关系——Agent是那个根据目标做规划、决定下一步做什么的“大脑”,Skill是它随时能抽出来用的“专业工具包”。你让Agent去完成一次回归测试,它会想:先准备环境、再生成用例、执行、出问题再诊断、最后出报告。这个想的过程就是Agent在工作,而每一步具体用什么方式执行,靠的是Skill。
做完这个拆分之后,我回头看热搜里那些关于“skill和agent的区别”“agent做项目是不是需要很多个skill”的问题,其实答案已经很明显了:项目当然需要多个Skill,但Skill多不代表堆料。关键是粒度要小、职责要单一、能独立验证。接下来就按这个思路,讲讲我在UI自动化这条链路上最终沉淀下来的5个Skill。
2. UI自动化全链路里,哪些环节值得做成Skill
在写Skill之前,你得先把这条流水线摆上桌看清楚。一次完整的UI自动化从0到1,大致要经过这些环节:梳理测试需求、准备浏览器和驱动环境、录制或编写操作轨迹、把轨迹转成可执行脚本、编写断言、跑测试用例、失败用例诊断、聚合测试结果、生成报告、发给相关同事。这中间并不是每一环都适合用Skill封装,有的环节交给AI反而画蛇添足。
我把每个环节过了一遍,做了个价值评估:
| 环节 | 做成Skill的价值 | 是否推荐 | 原因 |
|---|---|---|---|
| 需求分析和用例设计 | 低 | 否 | 高度依赖业务上下文,AI容易放飞,更适合人工主导 |
| 环境准备(浏览器驱动) | 高 | 是 | 版本匹配问题出现频率极高,解决路径确定 |
| 脚本生成 | 高 | 是 | 录制轨迹到代码是强模板化翻译,AI效率远超手写 |
| 断言编写 | 中高 | 是 | 需要语义判断,但限定边界后AI很稳 |
| 测试执行 | 中 | 否 | 本来就该由CI或pytest负责,不需要单独Skill |
| 失败诊断 | 高 | 是 | 排查链路人肉走很慢,AI能显著提速 |
| 结果聚合 | 中低 | 否 | 测试框架自带,做成Skill反而增加维护成本 |
| 报告生成 | 高 | 是 | 模板化强、跨团队复用价值高,还能顺带接监控数据 |
| 通知与告警 | 低 | 否 | 一行配置的事,做成Skill收益低 |
筛完之后,我留下了5个,分别对应脚本生成、驱动配置、断言编写、失败诊断、报告生成。这5个环节省掉的都是每天重复的机械劳动,而且每一环的输入输出都能被明确定义,Skill做出来之后天然方便串联。
为什么不是更多?比如执行环节我也试过,但做完之后发现多此一举——pytest或者CI本来就能跑,Agent去调用一遍反而绕远路。Skill不是越多越好,而是要把Agent从“被迫思考如何执行细节”的负担里解放出来。5个刚好覆盖从执行到报告的主干价值链路,再多就会回到维护成本失控的老路。
3. 逐个拆解:5个Skill的输入、输出与核心设计
这一节是整篇的核心干货。每个Skill我会把它的定位、输入输出设计和最容易踩的细节一次说清。
3.1 Skill 1:ui-recorder,录制轨迹一键变成可用脚本
录制回放工具很多,但录完生成的脚本往往带着一堆录制器独有的冗余标记,直接跑根本不行。ui-recorder这个Skill的作用是:接收录制轨迹文件,输出干净的、符合团队代码规范的测试脚本。它面对的核心问题是格式转换,AI做这种强模板化的翻译非常擅长。
我在设计里把录制轨迹定义成了统一的JSON schema,不管Web端还是Android端,录制器导出的数据都先规范成这个格式:
{ "url": "https://shop.example.com/login", "device": "desktop", "actions": [ {"type": "goto", "value": "https://shop.example.com/login", "waitUntil": "load"}, {"type": "type", "selector": "#username", "value": "test_user"}, {"type": "type", "selector": "#password", "value": "Passw0rd!"}, {"type": "click", "selector": "button.login-btn"}, {"type": "waitForURL", "value": "**/home"} ] }Agent拿到这个JSON之后,按要求输出一段Playwright脚本:
def test_login(page): page.goto("https://shop.example.com/login", wait_until="load") page.fill("#username", "test_user") page.fill("#password", "Passw0rd!") page.click("button.login-btn") page.wait_for_url("**/home")这里有一个很关键的细节,Skill指令里必须写明“识别动态数据并参数化”。录制轨迹里经常会出现时间戳、随机生成的订单号、手机号这些动态值,如果不处理,第二天回放就必定失败。我的做法是让Skill把这类值自动替换成参数占位符,并在脚本里加上对应的fixture或数据文件。如果用的是Appium录制的Android轨迹,schema不一样,但只要在Skill指令里声明支持两种格式,并给每个端一到两个示例,Agent就能完成跨端转换。
3.2 Skill 2:browser-driver-setup,把环境配置做成确认式操作
UI自动化里有一个特别烦的问题:浏览器驱动和浏览器版本不匹配。Chrome一升级,chromedriver马上罢工,所有用例在环境准备阶段就挂了。我之前处理这个问题靠的是搜索引擎和记忆,现在靠browser-driver-setup这个Skill。它的设计目标是:把环境配置做成一次确认式操作,而不是靠人肉翻文档。
这个Skill会先指导Agent去探测当前浏览器版本:
google-chrome --version拿到版本号之后,再根据项目技术栈输出对应的安装命令和验证脚本。如果是Selenium项目,会给到对应版本的ChromeDriver下载链接;如果是Playwright项目,直接让Agent输出playwright install这类自动管理驱动的命令。Skill里还内置了一个决策逻辑:优先推荐让工具自动管驱动,因为手动维护driver版本清单的工作量根本不值得。
这里我想强调一个经验:很多测试同学用Selenium习惯了,但从维护成本来说,Playwright这类自带驱动管理的方案省心得多。Skill的输出里会明确标注更优方案,让Agent在遇到版本匹配问题时,不只是“修好”,还会提示团队可以往哪个方向做根治。
3.3 Skill 3:assert-writer,让AI写会“等”的断言
断言是最容易“看起来对、跑起来挂”的环节。很多AI生成的断言是直接把页面取到的文本和期望值做硬比较,完全没考虑页面渲染、网络延迟这些因素,结果就是用例偶发失败。assert-writer这个Skill要解决的就是这个问题:让AI生成带显式等待和重试语义的断言代码。
使用方式很简单,测试人员告诉Agent“我要校验订单金额大于99,支付成功文案要出现”,Skill会约束AI输出分类明确的断言:
# 存在性断言 expect(page.locator(".toast.success")).to_be_visible(timeout=10000) # 文本匹配 expect(page.locator(".order-title")).to_have_text("支付成功") # 数值断言 total_price = float(page.locator(".total").inner_text().replace("¥", "")) assert total_price >= 99, f"金额异常: {total_price}"三类断言分别对应页面元素是否出现、文本内容是否符合预期、数值范围是否合理。用到to_be_visible和to_have_text这种带自动重试的API,比直接assert一个inner_text要稳得多。这也是我给这个Skill定的一条铁律:凡是涉及UI状态的断言,禁止输出没有等待逻辑的裸代码。如果你在别的AI工具里让它“随便写一段断言”,大概率拿到的就是裸断言,跑两轮就露馅了。
3.4 Skill 4:failure-doctor,失败用例不再靠肉眼翻日志
用例失败之后最耗时间的不是修代码,而是定位问题出在哪。并发冲突、元素没找到、断言值对不上、驱动挂了、网络超时,每一种失败的处理方式都不一样。failure-doctor这个Skill做的事情就是给AI划定一个分析框架,让它按分类来诊断,而不是自由发挥。
我在Skill里内置了这么一张分类表:
| 失败类型 | 常见日志 | Agent应该看哪里 | 处理建议 |
|---|---|---|---|
| 元素定位失败 | TimeoutError, NoSuchElement | selector是否为动态ID或绝对路径 | 改用data-testid或相对定位 |
| 断言失败 | AssertionError | 实际值与期望值对比 | 判断是否为文案或数据变更 |
| 环境异常 | WebDriverException | 浏览器版本与驱动匹配情况 | 调用browser-driver-setup重新配置 |
| 疑似业务bug | 无通用日志关键词 | 截图和页面元素状态 | 标记为需人工确认 |
Skill的指令会要求Agent先输出“失败类型选项”,再给“判断依据”,最后才给“修复建议”。这样设计的好处是,即使AI最后的判断出了偏差,测试人员也能快速看出它是在哪一步跑的偏。实际用下来,这个Skill能消灭大概80%的“哦原来是环境问题”的无效人工排查,剩下的20%它也能把现场信息整理得明明白白再交给人工。
3.5 Skill 5:report-builder,测试结果和监控截图自动汇成PDF
报告生成是跨团队价值最高的一个环节,却往往被忽略。report-builder这个Skill的职责是:读取测试结果JSON,结合截图和监控数据,输出一份能直接发出去的HTML和PDF报告。它不是简单地堆数据,而是按团队模板排版。
我的做法是让Skill先生成干净HTML模板,再用无头浏览器打印成PDF:
playwright pdf report.html report.pdf如果团队有Grafana监控,我会在Skill里额外编排一个步骤:用Grafana的渲染接口导出一张当晚时段的监控截图,然后把它嵌到报告里。这样测试负责人打开PDF,一眼就能把用例通过率和后端负载关联起来看,不用再切三个系统。
模板里的关键信息一般包括:执行项目、执行时间、通过率、失败用例明细、趋势图、每个关键用例的截图、环境说明。Skill允许传入团队自定义模板变量,比如品牌名称、报告标题前缀,生成时自动代入。
4. 一次真实回归测试:五个Skill是怎么在各站接力干活的
理论说完了,上实战。前阵子公司商城要做一次发版回归,覆盖Web端的登录、搜索、加购、下单,外加Android端的关键路径冒烟。放在以前,这个流程光准备环境和处理录制脚本就得花掉一整个下午,这次我全程用Agent编排这5个Skill来跑。
整体调用顺序是这样的:
| 步骤 | 使用的Skill | 输入 | 输出 |
|---|---|---|---|
| 1 | browser-driver-setup | Chrome版本、项目类型 | 驱动配置命令与验证脚本 |
| 2 | ui-recorder | 录制轨迹JSON | 可运行的pytest脚本 |
| 3 | assert-writer | 页面URL、断言需求 | 带等待策略的断言代码 |
| 4 | 执行阶段 | pytest命令 | 测试结果JSON、截图、日志 |
| 5 | failure-doctor | 失败用例的日志、截图 | 原因分类、修复建议、是否重跑 |
| 6 | report-builder | 结果JSON、截图、监控数据 | HTML报告 + PDF |
编排层我直接用了Agent工作流,逻辑是这样一段伪代码:
steps: - skill: browser-driver-setup input: { browser: chrome, project: shop } - skill: ui-recorder input: { trace: trace.json, framework: playwright-pytest } - skill: assert-writer input: url: order_success assertions: - total_price >= 99 - title == "支付成功" - run: pytest tests/ -m regression - if_failed: skill: failure-doctor input: { logs: pytest-output, screenshots: failure_screenshots } - skill: report-builder input: results: result.json screenshots: [*.png] grafana_dashboards: [overview]中间有一个细节值得单独说一下。执行阶段中有一条用例因为网络抖动超时了,failure-doctor读取日志后判定为环境异常,并建议重跑一次。编排层按它的建议自动重跑了该用例,第二次通过,最终报告里这条用例被标记为“通过(重跑一次)”。整个过程我没有手工介入,放在以前这种波动失败通常要人在CI日志里翻半天。
整轮跑完,结果JSON、失败截图、Grafana监控图自动汇进报告,PDF生成后直接推到钉钉群。时间从以前的2到3天压缩到一上午,主要省下来的不是执行时间,而是脚本整理、环境处理、失败排查这些穿插在中间的等待和沟通成本。这给我最深的体会是:Agent的价值不一定是一次性把节奏拉满,而是把链路中每一段碎片化的人工操作,变成了一条自动运转的流水线。
5. Skill设计里容易忽略的四个关键决策
很多教程只会教你写SKILL.md的格式,但真正决定这套体系能不能长期跑下去的,是下面四个设计决策。它们都是我在实际落地过程中被反复折磨之后总结出来的。
第一个决策是输入输出全部JSON化。五个Skill接力,上一站的输出就是下一站的输入,如果输出是一段格式混乱的自然语言,下游Skill根本没法稳定消费。JSON是最不依赖语言的中间格式,字段可预期、可校验。我给每个Skill规定了严格的输入输出schema,即使某个环节换成了别的实现,接口也不变。
第二个决策是Skill的粒度要“一站一段”。一个Skill只处理一个环节,如果某个Skill的指令字数超过800,就要考虑拆分了。这是一个经验阈值,超过这个量级之后,AI误读指令的概率明显上升,且后续维护成本激增。反之也不要拆得过于细碎,比如把“点击按钮”单独做成一个Skill,那纯粹是折腾自己。
第三个决策是Skill内部的结构要分层。一份好的SKILL.md不是一段长篇指令,而是角色定位加固定指令加示例输入输出加防呆条款。角色定位告诉AI它在这个环节扮演什么,固定指令是必须遵守的操作步骤,示例让AI少走弯路,防呆条款把容易犯的错误明令禁止。把这四部分清楚地分开,比混在一段自然语言里稳定得多。
第四个决策是给Skill本身写验收用例。每个Skill都准备一份最小的夹具数据,比如ui-recorder的夹具是一条只有三步操作的录制轨迹,assert-writer的夹具是一个带浮动价格的订单页面。每次改了Skill内容,先跑一遍夹具,确认输出没有回归。这个习惯一开始我觉得麻烦,但救了我好几次——有次我在report-builder里加了个模板变量,差点把原本能正常生成PDF的链路搞挂,跑夹具后秒发现问题。Skill也是代码,不测试就等于裸奔。
6. 实测中踩到的五个坑,以及对应修法
最后分享五个我实际踩过的坑,每一个都是付费买来的经验。如果你也打算把这套方法落地,大概率会碰到其中几个。
坑一是把整页DOM快照直接塞给AI。第一次用assert-writer时,我让Agent读取page.content()拿到的完整HTML,结果Prompt里塞了接近10万个字符,模型开始输出一些前言不搭后语的断言。修法是在Skill指令里写明“只提取关键节点,比如input、button、提示文案、价格等,汇总为精简结构再提交给AI”,也可以先用locator把目标元素缩小到具体范围,再让AI分析。
坑二是录制轨迹里的动态数据没有处理。录制时输入了带时间戳的账号,第二天回放时账号已存在,脚本启动就挂。修法是ui-recorder指令里强制要求识别动态值并参数化,这个前面已经提过,但值得再强调一遍,因为它发生的频率太高了。
坑三是AI生成了一堆又长又脆的XPath。它给了一个这样子的路径://*[@id="app"]/div[2]/div[1]/div[3]/button[2],下次前端加了一个隐藏节点,选择器就挂了。修法是在Skill里明确规定:禁止生成绝对路径XPath,优先使用data-testid、role或者文本定位。前端同学只要约定好data-testid,这个问题几乎可以根除。
坑四是报告里的截图路径打不开。report-builder早期生成的PDF里,图片用的是本机绝对路径,文件发给Windows同事直接裂图。修法是所有图片在Skill内部先拷贝到报告目录,用相对路径引用,条件允许时直接base64嵌进HTML,彻底摆脱环境依赖。
坑五是录制轨迹里全是冗余回放动作。录了20次鼠标经过同一菜单,生成的脚本跑起来慢、还容易误触。修法是在Skill指令里加一条“轨迹清理规则”:连续对同一元素的操作只保留最后一次,间隔过短的移动事件合并,超过3秒的等待记录为显式等待。经过这一层过滤,脚本长度和稳定性都有明显改善。
等这些坑填完之后,我发现这5个Skill带给团队最大的价值反而不是测试提效本身,而是把个人脑子里那套“怎么跑测试”的经验变成了每个成员都能调用的知识资产。招了个新人,不需要从零教他怎么配驱动、怎么写断言,直接让他跟着Agent把这些Skill跑一遍,基本就上手了。如果你想照着做,我的建议是别急着一次性铺开所有环节,先从你上个月最痛的那个测试环节开始,做一个Skill,跑稳,再加下一个。