最近大半年,我一直在推进AI在测试团队里的真实落地,前后捣鼓了不少方案,最后反而是这个看起来最简单的“Skills包落地模板”跑出了效果。这里说的AI测试,不是拿ChatGPT写几条用例交差,而是让AI真正参与测试执行:它自己看页面、自己调用接口、自己判断结果对不对。要实现这一步,光有模型不够,还得有一整套可复用的、能被AI调用的测试能力封装,这就是Skills包要做的事。
我写这篇东西,就是想把这套模板完整拆开来讲。适合谁看?正在做AI测试平台、AI测试提效、或者准备AI测试工程师面试的人,都应该会有收获。里面没有玄乎的理论,全是能直接抄走的目录结构、配置写法、执行逻辑,以及我踩过的一些坑。
1. 为什么测试团队需要一套Skills包模板
1.1 从“问答式AI”到“执行式AI”的关键一跳
很多团队所谓的AI测试,其实停留在“对话式”阶段:把需求丢给大模型,让它生成测试用例,然后人再手动去执行。这个阶段当然有价值,但提效天花板很低,因为模型只参与了“思考”,没参与“执行”。真正的AI测试提效,应该让模型能操作浏览器、能发起接口请求、能读写测试数据、能比对预期结果,这就是“执行式AI”的能力。
但要达到这个状态,第一个拦路虎就是:大模型本身不具备操作工具的能力,得给它配一套“手和脚”。Skills包就是这套手脚的标准封装。它把测试执行中重复、通用、可标准化的动作(比如“点击页面元素”“读取接口响应”“写入测试报告”)抽象成一个个可被模型调用的单元。做AI测试的核心工作,从“写代码”变成了“定义这些单元并组织它们”。
我在早期尝试时踩过一个典型弯路:每条测试需求都单独写提示词,让模型自由发挥。结果模型一会儿用这种方式定位元素,一会儿用另一种方式,完全不可控。后来换成Skills包模板后,所有执行动作都由固定逻辑接管,模型只需要负责“决策”和“组装”,稳定性立刻上了一个台阶。
1.2 模板要解决的核心问题
测试Skills包模板不是一堆代码的简单堆积,它要回答几个实际问题:
第一,模型怎么知道有什么能力可用?这需要一份“能力清单”,也就是Manifest文件,里面写清楚每个Skill叫什么、接收什么参数、输出什么结果。模型在规划任务时,会先扫描这份清单,理解自己能调用哪些能力,才能生成有效的执行计划。
第二,执行完动作之后,模型怎么知道结果对不对?这就需要把执行结果结构化。比如UI自动化测试点击了一个按钮,返回的不能是“点击成功”这种废话,必须包含页面变化、元素状态、截图路径等结构化信息,模型才能基于这些信息判断测试是否通过。
第三,多个Skill组合时怎么保持上下文连贯?一个完整的测试用例往往不是一次调用就能完成的,可能是“打开页面 -> 输入表单 -> 提交 -> 校验结果”这样的链路。模板必须定义好链路间的数据传递方式,也就是上下文管理机制。
这套模板就是把上面三个问题的答案固化成文件结构、配置格式和代码约定。新项目拿过去,不用从零设计,替换业务相关的部分就能跑起来。
2. 理解Skills包的本质:它到底封装了什么
2.1 Skills包在AI测试链路中的位置
先说清楚整体架构,方便后面展开。一个完整的AI测试链路大致是这样:
- 用户输入测试目标(比如“测一下登录功能是否正常”)
- 调度层(Agent)接收目标,拆解成任务计划
- 任务计划里的每一步,对应调用一个Skill
- Skill执行具体动作(操作浏览器、发请求、读数据)
- 执行结果回传给Agent
- Agent综合所有结果,给出最终测试结论
Skills包就是中间那个“Skill执行层”的具体实现。它向上对Agent暴露标准化接口,向下对接各种测试工具和框架。这个位置的封装质量,直接决定了整个AI测试系统的上限。
有人问我,这个和传统的关键字驱动测试有什么区别?区别在于“决策权”的归属不同。关键字驱动里,关键字和用例顺序都由人预先定义,执行器是死板的翻译官;而在AI测试中,Skill虽然也是封装动作,但调用顺序由模型动态规划,模型会根据实际执行情况实时调整策略。比如测试登录时发现验证码识别失败,模型可以立即切换成“等待人工介入”的Skill,而不是傻乎乎地继续执行原计划。
2.2 三个核心层次:任务定义、执行调用、结果回传
我把Skills包的内容拆成三层,模板也是按这个逻辑组织的。
第一层是任务定义层,对应每个Skill的说明文件。里面要写清楚:这个Skill是干嘛的、什么时候该用、什么时候不该用、需要哪些参数、参数格式怎么定义。这层服务的是模型,模型通过阅读这些说明来理解能力边界。写这层文档时特别考验功力,太粗了模型理解不了,太细了又限制模型的灵活性。
第二层是执行调用层,对应真实的实现代码。每个Skill背后都有一段真实可执行的逻辑,比如“根据CSS选择器定位元素并点击”这个Skill,代码里就是完整的Selenium或Playwright调用封装。这层面向技术实现,核心追求是稳定性和错误处理。我在模板里固定了异常捕获和截图存档机制,确保任何执行异常都有据可查。
第三层是结果回传层,对应统一的输出格式。我设计了一套JSON结构,包含状态、数据、耗时、截图路径、错误信息等字段。所有Skill都返回这个结构,模型拿到后能直接解析。关键是结构要简单,字段要稳定,模型处理起来才不容易出错。
2.3 模板化的取舍:什么该固化,什么该留活口
做落地模板最大的坑,就是把所有东西都固化死,导致换个项目就废掉。我经过几轮迭代,总结了一套取舍标准:
固化的部分包括:目录结构、Manifest格式、结果返回格式、日志和截图规范、基础能力封装(浏览器操作、请求发送、数据读写)。这些属于所有测试场景共通的部分,固化它们能大幅减少重复建设。
留活口的部分包括:业务页面对象定义、断言规则、数据源配置、环境地址、测试账号。这些每个项目都不一样,模板里只提供示例和约定,具体内容由使用者填充。
举个例子,模板里的“点击元素”Skill,本身就支持通过参数传入任意选择器,但某个特定项目的登录按钮,由项目自己的配置来定义。这样既保证了通用性,又保留了项目扩展的余地。
我用一句话总结这套设计哲学:“Shell固定,Payload可变”。Shell是模板固化的骨架,Payload是每个项目注入的血肉。
3. 落地模板完整拆解:目录结构、Manifest与执行逻辑
3.1 一套可以直接抄的目录结构
这是我在多个项目中验证过的目录结构,你直接照着建就行:
ai-test-skills/ ├── manifest.yaml # 全局能力清单,Agent入口先读这个 ├── skills/ │ ├── ui_click/ # 单个Skill目录 │ │ ├── SKILL.md # Skill说明文档,给模型看的 │ │ ├── action.py # Skill实现代码 │ │ └── requirements.txt # 依赖(独立Skill可单独声明) │ ├── ui_input/ │ ├── ui_assert_text/ │ ├── api_get/ │ ├── api_post/ │ ├── data_query/ │ └── report_append/ ├── templates/ │ ├── skill_template/ # 新建Skill时的脚手架 │ └── agent_prompt/ # 编排提示词模板 ├── core/ │ ├── runner.py # Skill执行调度器 │ ├── context.py # 上下文管理(多Skill数据传递) │ ├── result_schema.py # 统一结果结构定义 │ └── logger.py # 日志与截屏处理 ├── config/ │ ├── environments.yaml # 环境地址配置 │ └── accounts.yaml # 测试账号配置(走密钥管理) ├── test_data/ │ ├── login_cases.json # 测试数据集 │ └── api_cases.yaml └── output/ └── reports/ # 测试报告输出这套目录的设计逻辑是:模型通过manifest.yaml和各个SKILL.md来理解系统能力,调度器通过runner.py来实际执行,上下文通过context.py在多个Skill调用之间传递数据,最终所有产物统一落到output目录。
3.2 manifest.yaml里每个字段的用意
Manifest是整个Skills包的心脏,Agent启动时第一个读取的文件就是它。我见过不少团队在这个文件上偷懒,随便写几行描述就完事,结果是模型经常用错Skill。正确写法是有讲究的。
来看一个实例:
version: "1.0" agent_instruction: | 你是AI测试执行引擎,负责根据用户测试目标,规划并执行测试动作。 你需要先调用ui_open打开目标页面,再按测试需要调用其他技能。 每次动作完成后,要检查返回结果中的status字段,status为fail时不要继续执行后续动作。 skills: - name: ui_open description: 打开一个URL地址,等待页面加载完成。 when_to_use: 测试开始时,或需要跳转到新页面时使用。 when_not_to_use: 页面已经处于目标地址时,不要重复调用。 parameters: - name: url type: string required: true description: 目标页面完整地址,以http或https开头。 returns: - field: page_title type: string description: 页面加载完成后的标题 - field: screenshot type: string description: 页面截图保存路径 - name: ui_click description: 根据CSS选择器点击页面元素。 when_to_use: 需要触发按钮、链接、菜单点击时使用。 when_not_to_use: 目标元素未出现在页面上时,不要调用,先使用ui_wait或ui_assert_element。 parameters: - name: selector type: string required: true description: CSS选择器,定位唯一元素。优先使用id或data-testid等稳定属性。 - name: wait_seconds type: number required: false default: 1 description: 点击后等待时间,用于等待页面响应。 returns: - field: clicked type: boolean description: 是否成功点击 - field: screenshot type: string description: 点击后页面截图路径每个字段都不是废话。“when_to_use”和“when_not_to_use”是在大量实践中总结出来的关键设计。模型非常依赖这两个字段来判断调用时机,写得好能减少一半以上的误调用。我还在文件顶部加了agent_instruction,相当于对整个Agent下达了一个总纲性的提示词,这个做法非常有效,建议保留。
3.3 执行脚本如何与测试框架对接
Skills包不是凭空执行的,它下面得挂一个真实可用的自动化框架。我建议直接用Playwright或Selenium作为底层驱动,模板里默认提供了Playwright的实现,因为它的API更现代,处理等待、截图、追踪都很方便。
关键的设计在“对接”这个动作上。模板里做了一个统一的执行入口,让Agent不管调用哪个Skill,最终都走同一个调度器:
# core/runner.py import importlib import traceback from core.context import Context from core.result_schema import SkillResult class SkillRunner: def __init__(self, context: Context): self.context = context def execute(self, skill_name: str, parameters: dict) -> SkillResult: # 动态加载Skill实现模块 try: module = importlib.import_module(f"skills.{skill_name}.action") handler = getattr(module, "handle") # 执行前把上下文注入,执行后把结果写回上下文 self.context.set("last_skill", skill_name) result = handler(parameters, self.context) self.context.record(f"{skill_name}_result", result) return result except Exception as exc: # 统一异常捕获,所有Skill异常都转成结构化结果,避免Agent误解 return SkillResult( status="error", error_message=str(exc), traceback=traceback.format_exc() )这个runner的关键是统一入口和统一异常处理。所有Skill异常都被转成结构化的error结果,而不是直接抛出堆栈。因为模型拿到堆栈信息反而容易混乱,拿到结构化的错误描述反而会知道下一步该怎么办。
每个Skill的具体实现也有固定模板,我拿ui_click的action.py来做示例:
# skills/ui_click/action.py from playwright.sync_api import Page def handle(params: dict, context): selector = params.get("selector") wait_seconds = params.get("wait_seconds", 1) # 从context中获取Page实例 page: Page = context.get_browser_page() if page is None: raise RuntimeError("浏览器页面未初始化,请先调用ui_open") # 等待元素可见再点击,避免直接点击报错 page.locator(selector).first.wait_for(state="visible", timeout=5000) page.locator(selector).first.click() page.wait_for_timeout(wait_seconds * 1000) # 截图存档 screenshot_path = context.save_screenshot(f"click_{context.step_count}") return { "status": "success", "clicked": True, "screenshot": screenshot_path, "page_title": page.title() }这里的context是一个贯穿全链路的上下文对象,里面管理着浏览器实例、截图、步骤计数等公共资源。不同Skill之间通过context共享状态,而不用每次调用都重新创建浏览器,这是性能优化的关键。
3.4 测试数据的注入与管理
测试数据是AI测试里特别容易出问题的地方。模型不了解测试环境的数据状况,经常拿着不存在的数据去测,得出的结论自然不可信。模板里专门设计了一套数据注入机制。
最简单可靠的方案是:在Manifest里增加一个data_query的Skill,通过它对外暴露数据查询接口:
- name: data_query description: 查询指定测试数据集中符合条件的记录。 when_to_use: 需要构造测试数据、获取测试账号或查询预期值时使用。 parameters: - name: dataset type: string required: true description: 数据集名称,例如login_cases。 - name: filters type: object required: false description: 过滤条件,例如{"scenario": "locked_account"} returns: - field: records type: array description: 查询到的记录列表数据集放在test_data目录下,用JSON或YAML管理。例如登录测试的数据集长这样:
[ { "scenario": "normal_login", "username": "tester_normal", "password": "normal123", "expected": "登录成功" }, { "scenario": "locked_account", "username": "tester_locked", "password": "anypassword", "expected": "账号已被锁定" } ]模型执行测试时,先通过data_query从数据集里选一条用例,拿到具体数据再执行后续动作。这样设计的好处是数据和执行逻辑解耦,非技术人员也能维护测试数据。
实际操作中遇到一个问题:模型可能会不分青红皂白把所有数据都执行一遍,造成测试时间过长。解决方案是在data_query的描述里写清楚“只查询符合当前场景的1-2条记录”,实践中确实有效。
4. 从模板到项目:一个Web端回归测试的真实落地过程
4.1 第一步:定义需求范围与输入
理论说完了,接下来用一个真实案例串一遍整个流程。假设现在要测一个Web后台的“创建用户”功能,涉及表单填写、提交按钮、成功提示三个关键点。
传统做法是把这三个点写成三条用例,然后人去执行。用我们的Skills包模板,任务变成了:Agent读需求、规划动作序列、按序调用Skill、汇总报告。
第一步是明确输入。给Agent的原始需求要尽可能清晰:
测试目标:验证后台“创建用户”功能正常。 操作路径:使用管理员账号登录后,点击“用户管理”,点击“新建用户”按钮, 填写用户名、邮箱、角色,点击“提交”,验证出现“创建成功”提示。 预期结果:提交后页面出现成功提示,且在用户列表中能看到新用户。这段描述直接喂给Agent。Agent会结合manifest里定义的Skills,规划出大致执行步骤:登录 -> 进入用户管理 -> 点击新建用户 -> 填写表单 -> 提交 -> 验证提示 -> 查询列表。每一步对应一个或几个Skill调用。
4.2 第二步:让模型学会看页面
这是UI自动化测试里最有挑战的一环。传统自动化靠人写好选择器,AI测试里模型必须自己找元素。
我的方案是给模型提供一个ui_inspect的Skill,作用是把当前页面的可交互元素抓出来,整理成带选择器的结构化列表:
- name: ui_inspect description: 获取当前页面上所有可交互元素(按钮、输入框、链接)的列表,包含元素标识和关键属性。 when_to_use: 不确定目标元素的选择器时使用,或元素定位失败时用来探索页面。 parameters: - name: max_elements type: number required: false default: 30 description: 最多返回元素数量,防止页面元素过多导致上下文超长。 returns: - field: elements type: array description: 元素列表,每项包含tag、text、css_selector等。action.py里通过Playwright快速收集页面可交互元素并生成稳定的选择器。生成选择器的优先级是:id >>1. 调用ui_open,打开后台登录页 2. 调用ui_input_placeholder,找到“用户名”输入框并输入管理员账号 3. 调用ui_input_placeholder,找到“密码”输入框并输入密码 4. 调用ui_click,点击“登录”按钮 5. 调用ui_wait_for_text,等待“用户管理”菜单出现 6. 调用ui_click,点击“用户管理” 7. 调用ui_inspect,查看当前页面的可交互元素 8. 调用ui_click,点击“新建用户”按钮 9. ...依次完成表单填写和提交
每一步调用都靠context连接。比如第5步的ui_wait_for_text,它的执行结果(如元素是否出现、出现后的页面截图)会被写进context。模型判断成功后才会继续走第6步。这个“一票否决”机制很关键,一旦某一步返回fail,后续步骤自动取消,Agent会尝试换路径兜底或直接报告失败。
你可能已经注意到了,这里我用了ui_input_placeholder这个Skill,它可以按输入框的占位符文本定位元素。这个Skill同样基于element selectors,但使用时更容易被模型理解(“带‘用户名’提示的输入框”比“input[name=username]”对模型来说更自然)。模板中我内置了多个这类语义化Skill,它们极大地降低了模型对传统定位语法的依赖。
4.4 第四步:让AI输出结构化结果
所有动作执行完之后,最关键的一步是让模型输出结构化的测试结果,而不是一段自由文本的总结。我在模板里定义了一个report_append的Skill,所有测试结论最后都通过它写入统一的报告文件。
- name: report_append description: 向测试报告追加一条结果记录。 when_to_use: 每个测试场景结束后,需要记录测试结论时使用。 parameters: - name: case_name type: string required: true description: 用例名称 - name: status type: string required: true enum: ["pass", "fail", "blocked", "error"] description: 用例执行结果 - name: evidence type: string required: false description: 证据信息,如截图路径、请求返回码等 - name: notes type: string required: false description: 补充说明,如缺陷描述、异常信息这样设计有什么好处?第一,报告格式统一,后续统计、推送、集成都能直接对接;第二,模型无法在结论上“胡说八道”,因为Status字段有枚举限制;第三,每个结论都要求填evidence,强制模型给出证据,减少AI“幻觉式通过”。
跑一轮“创建用户”测试后,报告文件大概长这样:
[ { "case_name": "创建用户-正常路径", "status": "pass", "evidence": "页面截图: output/reports/click_3.png, 用户列表查询到新用户", "notes": "提交后1秒内出现创建成功提示" }, { "case_name": "创建用户-邮箱格式错误", "status": "fail", "evidence": "页面截图: output/reports/click_7.png", "notes": "点击提交后出现错误提示:邮箱格式不正确,但按钮无任何反馈" } ]到这里,一个Web自动化测试的完整闭环就打通了。Agent可以完全自主地完成从理解需求到输出报告的整个流程。
5. 常见问题与排查技巧实录
5.1 模型不按预期执行怎么办
这是我在实践里遇到最多的一类问题。模型明明读了Manifest,但在实际调用中还是会出现偏差,比如不调用任何Skill直接给出代码、调用不存在的Skill名称、参数格式严重错误等。
第一步排除的是Manifest问题。重新检查描述是否明确、when_not_to_use是否写清楚。很多时候模型不执行Skill,是因为它根本不理解当前该用什么,比如页面还停在登录页,却直接调用了点击“创建用户”按钮的Skill。我给每个Skill补充了更精确的前提说明后,这类情况大幅减少。
第二步是用搜索与约束结合的方式修复。在Agent入口的提示词里明确约束“你只能调用manifest.yaml里列出的Skill,禁止直接编写代码完成操作”。这条约束在不支持强制工具调用的模型上有奇效。
如果还是不行,那就得考虑底层模型能力问题了。一些模型在多步规划和工具调用上天生较弱,这种情况下应引入大模型加小模型的混合架构:强模型负责规划,弱模型负责单一动作判断,成本也能控制住。
5.2 提示词不稳定怎么处理
AI测试中提示词的作用被严重低估了。同样的模板,在不同模型、不同版本上表现差异很大。我的经验是提示词要做“版本管理”,每轮调整保留快照,避免调着调着把好配置调丢了。
另外推荐一个技巧:把提示词按“不动区”和“可变区”拆开。不动区放系统性约束(如输出格式、工具调用纪律),写死不动;可变区放场景性指导(如本次测试目标、测试数据),每次动态填充。这样既稳定又灵活。
我封装了一套提示词模板,放在templates/agent_prompt/下,结构是system_prompt.md和user_prompt.md两个文件。system_prompt负责稳定约束,user_prompt由Agent运行时动态生成。改任何一个场景需求,都不需要动system层。
5.3 调用超时与资源占用
AI测试跑起来后,另一个现实问题是慢和费。一次全流程调用可能要一两分钟,如果测试用例多,整个回归周期不短。资源占用上也容易失控,多个浏览器实例并行时,内存直接拉满。
控制手段有几个。第一,给每个Skill执行设置超时上限(比如UI操作单次不超过20秒),超时后自动返回error并截图,不无限等待。第二,设置全局并发上限,默认1-2个并发任务,避免浏览器实例群同时爆炸。第三,长时间运行任务自动开启浏览器复用(context复用同一个浏览器实例),减少新建窗口的开销。
实际过程中我还发现一个细节:Playwright的trace(录制轨迹)虽然调试价值很高,但会把日志文件撑到几十MB,建议默认关闭,只在调试阶段开启。
5.4 数据格式验收与回传规范
最后讲一个很多人忽略的问题:Skill返回的数据结构不一致导致Agent理解混乱。比如有的Skill返回数组,有的返回对象,有的字段叫title有的叫page_title,模型就很容易抓狂。
模板的结论是:所有Skill返回的都是一等对象。即便只返回一个布尔值,也要包一层标准结构(status、data、timestamp)。这个结构由core/result_schema.py统一约束,每个Skill写完后跑一遍schema校验,不通过就发不了版。
另外,凡是返回列表类数据的Skill,必须明确给出一次性返回的上限(如最多返回20条),并预估token长度。否则一旦数据量大,模型上下文立刻爆炸,后续所有操作全乱套。我在ui_inspect和data_query两个Skill里都写了这个限制,保证即便测试数据有几千条,模型当前上下文里也只看到寥寥几条。
这套“Skills包落地模板”,是我在几个项目中不断踩坑后总结出来的产物。它不一定完美,但至少证明了一条路是走得通的:AI测试的落地,与其追求大而全的平台,不如先把能力封装这层做实,让模型有一双稳定、可控、可反馈的“手”。如果你也在做AI测试,建议先别急着让模型理解复杂业务,先把它脚下这层Skills包打好。地基稳了,上面长什么都容易。