“AI + 测试”喊了这么久,真正让它落地产生价值的场景并不算多,UI 自动化是其中一个比较实的。但这半年我观察到一个现象:很多人一上来就想搞一个“万能 Skill”,想让 Agent 一个技能包干完所有事——从解析需求、生成脚本、跑用例、分析失败到出报告,全塞进一个 SKILL.md 里。结果往往是一次次被上下文长度打脸,被工具误调度折磨,最后一顿操作猛如虎,回头还得手工补报告。
我现在的做法正好相反:拆。把 UI 自动化到报告生成拆成 5 个各管一摊的小 Skill,让 Agent 像项目经理一样调度它们,跑通了整条链路。这篇文章就把这套设计思路、每个 Skill 的落地方案、以及踩过的坑完整写出来,给正在用 Agent 做测试提效的朋友一个可直接参考的样本。
1. 先想清楚:为什么一个“万能 Skill”撑不起全流程
1.1 大而全的 Skill 在 Agent 机制里是怎么“卡壳”的
我先说个真实感受。Agent 用 Skill 的执行机制,是基于模型对 Skill 描述的理解动态调用工具和流程的。你把需求解析、脚本生成、执行命令、失败重试、报告模板全写进一个 Skill 里,看着很全能,模型每次调用之前得先把几百行指令读完,再自己判断“现在该走哪条分支”。这在短流程里没什么问题,一旦 UI 自动化这种多步骤长流程跑起来,问题就非常明显。
首先是上下文窗口的浪费。一个万能 Skill 的 SKILL.md 动辄两三百行,模型每轮对话都要把这些内容当成上下文的一部分。前面几轮还能保持判断力,跑到中间阶段,前面读过的指令早就被冲淡了,模型开始出现“该执行脚本的时候去解析需求”“该生成报告的时候去改代码”这种错乱。
其次是 Prompt 冲突。你想让 Skill 既覆盖“严谨的脚本生成”,又覆盖“简洁的报告输出”,这两类任务的信息优先级、输出格式完全不同。写在一个 Skill 里,模型很难同时满足。你要是再塞进去一个“失败重试策略”,它分分钟把重试逻辑写到测试代码里去。
最后是没法针对单个环节优化。真实项目里,脚本生成这个环节天天改,报告模板可能一两个月才动一次。放在一起,改脚本生成会担心影响报告逻辑,改报告模板又怕碰坏执行流程,维护成本成倍上升。
用一句话概括就是:万能 Skill 把“串行流程的复杂度”全压在了“模型一次性理解所有规则”上,模型扛不住,人的维护也扛不住。
1.2 用“工厂流水线”思路拆解测试流程
想通之后,我换了个类比:别把 Skill 当“全能工人”,把整套流程当工厂流水线。流水线上每个人只负责一道工序,工位之间用标准容器传递半成品。哪个工位出了问题,就修哪个工位,其他环节完全不受影响。
UI 自动化测试这个流程,本质上也能切成几段独立工序:
- 理解需求:用户一句“帮我测一下登录,出个报告”,怎么变成可执行的测试用例?
- 生成脚本:有了用例步骤,怎么变成浏览器或手机能跑的自动化代码?
- 执行调度:代码跑起来需要什么环境?浏览器驱动、设备、依赖库谁来管?
- 失败分析:跑挂了,是元素没找到?断言不对?还是环境问题?
- 报告输出:结果怎么汇总成一份看得懂的文档,并且送到该看的人手里?
每一段的输入输出清晰,依赖的工具也不同。自然语言解析靠大模型,执行调度需要 Shell 能力,报告生成又依赖模板渲染和文件写入。硬塞一个 Skill,本质上是逼模型在同一个上下文里同时扮演五个角色,当然容易精神分裂。拆开之后,每个 Skill 只负责一个角色,Prompt 可以写得特别专注,上下文只在对应环节被加载,反而省 token。
1.3 拆完之后:五个 Skill 各守一段
基于这个思路,我最终沉淀下 5 个 Skill,名字和各自职责如下:
| Skill 名称 | 核心职责 | 典型输入 | 典型输出 |
|---|---|---|---|
| spec-parser | 解析用户需求为结构化用例 | 自然语言测试描述 | test_plan.json(用例清单) |
| script-generator | 根据用例清单生成 pytest 代码 | test_plan.json | test_xxx.py(可执行脚本) |
| executor | 执行测试、管理驱动与设备、收集产物 | 脚本路径 | results.json、截图、日志 |
| failure-analyzer | 分析失败原因并给出修复建议 | results.json、截图、日志 | failure_analysis.md |
| report-builder | 生成测试报告并推送通知 | 各类执行产物 | report.html、report.pdf、推送消息 |
后面你会看到,这 5 个 Skill 之间不只是“各干各的”,它们有一套固定的前后依赖关系。这套组合跑通之后,一个原本要人工花 1 小时完成的中小型测试流程,压缩到十几分钟完全可行。
提示:这 5 个 Skill 的划分不是绝对的,如果你的项目没有 App 端,或者报告不需要推送,完全可以砍掉某个 Skill 或者合并相近的职责。关键是保持“单一职责”这个原则。
2. 五个 Skill 怎么串起来:编排逻辑与上下文流转
2.1 Agent 是“项目经理”,Skill 是“专员”
拆完 5 个 Skill 之后,很多人会问:那 Agent 本身还干什么?答案是:Agent 当项目经理,只做两件事——理解用户意图,决定调用哪个 Skill 以及什么时候调用。
我举个实际场景。用户对 Agent 说:“验证一下登录模块,正确密码能登录成功,错误密码要报提示,跑完把报告发群里。”
Agent 收到这句话后的内部思考大致是这样:用户要的是“验证登录模块”,这属于解析需求 → 生成脚本 → 执行 → 出报告的完整链路。于是它开始一级级调用:
- 调用 spec-parser,把一句话拆成两个用例:正确密码登录成功、错误密码提示错误;
- spec-parser 把结果写进 test_plan.json,Agent 读取后交给 script-generator;
- script-generator 生成 test_login.py;
- Agent 调用 executor,跑 pytest test_login.py;
- 执行结束,executor 把结果写入 results.json,Agent 再调 failure-analyzer 看有没有失败;
- 最后调 report-builder,生成 HTML 报告,并推送到企业微信。
注意,这个过程中 Agent 没有直接写测试代码,没有自己跑命令,也没有自己拼报告。它做的是“看下一步该谁上场”。当然,5 个 Skill 个个都可能需要调用底层工具(跑 Shell、读写文件),但这些工具绑定在各自 Skill 内部,而不是堆在 Agent 的全局上下文里。
2.2 上下文流转:用“文件”传递半成品,不靠对话记忆
这是整条链路能不能稳定跑通的关键,也是我最想强调的经验:Skill 之间的数据交换,一律通过文件完成,不要让 Agent 靠对话历史记上一轮结果。
具体流程是:
- spec-parser 写
artifacts/test_plan.json; - script-generator 读
test_plan.json,写artifacts/test_login.py; - executor 读脚本路径,执行后写
artifacts/results.json,并把截图、日志放进artifacts/screenshots/和artifacts/logs/; - failure-analyzer 读
results.json和附件,写artifacts/failure_analysis.md; - report-builder 读所有这些产物,在
artifacts/reports/下生成报告。
为什么必须走文件?两个原因。第一,UI 自动化执行期间会有大量中间产物,截图、日志一多,塞进对话历史马上把上下文撑爆。第二,文件是“断点续跑”的基础。如果流程跑到第 4 步挂了,你只需要重置 Agent 会话,告诉它“读一下 artifacts/test_login.py 和 results.json,接着分析”,它就能跳过前 3 步继续干活。靠对话记忆的话,会话一断全部归零。
我在每个 Skill 的 Prompt 里都会写一句硬性要求:“执行完成后,只返回产物文件路径和一两行摘要,不得把完整日志或代码内容粘贴回对话。”这一句直接让 token 消耗降低了非常可观的比例。
2.3 顺序链路之外,还要给 Agent“提前判断”的提示
Skill 的 Trigger 条件也得写清楚。比如 report-builder 的 SKILL.md 描述里,我第一句话就是:“等测试执行结束、results.json 存在之后再调用。用户问报告时如果还没执行,先调用 executor。”这句话不是给人类看的,是给模型看的。它可以有效避免 Agent 在用户说“给我出个报告”时,真的跳过执行环节直接拿空数据渲染。
同样,failure-analyzer 的描述里写:“仅当 results.json 中存在 failed 或 error 状态的用例时优先调用;全部通过的情况下可以跳过这一步。”这一个条件就能省掉大部分多余调用。
3. 每个 Skill 的落地细节:核心 Prompt 与实操参数
3.1 spec-parser:把“人话”变成结构化用例
这个 Skill 是所有环节的地基。如果需求解析错了,后面脚本、报告全是白搭。我给它定义的核心指令是:把用户的测试描述转换成 JSON 数组,每个用例包含id、title、preconditions、steps、assertions五个字段,并且只输出 JSON,不要任何解释。
一个简单的 Prompt 骨架如下:
你是测试用例解析器。用户会输入对某个功能模块的测试描述,你需要: 1. 提取出所有可独立验证的测试场景; 2. 每个场景输出为一个 JSON 对象; 3. 字段格式:{"id": "TC001", "title": "...", "preconditions": [...], "steps": [...], "assertions": [...]}; 4. 如果用户描述中没有明确前置条件,preconditions 给空数组; 5. 只输出 JSON 数组,不要输出任何解释文字。有一段经验我想特别分享:一开始我让这个 Skill 自由发挥,结果 AI 把一些口语化的表达也当成测试步骤写进去了。比如用户说“顺便看看登录按钮好不好看”,模型居然生成了一条“验证登录按钮美观度”的用例。后来我在指令里加了一句“忽略与功能验证无关的主观描述”,这个问题就基本消失了。
另外我要求这个 Skill 输出里带一个assumptions字段,记录需求不明确时的合理假设。例如用户没说用哪套环境,它默认测试环境。这个字段在后续脚本生成阶段非常有用,能减少很多来回确认。
3.2 script-generator:管住技术栈,防止 AI 自由发挥
这个 Skill 是最容易翻车的,原因是大模型写代码时“自由意志”太强。今天给你生成 Selenium,明天给你生成 Playwright,后天甚至冒出个你没装的库。所以 script-generator 的 Prompt 第一要务是“锁死技术栈”。
我在这个 Skill 里固定了规则:Web 端用 Playwright + Pytest,App 端用 Appium + Pytest。脚本风格统一用 Page Object 模式,选择器优先用role、placeholder、text等语义化定位方式,禁止使用动态生成的 id。等待策略统一用 Playwright 的自动等待或显式expect,禁止写死time.sleep()。
一个生成出的测试脚本大概长这样:
import pytest from playwright.sync_api import Page, expect def test_login_success(page: Page): page.goto("https://example.com/login") page.get_by_placeholder("用户名").fill("test_user") page.get_by_placeholder("密码").fill("correct_password") page.get_by_role("button", name="登录").click() expect(page).to_have_url("https://example.com/home")这个 Skill 我设置了明确的输入输出,它的 Prompt 里有这么一条:“读取test_plan.json,为每一个测试场景生成一个独立测试函数,函数命名必须是test_开头,文件保存到artifacts/目录,只返回文件路径和函数清单。”
为什么这么强调“只返回文件路径”?因为之前它跑完会把整段代码完整打印回对话里,一个 8 个用例的脚本能占掉 3000 token,吃了大亏。
3.3 executor:驱动管理、失败重试与环境检查
executor 是所有 Skill 里最需要“实操能力”的一个,它不能只靠 Prompt 指挥模型,还得挂上真实工具调用。我的实现方式是给这个 Skill 绑定几个脚本工具:run_shell(command)、read_file(path)、write_file(path, content)。Agent 通过这几个工具去执行命令。
它的核心指令包括四步:
- 检查环境:Web 端确认浏览器与对应 driver 是否已安装且版本匹配;App 端先用
adb devices确认设备在线; - 执行测试:运行
pytest artifacts/test_login.py --tb=short -q,加--tb=short是为了控制日志长度; - 收集产物:不管成功还是失败,把输出重定向到
artifacts/logs/,并截图保存到artifacts/screenshots/; - 汇总结果:把 pytest 的 ExitCode 转为 JSON 结果,写入
artifacts/results.json。
失败重试策略我踩过不少坑,总结下来规则可以是这样的:环境类失败(driver 启动失败、端口占用、设备断连)自动重试 1 次;断言失败和元素定位失败不重试,直接进入失败分析环节。为什么断言失败不重试?因为 UI 自动化里断言失败往往是功能 bug,重试只会浪费时间。
这个 Skill 的 Prompt 我加了一段自我检查逻辑:“执行完命令后,确认 exit code 为 0 才算成功。如果 pytest 返回非 0,不要擅自改脚本,先记录失败状态,留给 failure-analyzer 处理。”一开始没加这句的时候,模型会在用例失败后自己动手改测试代码,然后重新跑,改得面目全非还不告诉你,非常坑。
3.4 failure-analyzer:定位根因,但禁止编造
失败分析是 AI 提效最明显的一环。以前出问题了,人得打开日志、翻截图、猜原因。现在 Agent 能一口气把这些材料看完,给你一个分类结论。但这个环节也是最容易“AI 一本正经胡说八道”的,必须给模型立好规矩。
这个 Skill 的输入是results.json+ 对应日志 + 截图。它在 Prompt 里被要求把失败原因归为以下五类:
| 失败分类 | 典型表现 | 处理建议 |
|---|---|---|
| 元素定位失败 | NoSuchElementException / timeout | 检查选择器是否动态、是否在 iframe 中 |
| 页面加载超时 | Page load timeout | 检查网络、环境、接口响应 |
| 断言不通过 | AssertionError / expect 失败 | 比对预期与实际值,看业务逻辑 |
| 脚本自身错误 | AttributeError / NameError | 检查代码逻辑、依赖缺失 |
| 环境问题 | driver 崩溃、设备离线 | 检查驱动版本、设备连接 |
Prompt 里有两条硬性约束。第一,优先看截图和日志最后 20 行,不要把整个日志丢给模型。第二,如果分析不出根因,必须明确写“需要人工确认”,严禁为了凑结论编一个原因。我见过模型把一次环境问题一本正经地分析成“登录按钮文案变化导致断言失败”,原因就是它只看了代码没看截图,又不好意思说不知道。
实操里这个 Skill 的输出我设计成failure_analysis.md,格式固定为“失败用例 ID → 失败分类 → 证据(截图/日志片段) → 根因分析 → 修复建议”。这个文件既给 AI 自己看,也会被 report-builder 引用到报告里,给开发同学做参考。
3.5 report-builder:聚合结果、生成报告、推送通知
最后一个环节是报告生成。很多团队到了这步就把结果直接贴到群里,一张丑丑的文本截图。实际上模型在这里能做的远远不止贴日志,而是值得“重新组织信息”。
我的 report-builder 指令包含三件事:
- 读取
results.json、failure_analysis.md、截图等产物,生成一份 Markdown 格式的测试摘要:包含总用例数、通过数、失败数、通过率、耗时、失败列表及原因; - 使用 HTML 模板文件渲染一份带样式的报告,报告头部放通过率,主体放失败用例详情和截图缩略图;
- 将 HTML 转为 PDF,通过 Webhook 推送到企业微信或钉钉群。
关键在第一步。AI 的价值不是把 results.json 里的字段原样抄一遍,而是写出类似“本次共执行 6 个用例,通过 5 个,通过率 83.3%。失败的 TC002 经分析是环境问题导致,非业务功能缺陷”的人类可读摘要。后面再用模板把摘要和明细拼成正式报告。
HTML 转 PDF 的方案我用了两种:轻量报告用 Playwright 的page.pdf()直接渲染;如果需要带监控趋势,比如把历史多轮通过率做成曲线,我会上报数据到 Grafana,再用 Grafana 的截图导出 PDF 拼进报告。这个组合实测下来比较稳,美观度也能达到给领导看的水平。
4. Skill 工程化:SKILL.md 结构、描述写法与调试方法
4.1 一个标准 Skill 的文件结构
Skill 不是只有 SKILL.md 一个文件。我的每个 Skill 都按固定目录组织,以 report-builder 为例:
skills/report-builder/ ├── SKILL.md ├── scripts/ │ ├── render_report.py │ └── send_webhook.py ├── templates/ │ ├── report.html.j2 │ └── report.md.j2 └── references/ └── grafana_dashboard.jsonSKILL.md是入口,里面写清楚何时该用这个 Skill、底层怎么操作、输入输出在哪里。scripts/放的是可以被 Agent 调用的现成工具脚本。templates/放报告模板。这样模型拿到 Skill 后,不需要凭空想“报告长什么样”,只需要往模板里填数据。
4.2 desc 怎么写,Agent 才不会乱调用
SKILL.md 的 YAML Frontmatter 里description字段是最重要的。它是 Agent 决定“要不要调这个 Skill”的唯一依据。写得模糊,Agent 就会在错误的时机调用。
以 failure-analyzer 为例,我的 desc 写作:
分析 UI 自动化测试失败结果,定位失败根因并给出修复建议。 仅当 artifacts/results.json 已存在且包含失败用例时使用; 如果所有用例通过,无需调用此 Skill。这段描述只有两句话,但信息密度很高。第一句说明功能,第二句是精确的触发条件,并且反着告诉 Agent 什么情况不用调。后一句话往往比第一句更管用。
我对比过不写触发条件的版本:Agent 在用户说“帮我跑一下测试,顺便看看有什么问题”时,会先调 failure-analyzer,因为用户提了“问题”两个字。加了后半句之后,它就会先判断结果文件里有没有失败,再决定调用,准确率高很多。
4.3 调试 Skill 的两类实用技巧
第一个技巧是“干跑模式”。我在每个 Skill 的“测试环境”里都加了同样的开关:如果DRY_RUN=true环境变量存在,只输出“将要执行的动作和参数”,不真正执行测试、不生成文件。平时我写完一个新 Skill,先开干跑模式,用几条典型指令验证调度逻辑对不对,确认无误再改成真实执行。这个模式省了我大量时间精力,因为很多错误根本不在代码里,而在模型对 Skill 理解的偏差上。
第二个技巧是给每个 Skill 记录调用日志。我在 SKILL.md 里加了一条通用指令:“每次调用结束后,将调用时间、输入摘要、输出文件路径追加写入artifacts/skill_log.csv。” 一旦 Agent 链路出了诡异问题,我只需要打开这个 CSV 就能还原它每一步干了什么,再也不用靠猜。
有次线上链路突然全挂,就是这个日志救了我。打开发现 executor 连续调了三次,第一次是执行,第二次是 Agent 误以为没跑成功又跑了一次,第三次是它把第一次的截图删了。排查起来一目了然。
4.4 什么时候该用代码工具,什么时候纯 Prompt 就够
不少人在做 Skill 时容易陷入一个误区:什么都让模型自己发挥。实际上有一个简单的判断标准——如果这个环节的结果“确定性要求高”,就必须绑代码工具;如果结果“开放性要求高”,可以让模型自由生成。
比如 executor 执行 pytest,exit code 是多少、日志存哪里,这些必须是确定性的,不能让模型临场发挥。report-builder 里的 HTML 渲染也最好用模板脚本控制。反之,spec-parser 把自然语言拆成用例、failure-analyzer 分析根因,这类任务没有标准答案,就用 Prompt 驱动模型。
按这个标准,我的 5 个 Skill 里,spec-parser 和 failure-analyzer 是偏纯 Prompt 的,script-generator 是 Prompt + 代码生成的混合,executor 和 report-builder 主要是脚本工具 + 少量 Prompt 调度。
5. 常见问题与排查技巧实录
整套体系跑了一段时间,也积累了不少“翻车现场”的处理经验。我挑几个高频问题整理成速查表,供大家直接对号入座:
| 现象 | 根因 | 排查与解决 |
|---|---|---|
| 浏览器启动后秒退,报 session not created | Chrome 自动升级,driver 版本不匹配 | 检查 chromedriver 与浏览器版本,改用 WebDriver Manager 自动匹配,在 executor 脚本里加版本校验 |
AI 生成的 xpath 全是//*[@id="app"]/div[3]/...,一改版就挂 | 模型偏好用绝对路径或动态 id | script-generator 的 Prompt 明确禁止使用动态 id,要求优先按 role/placeholder/text 定位 |
| Agent 在没执行测试时就直接去生成报告 | report-builder 触发条件没写清楚 | 在 SKILL.md 里增加“仅当 results.json 存在时调用”的条件 |
| 长流程跑到第 4 步突然开始乱来 | 上下文被中间产物撑爆,模型丢失前序判断 | 严控 skill 输出内容长度,强制只返回文件路径和摘要,重要数据全部走文件流转 |
| pytest 并发执行时端口冲突 | 多个用例同时启动同一服务 | executor 里加一个并发控制策略,同一时间只跑一个危险用例,或用独立端口参数 |
| 报告里的通过率跟实际执行对不上 | report-builder 直接读取 pytest 原始输出而不是标准化 results.json | 统一以 executor 写的 results.json 为准,删除对终端文本的依赖 |
| Appium 始终连不上 Android 设备 | adb 服务状态异常或设备离线 | executor 执行前先跑adb devices,设备不存在则提示重启 adb 服务,不要盲目重试 |
这些问题的共同点,大多数不是模型能力不够,而是 Skill 的边界和触发条件没设计好。把它们一一修正之后,整条链路的稳定性会有非常明显的提升。
6. 落地效果与后续还能怎么扩展
这套五个 Skill 的组合跑通之后,最直观的收益是“从需求到报告”的耗时大幅缩短。举个例子,一个登录加下单的中型流程,拆成 6 条用例,过去测试工程师从头写脚本、跑环境、定位失败、拼报告,平均要 50 分钟到一个小时。现在丢给 Agent 一条指令,平均 10 到 15 分钟能拿到一份带失败根因和修复建议的 PDF 报告。模型偶尔生成的代码还要人工 review 微调,但整体节奏完全不一样了。
后续我计划做三件事。一是接一个页面元素知识库,让 script-generator 生成选择器时参考历史沉淀,减少踩重复的坑。二是引入视觉能力,让 failure-analyzer 不只看日志,还通过截图做视觉断言,比如判断按钮是否被遮挡、样式是否错乱。三是把多轮执行结果存起来,让报告不仅显示本次情况,还能带出连续几轮通过率的变化趋势,给团队做质量看板用。
如果你也想在自己的项目里落地这套玩法,我的建议是别一上来就把 5 个 Skill 全部搭完,先把最核心的 3 个跑起来:spec-parser、script-generator、executor。这三者能打通从需求到执行的链路,价值已经非常明显。等稳定了,再加上 failure-analyzer 和 report-builder,逐步补全整条流水线。实测下来,这种小步快跑的方式,比一次到位的试错成本低太多。