测试开发之路 万字长文:如何用 harness 的理念设计一个 AI 驱动的 UI 自动化工程。

孙高飞 · 2026年05月08日 · 471 次阅读

目录

  1. 什么是 Harness 设计模式?
  2. 为什么一个 AI 搞不定测试?
  3. 认识这套多 Agent 架构
  4. 从零设计你的总控层(Harness)
  5. 手把手写三个 Agent Prompt
  6. 实操:让 AI 帮你写第一个测试
  7. 常见问题与进阶技巧

第 0 章:什么是 Harness 设计模式?

在星球的过往文章中,其实已经介绍了很多编写 skill 的技巧,而把他们组合起来,其实就是 Harness 了。 其实 Harness 没有一个业界规定好的,固定的规范, 一切让智能体运行的更稳定,高效,准确的方法,其实都算作 Harness 的实践方法。所以 Harness 是一个方法论,而不是一个固定的实现规范。 我在这篇文章中介绍的, 也只是比较常见的一些设计技巧。

0.1 从「马具」说起

Harness 这个词,英文原意是「马具」——套在马身上、控制马匹方向和力量的一套装置。

在 AI 工作流领域,Harness 设计模式借用了同样的比喻:

用一套结构化的「约束装置」,把强大但方向不稳定的 AI 能力,精确地引导到正确的轨道上,让它高效、稳定、准确地完成复杂任务。

AI 本身很强大,就像一匹快马。但如果没有 Harness,它可能:

  • 走错方向(做了你没让它做的事)
  • 中途卸力(跳过某些步骤,比如你希望它执行测试, 但它为了完成任务, 直接跳过测试步骤, 甚至直接修改测试用例,强行通过。我见过一个同事, 他经历过 AI 直接改了需求工单,就为了能通过本次任务)
  • 跑飞了收不回来(进入无限循环或过度生成)

Harness 不是限制 AI,而是给 AI 提供明确的结构,让它的能量用在对的地方。


0.2 Harness 的四个核心要素

我认为一个 Harness 至少应该由四个部分组成:

┌─────────────────────────────────────────────────┐
│                   HARNESS                        │
│                                                  │
│  ① 角色边界    → 每个 Agent 只做一件事           │
│  ② 状态机(工作流) → 定义流程走向,防止跳步          │
│  ③ 产物契约    → Agent 间用文件传递,不靠记忆     │
│  ④ 护栏规则    → 明确禁止清单,防止越权          │
│                                                  │
└─────────────────────────────────────────────────┘

① 角色边界(Role Boundary)

每个参与工作流的 Agent,都只被授权做一件明确定义的事。

没有 Harness 的情况:

你对 AI 说「帮我写测试」,它同时在分析需求、写代码、还顺手改了几个旧文件。你根本不知道它改了什么。

有 Harness 的情况:

分析 Agent 只能读文件、产出分析报告,它没有权限写代码。实现 Agent 只能按照分析报告写代码,它没有权限运行测试。每个 Agent 的边界清晰,行为可预测。

② 状态机(State Machine)

Harness 不是简单的「一步接一步」,而是一张完整的状态转移图,涵盖正常路径和所有异常路径。

正常路径:A → B → C → 完成
异常路径:C 失败 → 分类错误 → 选择修复策略 → 回到 B → C → 再判断
上限保护:失败超过 N 次 → 停止,报告给人类

有了状态机,AI 遇到失败时不会「不知道该怎么办」,也不会无限循环。

③ 产物契约(Artifact Contract)

Agent 之间的信息传递,不能依赖对话上下文(对话会被截断、遗忘),必须通过写入文件来传递。

每个 Agent 的输出格式在 Harness 里预先约定好:

  • 分析 Agent 输出 _analysis.md,格式固定
  • 验证 Agent 输出 _verification.md,格式固定
  • 总控通过读取这些文件来做判断,不依赖 Agent 的口头汇报

这让整个工作流可审查、可重放、可调试

④ 护栏规则(Guard Rails)

Harness 的最后一道防线:明确列出每个 Agent 绝对不能做的事

这不是因为不信任 AI,而是因为 AI 在面对复杂问题时,有时会「走捷径」——比如实现 Agent 为了让测试通过,悄悄修改了一个已有的 Page 方法,破坏了其他 10 个测试。护栏规则明确禁止这种行为。


0.3 Harness vs 普通提示词,差别在哪?

很多人用 AI 辅助工作时,习惯写一段很长的提示词,把所有要求都塞进去。这和 Harness 有什么区别?

维度 普通提示词 Harness 设计模式
结构 单个 Agent,线性执行 多个专职 Agent,状态机调度
信息传递 靠对话上下文(容易丢失) 靠 Artifact 文件(持久可查)
错误处理 失败了重新说一遍 分类错误,选择对应修复模式
越权防护 没有,AI 可能做额外的事 有护栏规则,行为可预测
可维护性 提示词改一处,影响所有逻辑 每个 Agent prompt 独立维护
适用规模 简单、一次性任务 复杂、重复执行的工作流

普通提示词适合「帮我写一段代码」这样的一次性任务。Harness 适合「每次有新需求,都要走完分析→实现→验证流程」这样需要反复、可靠执行的工作流。


0.4 UI 自动化测试为什么特别适合 Harness?

UI 自动化测试有三个特点,让它天然适合用 Harness 来管理:

1. 步骤多,分工明确

分析需求、扫描代码库、写代码、跑测试、分析失败——这五件事的技能要求完全不同,天然适合拆给不同的 Agent。

2. 有现成的质量标准

代码分层规范、AI 定位方式、命名规范——这些都是可以被机器检查的约束,可以直接写进 Verifier Agent 的护栏规则里。

3. 失败是常态,需要结构化的修复流程

UI 测试不可能第一次就 100% 通过。Harness 的状态机可以优雅地处理「失败→分析→修复→重试」这个循环,而不是每次都要人工介入。


0.5 我的这个 UI 自动化测试的 Harness 长什么样?

接下来的章节会教你从零设计下面这套完整的 Harness:

┌──────────────────────────────────────────────────────────┐
│                   UI 自动化 Harness                        │
│                                                           │
│  SKILL.md(总控状态机)                                    │
│       ↓ 调度                                              │
│  [Analyzer]  ──→  _analysis.md  ──→  [Implementer]       │
│                                           ↓               │
│  [Verifier]  ←──  源码变更  ←────────────┘               │
│       ↓                                                   │
│  _verification.md  ──→  总控判断  ──→  完成 / 修复重试    │
│                                                           │
└──────────────────────────────────────────────────────────┘
  • SKILL.md:Harness 的总控,定义状态机和调度规则
  • 三个 Agent prompt:分别定义角色边界和护栏规则
  • 两个 Artifact 文件:产物契约,Agent 间的信息中转站

需要说明的是, 我使用的是字节的 midsence UI 自动化测试框架。 支持在后台配置一个多模态大模型做视觉理解(我使用的是千问 VL3.5)。关于这个框架的底层原理, 使用 API 教程等,我在星球的前几篇文章中都有写过。 大家可以自行去翻阅。

理解了上面这些,你就理解了整篇教程的核心。


第 1 章:为什么一个 AI 搞不定测试?

1.1 先从一次失败的尝试说起

假设你第一次尝试用 AI 写 UI 自动化测试,你对 AI 说:

"帮我写一个测试:打开应用列表,点击新建按钮,填入名称,保存,验证应用出现在列表里。"

AI 可能给你生成了一段代码,看起来像那么回事。但你跑起来——失败了。

问题可能是:

  • AI 不知道你项目里的页面对象叫什么、方法签名是什么
  • 生成的代码把定位逻辑直接写在了测试文件里(违反了你们的分层规范)
  • 生成了 CSS 选择器,但那个元素在你们的 UI 框架里根本没有稳定的选择器
  • 测试结构和你们已有的测试用例风格完全不一样,没法合并

根本原因:你把一件需要「调研 → 实现 → 验证」三种不同能力的事情,全部丢给了一个 Agent,它没有足够的上下文,也没有自我纠错的机制。

1.2 三件事,三种能力

写一个合格的 UI 自动化测试,实际上需要三种截然不同的能力:

阶段 核心能力 问题
分析 读懂需求、扫描代码库、判断复用边界 "现有代码覆盖了哪些步骤?哪些要新写?"
实现 按规范写代码、遵守分层约束、输出可运行文件 "怎么写才符合项目架构?"
验证 运行测试、看日志、看截图、分析根因 "跑失败了是哪里出问题?"

如果让一个 Agent 承担全部三件事,它会在「分析和实现之间反复横跳」,或者「实现完了不去跑验证就宣布完成」。

1.3 解决方案:流水线 + 职责分离

把三种能力分给三个专职 Agent,由一个总控(Harness)按顺序调度。这就是本教程要教你设计的东西:

用户说需求
    ↓
[总控 Harness]
    ↓ 调度
[分析 Agent] → 产出分析报告
    ↓ 确认
[实现 Agent] → 写测试代码
    ↓
[验证 Agent] → 跑测试 + 分析结果
    ↓
判断是否通过 → 通过则完成 / 失败则修复重试

第 2 章:认识这套多 Agent 架构

在本项目里,这套架构已经实现。我们先整体了解它,再学如何自己设计。

2.1 四个核心文件

docs/codex/ui-automation/
├── SKILL.md                  ← 总控指令(Harness),告诉主 Agent 如何调度
├── agents/
│   ├── analyzer-agent.md     ← 分析 Agent 的 prompt
│   ├── implementer-agent.md  ← 实现 Agent 的 prompt
│   └── verifier-agent.md     ← 验证 Agent 的 prompt
└── artifacts/
    ├── _analysis.md          ← 分析结果(Agent 间传递信息的中转站)
    └── _verification.md      ← 验证结果(同上)

关键概念:Artifact(产物文件)

Agent 之间不靠口头对话传递信息,而是通过文件。分析 Agent 把结论写进 _analysis.md,实现 Agent 读这个文件来编写代码;验证 Agent 把结论写进 _verification.md,总控读这个文件来判断下一步。

这样设计的好处:信息不会在多轮对话中丢失,每次都能读到完整上下文。

2.2 状态机:总控的核心逻辑

总控不是随机调度,而是按状态机运行。你可以把它理解成一张流程图:

START
  ↓
ANALYZE(分析)
  ↓ [用户确认]
IMPLEMENT(实现)
  ↓
VERIFY(验证)
  ↓
JUDGE(判断)
  ├── ✅ 通过 → DONE(完成)
  ├── ARCHITECTURE_VIOLATION → IMPLEMENT(architecture_fix) → VERIFY → JUDGE
  ├── AI_ONLY_VIOLATION      → IMPLEMENT(review_fix)       → VERIFY → JUDGE
  └── ❌ 失败(retry <= 2)  → IMPLEMENT(retry_fix)        → VERIFY → JUDGE
                  失败(retry > 2)  → 报告失败,停止

两个关键设计决策:

  1. 只有一个用户交互节点:在分析完成后、实现开始前,展示摘要让用户确认。其余流程全自动。这避免了 AI 频繁打断你。

  2. 错误分类驱动修复模式:验证失败时,不是简单重试,而是先分类错误(架构违规 / AI 定位规范违规 / 测试运行失败),再用不同的修复模式调度实现 Agent。这大大提高了修复的准确率。

2.3 项目的四层代码架构

这套 skill 是为特定的代码分层架构设计的。了解这个架构,才能理解 Agent 的规则是从哪里来的:

tests/(测试层)
  只能调用 ↓
services/(服务层)
  只能调用 ↓
pages/(页面层)
  只能调用 ↓
components/(组件层)

每层的职责:

层级 职责 禁止做的事
tests/ 调用 Service、写 expect 断言 直接操作页面、直接调 AI 方法
services/ 编排业务步骤,调 Page 方法 直接操作 DOM、直接调 AI 方法
pages/ 封装页面交互,用 AI 定位元素 跨页面操作、写 expect
components/ 封装通用 UI 控件 依赖 Page 或 Service

为什么要分这么多层?

当 UI 变了,你只需要改 Page 层,不用动 Service 层和测试层。当业务逻辑变了,你只改 Service 层。分层让变更影响范围最小化。


第 3 章:从零设计你的总控层(Harness)

现在开始实战。假设你要为自己的项目设计一套类似的多 Agent Skill,总控(SKILL.md)怎么写?

3.1 总控的五要素

一个好的总控文件必须包含这五部分:

要素 作用 对应本项目
角色定义 告诉主 Agent 它是总控,不要自己做实现 "你是流程总控,只负责调度"
子 Agent 列表 列出每个子 Agent 的职责和产物 三个 Agent 的表格
状态机 定义流程走向和跳转条件 START→ANALYZE→IMPLEMENT→VERIFY→JUDGE
调度细则 每次调度子 Agent 时传什么参数 spawn_agent 的调用示例
禁止行为 防止主 Agent 越权 8 条禁止清单

3.2 第一步:写角色定义

角色定义是整个文件最重要的一句话。它决定了主 Agent 有没有「越权」的冲动。

反面示例(模糊):

你是一个帮助用户写测试的 AI 助手。

这样写,AI 会自己动手写测试代码,而不是去调度子 Agent。

正面示例(本项目的写法):

## 你的角色

你是流程总控。在这个工作流里,你的职责只有三件事:
1. 调度三个子 Agent 按正确顺序执行
2. 判断每个 Agent 的输出,决定下一步
3. 保证不跳步、不遗漏、不突破重试上限

默认不要在这个工作流里自己做需求分析、直接改测试代码、或自己跑验证命令;这些工作交给子 Agent 完成。

写法要点:

  • 用列表明确列出「你的职责只有 N 件事」
  • 在最后用一行反向约束:「默认不要自己做 X / Y / Z」
  • 越具体越好,不要留模糊空间

3.3 第二步:设计状态机

状态机是总控的大脑。画清楚状态流转,AI 才不会在循环里迷失。

模板框架:

## 状态机```text
START -> [阶段1] -> [阶段2] -> [阶段3] -> JUDGE
                                       |
                                       +-> DONE(成功条件)
                                       |
                                       +-> [阶段2(修复模式A)] -> [阶段3] -> JUDGE
                                       |
                                       +-> [阶段2(修复模式B)] -> [阶段3] -> JUDGE
                                       |
                                       +-> FAILED(失败上限)
​```

### 重试计数

- retry 初始值为 0
- [不计入重试的情况] 不计入重试
- 仅当 [真正的失败条件] 时,retry += 1
- 最多自动重试 [N] 次

关键决策:什么情况不计入重试?

本项目把「架构违规」和「AI 定位规范违规」排除在重试计数之外,原因是:这两类错误是代码质量问题,修复后理论上就消失了,不会反复出现。只有「测试运行失败」才计入重试,因为这可能是环境问题或 AI 定位不准,需要限制次数。

3.4 第三步:定义调度协议

每次调用子 Agent,需要规定「传什么」。这部分要具体到参数级别。

模板:

## 调度总则

### 子 Agent 启动前

1. 读取对应 prompt 文件完整内容。
2. 将 prompt 正文与本轮输入上下文拼接后,通过 spawn_agent 发给子 Agent。
3. 等待子 Agent 完成;必要时用 send_input 补充信息。

### [Agent 名称] 调度参数```
spawn_agent(
  agent_type="[explorer/worker]",
  fork_context=true,
  message=[
    [Agent] prompt 完整内容
    + 用户需求
    + [其他必要上下文]
  ]
)
​```

agent_type 选哪个?

类型 适合场景
explorer 需要大量读文件、搜索代码库(分析 Agent)
worker 需要写代码、执行命令(实现 Agent、验证 Agent)

3.5 第四步:写禁止行为清单

这是防止 AI「越权」的最后一道防线。禁止清单要能覆盖所有可能的「走捷径」行为。

思路:逐一列举每个可能的偷懒路径

## 禁止行为

| # | 禁止 |
|---|------|
| 1 | 跳过 [分析阶段] 直接进入 [实现阶段] |
| 2 | [实现阶段] 完成后不调 [验证阶段] |
| 3 | 在工作流里由主 Agent 直接亲自写测试代码 |
| 4 | 在工作流里由主 Agent 直接亲自跑测试 |
| 5 | 在 [实现→验证→判断] 循环中因非 [指定原因] 向用户确认 |
| 6 | 测试失败超过 [N] 次后继续自动重试 |
| 7 | 跨 Agent 只口头传递信息,不写入 artifact |

3.6 完整总控模板

把以上四步组合,就得到一个可直接复用的总控模板:

# [项目名] UI 自动化测试 - 多 Agent Skill

> [一句话描述:这个 Skill 在什么情况下被调用]

---

## 你的角色

你是流程总控。在这个工作流里,你的职责只有三件事:
1. 调度三个子 Agent 按正确顺序执行
2. 判断每个 Agent 的输出,决定下一步
3. 保证不跳步、不遗漏、不突破重试上限

默认不要在这个工作流里自己做需求分析、直接改测试代码、或自己跑验证命令。

---

## 三个子 Agent

| 阶段 | 文件 | 推荐 agent_type | 职责 | 产物 |
|------|------|----------------|------|------|
| 分析 | `docs/.../analyzer-agent.md` | `explorer` | 解析需求、扫描代码、产出复用计划 | `_analysis.md` |
| 实现 | `docs/.../implementer-agent.md` | `worker` | 根据分析结果生成或修复代码 | 源码变更 |
| 验证 | `docs/.../verifier-agent.md` | `worker` | 审查架构、运行测试、输出验证报告 | `_verification.md` |

---

## 状态机```text
START -> ANALYZE -> IMPLEMENT -> VERIFY -> JUDGE
                                       |
                                       +-> DONE
                                       |
                                       +-> IMPLEMENT(architecture_fix) -> VERIFY -> JUDGE
                                       |
                                       +-> IMPLEMENT(retry_fix) -> VERIFY -> JUDGE
​```

### 重试计数

- retry 初始值为 0
- ARCHITECTURE_VIOLATION 不计入重试
- 仅当验证报告状态为 ❌ 失败时,retry += 1
- 最多自动重试 2 次
- 超过 2 次仍失败,结束并向用户报告

---

## 调度总则

[调度参数细节]

---

## 完整执行流程

### 阶段 1:分析(ANALYZE)
[步骤描述]

### 阶段 2:实现(IMPLEMENT)
[步骤描述]

### 阶段 3:验证(VERIFY)
[步骤描述]

### 阶段 4:判断(JUDGE)
[各状态处理逻辑]

---

## 禁止行为

[禁止清单]

第 4 章:手把手写三个 Agent Prompt

总控写好了,下面分别设计三个子 Agent 的 prompt。每个 prompt 都有固定的结构。

4.1 通用 Prompt 结构

每个子 Agent 的 prompt 文件都应包含:

1. 角色定义     → 告诉 Agent 它是谁、它的唯一职责是什么
2. 输入         → 它接收什么(文件路径、参数等)
3. 工作流程     → 步骤编号的执行顺序
4. 输出格式     → 产物文件的精确格式(用 Markdown 代码块给出模板)
5. 约束         → 绝对不能做的事

4.2 分析 Agent(Analyzer)的写法

核心任务:把用户的自然语言需求,变成实现 Agent 可以直接使用的结构化文档。

关键设计原则:让下游 Agent 不需要再读源码

分析 Agent 产出的 _analysis.md 必须足够完整,使得实现 Agent 在「首次实现」模式下,不需要自己去翻项目源码。这是最重要的一条原则,它:

  1. 加快速度:实现 Agent 不用重复扫描代码库
  2. 减少错误:信息经过分析 Agent 过滤,更精准
  3. 便于审查:用户在确认节点能看到清晰的复用计划

分析 Agent prompt 的核心步骤:

## 工作流程

### 第一步:解析需求
将用户描述转成有序步骤。每步标注:
- 所属 Page 类或 Service 类
- 操作类型:输入、点击、导航、验证、提取

### 第二步:匹配最佳实践文档
先读 [你的最佳实践目录]/README.md。
根据需求关键词,选择 1-2 份文档:
[关键词 → 文档 的映射表]

### 第三步:确定测试目录并读取相邻 case
[你的测试目录映射表]
读 1-3 个最相关的现有测试文件,提取:
- Service 的实例化方式
- import 路径模式
- 测试数据生成方式
- 步骤组合模式

### 第四步:扫描代码库
扫描 components/、pages/、services/ 下的文件,提取类名和公共方法。

### 第五步:复用分析
对每个步骤标记:复用 / 扩展 / 修改 / 新建

> 「修改」需要标出来供用户审批,「扩展」和「新建」不需要

### 第六步:输出 _analysis.md

复用分析的四个标记,为什么这样设计?

标记 含义 是否需要用户确认
复用 直接用,不改任何代码
扩展 在现有文件里新增方法 否(不改旧代码)
新建 创建新文件
修改 改动已有方法 (破坏性变更)

这个设计的核心思路是:「加法」不需要审批,「改法」需要审批。新增代码不会影响现有功能,修改已有代码可能破坏已有测试。

_analysis.md 的输出格式模板(关键部分):

# 需求分析结果

## 原始需求
[用户原文]

## 参考的最佳实践
- 模块:[模块名]
- 关键参考:[推荐的方法调用模式]

## 参考的相邻 Case
- 目录:[目录路径]
- 参考文件:[文件名及简述]

## 操作步骤
1. [PageClass] 操作描述
2. [PageClass] 操作描述

## 代码复用分析

### 可复用
| 步骤 | 现有代码 | 覆盖内容 |

### 需扩展
| 文件 | 新增内容 | 对应步骤 |

### 需新建
| 文件 | 用途 | 对应步骤 |

## 测试文件
- 目标文件:src/tests/xxx.test.ts

## 代码上下文(Agent 2 直接使用,无需重新读取源码)

### 需扩展文件的现有代码结构
[文件路径、类名、已有方法签名、插入位置建议]

### 相邻 Case 代码模式
[完整代码或关键片段]

### 测试文件模板
[基于相邻 case 推导出的模板]

最后一个约束(非常重要):

## 约束

- 不执行任何测试
- 不启动浏览器
- 不写实现代码
- 必须让「代码上下文」章节足够完整,使 implementer 在 first_run 下无需再次读项目源码

4.3 实现 Agent(Implementer)的写法

核心任务:读取 _analysis.md,严格按照架构约束写出可运行的测试代码。

最重要的两个设计:运行模式 + 已有函数保护

设计一:四种运行模式

实现 Agent 不是每次都做同样的事,它有四种模式:

模式 触发时机 核心任务
first_run 首次实现 _analysis.md,按复用计划写代码
review_fix AI 定位规范违规 修复 Page 方法里的定位方式
architecture_fix 架构分层违规 把定位逻辑移到正确的层
retry_fix 测试运行失败 读日志和截图分析,修复具体错误

first_run 模式下,实现 Agent 禁止读取项目源码文件。它只能使用 _analysis.md 里的「代码上下文」章节。这个约束的好处:

  • 强制分析 Agent 把上下文准备好
  • 防止实现 Agent 边分析边实现导致决策漂移
## first_run 的强约束

当运行模式为 first_run 时:
- 禁止读取 src/pages/*.ts、src/services/*.ts、src/tests/**/*.ts
- 需要的实现上下文全部来自 _analysis.md 的「代码上下文」章节
- 所有新增 Page 方法默认使用 AI-Only 模式
- 不写真实 CSS 定位逻辑

设计二:已有函数保护机制

这是整个 Skill 里最精妙的设计之一,专门用来防止 AI「偷偷改旧代码」。

问题背景:AI 在修复失败的测试时,有时会「顺手」修改已有的 Page 方法来解决问题。这很危险——那个方法可能被其他 10 个测试用例依赖,你改了之后其他测试全挂。

解决方案approved_modifications + rejected_modifications 双清单

## 已有函数修改保护(最高优先级)

### 核心原则
任何对已存在函数的修改,必须经过用户明确确认。未经确认,禁止修改。

### 判断规则
1. 修改任何已存在的函数前,检查该函数是否在 approved_modifications 清单中
2. 若在 approved_modifications 中 → 允许修改
3. 若在 rejected_modifications 中 → 禁止,使用替代策略
4. 若两个清单都没有 → 禁止,在返回消息中报告,等待主 Agent 向用户确认

### 未预审批函数的报告格式

⚠️ 需要修改未预审批的已有函数:

| 文件 | 函数名 | 修改原因 | 修改内容摘要 |
|------|--------|---------|-------------|
| src/pages/XxxPage.ts | clickXxx() | 需要增加参数 | 添加 optional 参数 |

请用户确认后重新调度。

### 被拒绝时的替代策略

| 场景 | 替代方案 |
|------|---------|
| 需要给已有 Page 方法加参数 | 新增 clickXxxWithOptions(),保持原方法不变 |
| 需要修改已有 Service 编排逻辑 | 新增专用 Service 方法,不改原方法 |

withFallback + AI-Only 模式的代码模板:

这是本项目 Page 层的标准写法模板,需要在实现 Agent 的 prompt 里给出完整示例:

/**
 * [功能描述]
 * AI-Only(首次实现)
 */
async clickXxx() {
  await this.withFallback({
    label: "clickXxx()",
    cssAction: async () => false,   // 固定写 false,强制走 AI
    aiFallback: async () => {
      await this.getAgent().aiTap("[容器上下文]中的[文案][元素类型]");
    },
    aiOnly: true,                   // 固定写 true
  });
  await this.wait(500);
}

Service 层模板:

async doSomething(param: string) {
  await this.step(`操作描述: "${param}"`, async () => {
    await this.xxxPage.clickXxx();  // 只调 Page 方法
  });
}
// 禁止:this.page.locator(...)、this.getAgent().aiTap(...) 等

Test 层模板:

describe("功能描述", () => {
  const ctx = usePlaywrightWithAuth();

  test("测试用例名称", async () => {
    const svc = new XxxService(ctx.page);  // 只 new Service
    await svc.doSomething("参数");
    expect(result).toBe(expected);         // expect 只在 Test 层
  }, 180_000);
});

4.4 验证 Agent(Verifier)的写法

核心任务:做质量守门员——先审查代码合规性,再运行测试,最后写出能被实现 Agent 直接使用的失败分析报告。

关键设计:两层审查 + 截图分析

两层审查的顺序

## 工作流程

Step 1: 架构合规审查(不通过 → 立即输出 ARCHITECTURE_VIOLATION,不跑测试)
Step 2: AI-Only 合规审查(仅首轮,不通过 → 输出 AI_ONLY_VIOLATION,不跑测试)
Step 3: 运行测试
Step 4: 分类错误
Step 5: 分析截图(如有)
Step 6: 输出 _verification.md

为什么审查要在运行之前?

跑测试要花几分钟。如果代码里有明显的架构违规,先检查出来,避免浪费时间等测试跑完。

架构违规检查的实现

验证 Agent 需要对以下模式做静态检查(在测试层和服务层中查找禁止出现的代码):

## Step 1:架构合规审查

只检查本轮新增或修改的文件。

### 测试层(src/tests/)禁止出现的代码模式
- page.evaluate(
- page.locator(
- page.click(
- .aiTap(
- .aiQuery(

### 服务层(src/services/)禁止出现的代码模式
- this.page.evaluate(
- this.page.locator(
- this.page.click(
- .aiTap(
- .aiAction(

若违规,立即输出 ARCHITECTURE_VIOLATION,停止,不运行测试。

截图分析的重要性

当测试失败且有截图时,截图往往比日志更直接地说明问题。验证 Agent 应该:

  1. 从日志里提取截图路径
  2. 读取截图
  3. 结合日志分析:页面处于什么状态、失败在第几步、根因是什么

这些信息写进 _verification.md 的「截图分析」章节,让实现 Agent 在修复时有更准确的方向。

_verification.md 的输出格式

验证 Agent 必须输出严格格式化的报告,因为总控要读取其中的状态字段来做判断:

# 验证报告

## 状态
[ARCHITECTURE_VIOLATION / AI_ONLY_VIOLATION / ✅ 通过 / ❌ 失败]

## 审查结果
- 架构合规:✅ 通过 / ❌ 违规

## 测试结果(仅通过审查时填写)
- 状态:✅ 通过 / ❌ 失败
- 测试文件:<文件路径>
- 通过/失败用例数:N

## 完整测试日志
```text
[完整 stdout + stderr,禁止截断]

错误详情(仅失败时填写)

错误 1

  • 类型:[selector_not_found / timeout / logic_error / type_error / import_error / assertion_failed]
  • 失败用例:
  • 错误信息:...

截图分析(仅失败时填写)

  • 截图路径:<路径或"截图不可用">
  • 页面状态:<描述>
  • 失败定位:<失败在第几步>
  • 根因推断:<描述>
  • 修复方向:<描述>

修复建议

  • [建议 1]
  • [建议 2] ```

约束(非常重要):

## 约束

- 不修改任何源码
- 不跳过架构审查直接跑测试
- 测试日志不要截断(完整日志是修复的依据)
- 报告中的错误分类、截图分析、修复建议要能直接被 implementer 使用

4.5 三个 Agent Prompt 设计总结

设计三个 Agent prompt 时,有一个核心原则贯穿始终:

每个 Agent 只做一件事,做完就输出产物文件,把决策权交回总控。

对照检查表:

检查项 Analyzer Implementer Verifier
角色定义清晰,只有一个职责
输入来源明确(文件路径)
工作步骤有编号
输出格式有严格模板
有「禁止」约束防止越权
产物写入 artifact 文件 _analysis.md 源码 _verification.md

第 5 章:实操:让 AI 帮你写第一个测试

架构设计好了,现在学怎么用它。

5.1 你需要准备什么

在开始之前,确认以下文件都已存在:

docs/codex/ui-automation/
├── SKILL.md                      ← 总控
├── agents/
│   ├── analyzer-agent.md         ← 分析 Agent prompt
│   ├── implementer-agent.md      ← 实现 Agent prompt
│   └── verifier-agent.md         ← 验证 Agent prompt
└── artifacts/                    ← 空目录,AI 会自动写入

另外,.codebuddy/skills/adp-test/best-practices/ 目录下需要有对应的最佳实践文档,分析 Agent 会用到它们。

5.2 如何触发这套 Skill

在你的 AI 编码助手(如 CodeBuddy)中,触发这套 Skill 有两种方式:

方式一:自然语言描述测试步骤

请帮我写一个 UI 自动化测试,步骤如下:
1. 进入应用列表页面
2. 搜索名称包含 "test" 的应用
3. 验证搜索结果中出现了至少一条记录

方式二:参考已有测试,补充覆盖

参考 src/tests/app_dev/app-list/delete-app.test.ts,
帮我写一个类似的测试:验证复制应用功能。

AI 识别到 UI 测试需求后,会自动读取 SKILL.md 开始执行三阶段流程。

5.3 完整流程演示

以「搜索应用」测试为例,演示完整流程。

步骤 1:描述需求

你对 AI 说:

帮我写一个测试:进入应用列表,在搜索框输入 "autotest",
验证结果列表中显示了包含该关键词的应用。

步骤 2:分析阶段(自动)

AI 调度分析 Agent,你会看到类似这样的输出:

[Analyzer] 正在扫描代码库...
[Analyzer] 读取最佳实践文档: app-list.md
[Analyzer] 读取相邻 case: search-app.test.ts, copy-app.test.ts
[Analyzer] 分析完成,写入 _analysis.md

然后,AI 展示摘要让你确认:

分析完成,复用计划如下:

步骤 1: [AppListPage] 导航到应用列表    → [复用] navigateToAppList()
步骤 2: [AppListPage] 输入搜索关键词    → [扩展] 需新增 searchByKeyword()
步骤 3: [AppListPage] 验证搜索结果      → [扩展] 需新增 verifySearchResult()

需新建文件:无
需扩展文件:src/pages/AppListPage.ts(新增 2 个方法)

请确认是否继续?

这个确认节点非常重要。 你看到的是:哪些功能已有、哪些需要新写、测试文件会在哪里。确认后才进入实现阶段。

步骤 3:实现阶段(自动)

AI 调度实现 Agent,自动在 AppListPage.ts 里新增两个 AI-Only 方法,并创建测试文件:

新增的 Page 方法(src/pages/AppListPage.ts):

/**
 * 在搜索框中输入关键词
 * AI-Only(首次实现)
 */
async searchByKeyword(keyword: string) {
  await this.withFallback({
    label: "searchByKeyword()",
    cssAction: async () => false,
    aiFallback: async () => {
      await this.getAgent().aiInput(
        "应用列表页面顶部的「搜索应用」输入框",
        { value: keyword }
      );
      await this.getAgent().aiKeyboardPress(
        "应用列表页面顶部的「搜索应用」输入框",
        { keyName: "Enter" }
      );
    },
    aiOnly: true,
  });
  await this.wait(1000);
}

/**
 * 验证搜索结果中是否包含关键词
 * AI-Only(首次实现)
 */
async verifySearchResult(keyword: string): Promise<boolean> {
  let result = false;
  await this.withFallback({
    label: "verifySearchResult()",
    cssAction: async () => false,
    aiFallback: async () => {
      result = await this.getAgent().aiBoolean(
        `应用列表中是否存在名称包含「${keyword}」的应用条目`
      );
    },
    aiOnly: true,
  });
  return result;
}

对应的 Service 层:

// src/services/AppListService.ts(新增方法)
async searchApp(keyword: string): Promise<boolean> {
  await this.step(`搜索应用: "${keyword}"`, async () => {
    await this.appListPage.searchByKeyword(keyword);
  });

  let exists = false;
  await this.step("验证搜索结果", async () => {
    exists = await this.appListPage.verifySearchResult(keyword);
  });
  return exists;
}

生成的测试文件(src/tests/app_dev/app-list/search-app.test.ts):

import "../../../setup/env";
import { describe, test, expect } from "vitest";
import { usePlaywrightWithAuth } from "../../../fixtures/playwright.fixture";
import { AppListService } from "../../../services";

describe("应用开发 - 应用列表操作", () => {
  const ctx = usePlaywrightWithAuth();

  /**
   * 测试步骤:
   * 1. 导航到应用列表
   * 2. 搜索 "autotest"
   * 3. 验证结果列表出现包含该关键词的应用
   *
   * 预期结果:搜索结果非空,且包含 "autotest" 关键词的应用
   */
  test("搜索应用 - 关键词匹配成功", async () => {
    const svc = new AppListService(ctx.page);
    const keyword = "autotest";

    await svc.navigateToAppList();
    const found = await svc.searchApp(keyword);

    expect(found, `搜索「${keyword}」应有匹配结果`).toBe(true);
  }, 180_000);
});

步骤 4:验证阶段(自动)

AI 调度验证 Agent:

  1. 先静态审查架构合规性——确认测试层没有直接操作 DOM,Service 层没有 AI 调用
  2. 检查 AI-Only 合规性——确认 Page 方法用了 cssAction: async () => falseaiOnly: true
  3. 运行测试:node scripts/run-test.js --maxWorkers=1 src/tests/app_dev/app-list/search-app.test.ts

步骤 5:判断结果

如果通过:

✅ 测试通过

- 测试文件:src/tests/app_dev/app-list/search-app.test.ts
- 通过用例数:1
- 执行时间:45s
- 重试次数:0

如果失败:

AI 自动分析失败原因,例如:

❌ 第 1 次失败

- 错误类型:timeout
- 根因:AI 定位描述「搜索应用」过于模糊,搜索框未找到
- 修复方向:增加容器上下文描述,改为「应用列表页面右上角的搜索输入框」

然后自动修复、重跑,最多两次。

5.4 测试数据命名规范

在测试里创建的数据(应用名、模板名等)必须遵守命名规范,防止测试数据污染生产数据:

// 推荐模板:名称以 _qta 结尾,总长度不超过 15 字符
const suffix = String(Date.now()).slice(-6);

const appName = `a_${suffix}_qta`;        // 例:a_628193_qta
const templateTitle = `t_${suffix}_qta`;  // 例:t_628193_qta
const nickname = `u_qta_${suffix}`;       // 例:u_qta_628193

原因

  • _qta 后缀让测试数据在界面上一眼可识别,方便清理
  • 时间戳后缀避免测试并发时命名冲突
  • 总长度限制避免触发后端的字段长度校验

第 6 章:常见问题与进阶技巧

6.1 错误类型速查表

当测试失败时,根据错误类型选择对应的修复方向:

错误类型 典型日志特征 常见根因 修复方向
selector_not_found element not foundlocator resolved to 0 AI 描述太模糊或容器不对 在 AI 描述里加容器上下文、文案、元素类型
timeout timeout exceeded 页面加载慢、弹窗未出现 增加 aiWaitFor() 等待特定状态
logic_error 运行时异常,但非定位问题 步骤顺序错误、依赖状态未就绪 调整步骤顺序或补充前置步骤
assertion_failed expect 断言失败 实际值与预期不符 检查断言逻辑或等待时机
type_error TypeScript 编译错误 类型不匹配、方法签名错误 直接修正类型定义
import_error 模块导入失败 路径错误或未从 index.ts 导出 修正导入路径,检查 index.ts 导出

6.2 AI 定位描述的好与坏

AI 定位描述的质量直接决定测试稳定性。一个好的描述要包含四个要素:

[容器上下文] + [视觉位置] + [文案] + [元素类型]

对照示例:

// ❌ 太模糊
await this.getAgent().aiTap("确定按钮");

// ✅ 包含四要素
await this.getAgent().aiTap("在「添加用户」弹窗底部右侧的「确定」按钮");
//                           └─容器上下文─┘  └─位置─┘   └─文案─┘  └─类型─┘

// ❌ 缺少容器(页面上有多个搜索框)
await this.getAgent().aiInput("搜索框", { value: keyword });

// ✅ 加上容器
await this.getAgent().aiInput("应用列表页面顶部的「搜索应用」输入框", { value: keyword });

6.3 AI 方法选择优先级

每次写 Page 方法时,先按这个顺序选方法:

输入文本    → aiInput()
点击元素    → aiTap()
滚动页面    → aiScroll()
按键盘      → aiKeyboardPress()
悬停        → aiHover()
双击        → aiDoubleClick()
读取数据    → aiQuery() / aiBoolean() / aiString() / aiNumber()
等待状态    → aiWaitFor()
验证        → aiAssert()
复杂复合操作 → aiAction()(最后手段)

绝对不要用 aiAction() 做单步操作,例如:

// ❌ 可以用 aiTap,不要用 aiAction
await this.getAgent().aiAction("点击确定按钮");

// ✅ 正确用法
await this.getAgent().aiTap("弹窗底部的「确定」按钮");

6.4 如何把这套架构迁移到你自己的项目

这套多 Agent Skill 不只适用于本项目。迁移到你自己的项目,需要修改以下几处:

第一步:定义你的代码分层架构

在架构参考文件(architecture-reference.md)里写清楚你的项目分层规则:每层允许做什么、禁止做什么。Verifier Agent 会用这个文件做静态审查。

第二步:建立最佳实践文档库

.codebuddy/skills/your-project/best-practices/ 里,按功能模块写文档:

best-practices/
├── README.md          ← 关键词 → 文档 的映射表
├── login.md           ← 登录相关测试的最佳实践
├── user-management.md ← 用户管理相关
└── ...

Analyzer Agent 会根据需求关键词选择读哪份文档。

第三步:更新测试目录映射

analyzer-agent.md 里,把「需求类型 → 推荐目录」的映射表改成你项目的目录结构。

第四步:更新测试命令

verifier-agent.md 里,把测试运行命令改成你项目的命令,例如:

# 本项目
node scripts/run-test.js --maxWorkers=1 <测试文件路径>

# 你的项目(示例)
npx playwright test <测试文件路径> --workers=1

第五步:调整 AI 方法 API

如果你的项目用的不是 Midscene,把 midscene-api-reference.md 里的 API 换成你实际使用的 AI 定位库的 API。

6.5 常见设计陷阱

陷阱一:总控文件写得太复杂

总控只需要「调度 + 判断」,不要在 SKILL.md 里写实现细节。如果某个 Agent 的工作流程很复杂,写在那个 Agent 自己的 prompt 文件里。

陷阱二:分析 Agent 没有输出完整上下文

分析 Agent 最容易犯的错误:_analysis.md 里的「代码上下文」章节写得太简略,实现 Agent 拿到后还要自己去读源码。正确做法:把方法签名、import 路径、代码片段都写进去。

陷阱三:验证 Agent 截断了测试日志

验证报告里的测试日志必须完整保留。实现 Agent 修复失败时,往往需要看日志的某个特定行,被截断就找不到了。

陷阱四:重试计数设置太大

如果允许重试 5 次、10 次,一个有问题的测试会让整个 Skill 运行很长时间。2 次是比较合适的上限:如果 3 轮(首次 + 2 次重试)还过不了,说明问题不是简单修一修能解决的,需要人工介入。

陷阱五:用户交互节点太多

总控里理想情况只有一个用户交互节点(分析完成后确认)。如果在每个步骤都停下来问用户,自动化的价值就大打折扣了。


总结

说一点点感想吧,最近社区有些冷清,我不太清楚大家在 AI 测试这条路上到底探索成了什么样子。但我周围的感受是,同事们很焦虑, 老板也很焦虑。 AI 出来后大家都怕使用不好 AI 而淘汰。 这个 AI 来淘汰人类的趋势是很难违抗的,未来将会是特种兵 +AI 的协作模式。 这一点我深有感受,我现在干的活,在以前需要 4,5 个人才能完成。但现在只有我一个。而大家同样都是在用 AI,我和少数的同事,负责的工作量又是其他人的两到三倍。特种兵 +AI 的格局已经形成,如果想不被淘汰,只能继续在这条路上走到黑了。 嗯, 不贩卖焦虑了, 大家加油吧。

最后再宣传一下我得星球:

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册