告别提示词乱飞,用状态机和护栏让AI稳定写出可维护的UI自动化测试
前言:你也被AI“坑”过吗?
让AI帮忙写一个UI自动化测试,它秒出一段代码,你满怀信心跑起来——结果直接报错。
更“骚”的是,有些AI为了通过测试,会悄悄修改已有的Page对象,害得其他10个用例一起挂。
为什么会这样?
因为你把「需求分析 → 代码实现 → 验证修复」三种不同能力全扔给同一个AI。它没有足够的上下文,也没有自我纠错的机制。
本文分享一套经过实战验证的 Harness设计模式:把AI装进“马具”里,用多Agent + 状态机 + 产物契约,让它稳定输出符合工程规范的UI自动化测试。全文手把手教你从零设计这套架构,并附完整代码模板。
一、什么是Harness设计模式?
Harness原意是“马具”——一套约束装置,把马的力量精确引导到正确方向。
在AI工作流中,Harness就是一套结构化的约束:
- 不是限制AI,而是给它明确边界
- 让AI的行为可预测、可审查、可重放
Harness的四个核心要素
| 要素 | 作用 |
|---|---|
| ① 角色边界 | 每个Agent只做一件事 |
| ② 状态机 | 定义流程走向,防止跳步 |
| ③ 产物契约 | Agent间通过文件传递信息,不靠对话记忆 |
| ④ 护栏规则 | 明确禁止清单,防止AI“走捷径” |
为什么UI自动化特别适合Harness?
- 步骤多:分析、实现、验证需要不同能力,天然适合拆分
- 有现成质量标准:分层规范、命名规范、AI定位规范都可被机器检查
- 失败是常态:需要结构化的失败→分析→修复→重试循环
二、为什么一个AI搞不定测试?
写一个合格的UI自动化测试,需要三种完全不同能力:
| 阶段 | 核心能力 | 常见问题 |
|---|---|---|
| 分析 | 读需求、扫代码库、判断复用边界 | “现有代码能复用多少?” |
| 实现 | 按规范写代码、遵守分层约束 | “怎么写才符合项目架构?” |
| 验证 | 运行测试、看日志/截图、分析根因 | “失败了是哪一步的锅?” |
解决方案:流水线 + 职责分离
用户需求
↓
[总控 Harness] ← 调度 + 状态机
↓
[分析Agent] → _analysis.md
↓
[实现Agent] → 源码变更
↓
[验证Agent] → _verification.md
↓
总控判断 → 完成 或 修复重试
三、整体架构:总控 + 三个Agent
docs/codex/ui-automation/
├── SKILL.md ← 总控(状态机、调度规则)
├── agents/
│ ├── analyzer-agent.md ← 分析Agent prompt
│ ├── implementer-agent.md ← 实现Agent prompt
│ └── verifier-agent.md ← 验证Agent prompt
└── artifacts/
├── _analysis.md ← 分析结果(中间产物)
└── _verification.md ← 验证结果(中间产物)
关键设计:Agent之间不靠对话上下文传递信息,而是读写artifacts/下的文件。
这样信息不会在多轮对话中丢失,而且每一步都可审查。
四、从零设计总控层(Harness)
4.1 总控的五要素
- 角色定义:明确告诉AI它是总控,不要自己做实现
- 子Agent列表:每个Agent的职责和产物
- 状态机:定义流程走向和跳转条件
- 调度细则:每次调用子Agent传什么参数
- 禁止行为:8~10条禁止清单,防止越权
4.2 状态机设计(核心)
START → ANALYZE → IMPLEMENT → VERIFY → JUDGE
↓
├── DONE(通过)
├── IMPLEMENT(architecture_fix) → VERIFY → JUDGE
├── IMPLEMENT(retry_fix) → VERIFY → JUDGE
└── 失败超2次 → 停止并报告
重试计数规则:
- 架构违规 / AI定位规范违规 → 不计入重试(修复后理论上不再出现)
- 测试运行失败 → 计入重试,最多2次
4.3 禁止行为清单(示例)
| # | 禁止 |
|---|---|
| 1 | 跳过分析阶段直接实现 |
| 2 | 实现完成后不调用验证 |
| 3 | 总控亲自写测试代码 |
| 4 | 总控亲自跑测试命令 |
| 5 | 测试失败超过2次后继续自动重试 |
| 6 | Agent之间只口头传递信息,不写入artifact |
五、手写三个Agent Prompt(精华版)
5.1 分析Agent(Analyzer)
唯一职责:把自然语言需求 → 变成实现Agent可直接使用的结构化文档。
最关键要求:让下游Agent不需要再读源码。
输出文件 _analysis.md 必须包含:
# 需求分析结果
## 操作步骤
1. [PageClass] 操作描述
## 代码复用分析
| 步骤 | 现有代码 | 覆盖内容 |
| 需扩展 | 文件 | 新增内容 |
| 需新建 | 文件 | 用途 |
## 代码上下文(Agent2直接使用)
- 需扩展文件的现有代码结构
- 相邻Case的完整代码片段
- 测试文件模板
护栏规则:不执行测试、不启动浏览器、不写实现代码。
5.2 实现Agent(Implementer)
四种运行模式:
| 模式 | 触发时机 | 核心任务 |
|---|---|---|
| first_run | 首次实现 | 只读_analysis.md,按复用计划写代码 |
| review_fix | AI定位规范违规 | 修复Page里的定位描述 |
| architecture_fix | 分层违规 | 把定位逻辑移到正确的层 |
| retry_fix | 测试运行失败 | 读日志/截图,修复具体错误 |
已有函数保护机制(最高优先级)
禁止修改任何已存在的函数,除非该函数明确出现在approved_modifications清单中。
需要修改时必须报告,等待用户确认。被拒绝时改用新增方法(如clickXxxWithOptions)。
代码模板示例(Page层):
async searchByKeyword(keyword: string) {
await this.withFallback({
label: "searchByKeyword()",
cssAction: async () => false, // 强制走AI
aiFallback: async () => {
await this.getAgent().aiInput(
"应用列表页面顶部的「搜索应用」输入框",
{ value: keyword }
);
},
aiOnly: true,
});
}
5.3 验证Agent(Verifier)
两层审查 + 测试执行:
- 架构合规审查:检查测试层/服务层是否出现禁止代码(如
page.locator、.aiTap等),违规则直接输出ARCHITECTURE_VIOLATION,不跑测试 - AI-Only合规审查:确认Page方法使用了
aiOnly: true - 运行测试
- 分类错误 + 截图分析
- 输出
_verification.md
输出格式示例:
# 验证报告
## 状态
[ARCHITECTURE_VIOLATION / AI_ONLY_VIOLATION / ✅通过 / ❌失败]
## 测试结果(仅通过审查时)
- 状态:✅通过 / ❌失败
- 通过/失败用例数:1/0
## 错误详情(仅失败时)
- 类型:selector_not_found / timeout / assertion_failed
- 截图分析:页面处于XX状态,失败在第2步
- 修复方向:增加容器上下文描述
禁止行为:不修改源码、不跳过架构审查、日志禁止截断。
六、实操演示:让AI写出第一个测试
你的输入(自然语言)
帮我写一个测试:进入应用列表,在搜索框输入"autotest",验证结果列表中显示了包含该关键词的应用。
AI自动执行流程
① 分析阶段(自动)
[Analyzer] 扫描代码库...
[Analyzer] 读取最佳实践文档 app-list.md
[Analyzer] 读取相邻case: search-app.test.ts
[Analyzer] 输出 _analysis.md
展示确认摘要(唯一人工确认点):
步骤1: 导航到应用列表 → [复用] navigateToAppList()
步骤2: 输入关键词 → [扩展] 需新增 searchByKeyword()
步骤3: 验证结果 → [扩展] 需新增 verifySearchResult()
请确认是否继续?
② 实现阶段(自动)
实现Agent在AppListPage.ts中新增两个AI-Only方法,并创建测试文件:
// src/tests/app-list/search-app.test.ts
test("搜索应用 - 关键词匹配成功", async () => {
const svc = new AppListService(ctx.page);
await svc.navigateToAppList();
const found = await svc.searchApp("autotest");
expect(found).toBe(true);
}, 180_000);
③ 验证阶段(自动)
- 静态审查 → 通过
- 运行测试 → 通过(或失败则自动修复重试,最多2次)
七、常见问题与进阶技巧
7.1 错误类型速查
| 错误类型 | 根因 | 修复方向 |
|---|---|---|
| selector_not_found | AI描述太模糊 | 加容器上下文、文案、元素类型 |
| timeout | 页面慢/弹窗未出现 | 增加aiWaitFor() |
| assertion_failed | 实际值与预期不符 | 检查断言逻辑或等待时机 |
| import_error | 路径错误或未导出 | 修正导入路径,检查index.ts |
7.2 AI定位描述的好与坏
// ❌ 太模糊
await aiTap("确定按钮");
// ✅ 四要素:容器 + 位置 + 文案 + 类型
await aiTap("在「添加用户」弹窗底部右侧的「确定」按钮");
7.3 如何迁移到你的项目?
- 定义你的代码分层架构(Verifier用它做静态审查)
- 建立最佳实践文档库(Analyzer按关键词读取)
- 更新测试目录映射和测试运行命令
- 替换AI方法API(如把Midscene换成你用的库)
八、总结
Harness设计模式不是一套固定代码,而是一种让AI稳定工作的工程方法:
- 用角色边界防止AI越权
- 用状态机防止跳步和无限循环
- 用产物契约让信息可审查、可重放
- 用护栏规则明确禁止“走捷径”
当AI从“万能助理”变成“可调度的流水线工人”,UI自动化测试的生成和维护才能真正落地。
特种兵 + AI 的协作模式已经形成。
与其焦虑被淘汰,不如学会给AI装上Harness,让它成为你的得力干将。
参考资料:本文核心思想与架构设计参考自「霍格沃兹测试学院」公众号系列文章,结合工程实践整理。
如果觉得有用,欢迎点赞、收藏、评论交流~