AI UI 测试平台从 0 到 1:8 次跑通一条 19 步用例的实战复盘
系列文章:《AI 测试平台从 Demo 到生产》(1/N) 本文是 TestStar 项目 Tier 1 稳定性验证的工程实录,所有数字、所有错误、所有"我们当时没想到"的事故都是真实发生的。
写在前面
做 AI UI 测试平台,最容易掉进去的坑是:demo 跑得漂亮,信心爆棚;上生产后第三天开始怀疑人生。
这篇文章不讲概念,不讲"AI 改变测试"的宏观叙事。只讲一件事:我们在 Tier 1 稳定性验证里,用一条 19 步的真实业务用例(登录 → 进 SQL 控制台 → 输 SQL → 执行 → 断言),连续跑了 8 次,发生了什么,怎么解决的,以及每个解决方案背后踩过哪些坑。
如果你也在做 AI 测试平台,或者正在评估一个 AI 测试工具能不能上生产------这篇值得读一遍。
一、为什么是"一条用例 8 次"?
很多团队做 AI 测试稳定性验证,会跑几十条用例各跑一次,统计整体通过率。我们选了一条用例跑 8 次,原因是:
稳定性不是"通过率",是"同一个失败模式会不会在多次运行中复现"。
一条用例跑 8 次,每次失败原因都不同------这说明系统里没有一个稳定的失败模式,是好事(说明每次失败都有具体原因可查)。 一条用例跑 8 次,第 3 次开始都是同一个错------这说明底层有 bug,需要修。
Tier 1 验证的 8 次跑通,本质是在测系统的可重复性 + 失败的可解释性。
二、原始数据:8 次跑通的完整时间线
用例:登录 → 进入 SQL 控制台 → 输入 SQL → 执行 → 断言(19 步)
| Run | 模式 | 状态 | 耗时 | total_tokens | AI 调用次数 | 备注 |
|---|---|---|---|---|---|---|
| 1 | headed | failed | 367s | 480,329 | 99 | 数据问题(表不存在) |
| 2 | headed | failed | 104s | 74,213 | 18 | 浏览器驱动崩溃 |
| 3 | headed | succeeded | 178s | 191,440 | 40 | 修 SQL 后首次跑通 |
| 4 | headed | succeeded | 122s | 93,127 | 19 | cache 部分命中 |
| 5 | headed | succeeded | 99s | 57,019 | 17 | cache 高命中 |
| 6 | headless | 假失败 | 115s | 54,460 | 16 | 子进程成功,worker 卡死 |
| 7 | headed | succeeded | 114s | 54,460 | 16 | 修记忆后 cache 完整命中 |
| 8 | headless | succeeded | 65s | --- | --- | 记忆异步化后 |
关键数据:
- Token 节省 88.7%(480K → 54K)
- AI 调用次数节省 83.8%(99 → 16)
- 耗时节省 68.9%(367s → 114s)
- 修后用例稳定率 100%(5/5)
这四个数字不是孤立优化的结果,而是 cache + 工程卫生 + 自愈 的整体效果。下面展开每个 Run 背后的坑。
三、Run 1:第一次跑就挂了,挂在数据上
症状:超时失败,没有任何有用的错误信息。
排查路径(这是很多新人会踩的坑,列出来供大家避坑):
bash
# Step 1:看日志(用 wc -l 看大小)
wc -l backend/logs/vision_star/runs/run_001/*.log
# Step 2:找"最后一行错误"
grep -n "Error\|Timeout\|failed" backend/logs/vision_star/runs/run_001/agent.log | tail -5
# Step 3:看 AI 最后思考了什么
tail -200 backend/logs/vision_star/runs/run_001/ai.log
# Step 4:看浏览器实际状态
ls -lh backend/logs/vision_star/runs/run_001/screenshots/
根因 :测试用的数据表 daily_orders_2024_q4 不存在,AI 一直在等这个表加载,超时。
修复:补数据 + 在测试 setup 里加表存在性断言。
教训 :AI 测试不像传统测试有"明确的报错"。AI 说"超时"可能是 50 种原因,定位必须靠日志三件套------浏览器日志 / AI 日志 / 应用日志,缺一不可。

四、Run 2:浏览器驱动崩了(这次是真崩)
症状:跑 104 秒后整个 run 中断,exit code 非零。
排查:
bash
# 看错误码
cat backend/logs/vision_star/runs/run_002/agent.log | grep -i "exit\|signal\|crash"
# 看浏览器进程是否还活着
ps aux | grep -i chrom | grep -v grep
根因:Puppeteer 在某个边缘场景下崩溃(具体触发条件没完全复现,但和登录态反复切换有关)。
修复:
- subprocess 加 900 秒硬超时(之前是软超时,AI 卡住不会杀进程)
- 加
process.on('SIGTERM')处理器,确保子进程能优雅退出 - 浏览器驱动崩溃后,自动清理残留进程(
pkill -f chrom)
关键代码片段(抽象版,可直接套用):
python
# 伪代码:执行引擎的进程边界
import subprocess
import signal
class VisionRunner:
def run_case(self, case_yaml: str, timeout_sec: int = 900):
proc = subprocess.Popen(
['midscene', '--yaml', case_yaml],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
preexec_fn=os.setsid # 独立进程组,便于整组 kill
)
try:
stdout, stderr = proc.communicate(timeout=timeout_sec)
if proc.returncode != 0:
raise EngineCrashError(stderr.decode())
return self._parse_result(stdout)
except subprocess.TimeoutExpired:
# 整组杀,避免僵尸进程
os.killpg(proc.pid, signal.SIGTERM)
raise TimeoutError(f"Run exceeded {timeout_sec}s")
教训 :进程隔离是 AI 测试平台的硬性需求。AI 浏览器驱动一旦崩了,绝不能拖垮主进程。subprocess + 进程组 + 硬超时,三件套缺一不可。
五、Run 3-5:cache 命中带来的"省钱三连"
cache 是 TestStar 接入的底层引擎自带的机制------相同页面 + 相同元素描述的查询结果会被复用,不需要每次重新调 AI。
Run 3(修数据后第一次跑) :191K tokens,178s。 Run 4(cache 部分命中) :93K tokens,122s。 Run 5(cache 高命中):57K tokens,99s。
为什么 cache 这么重要?因为 token 不是优化问题,是生产化的生死线。
yaml
1000 个用例 × 每天跑 2 次 × ¥3/次 = ¥6000/天 = ¥18万/月
1000 个用例 × 每天跑 2 次 × ¥0.34/次 = ¥680/天 = ¥2万/月
差 9 倍。任何 AI 测试平台没有 cache 不应该上生产------这不是夸张,是事实。
cache 接入的关键配置(脱敏版):
yaml
# config.yaml
vision_star:
cache:
enabled: true
backend: sqlite # 或 redis
ttl_days: 7
similarity_threshold: 0.85 # 元素描述相似度阈值
max_entries: 100000
关键参数:
similarity_threshold = 0.85:低于这个相似度不命中,宁可重算不命中错ttl_days = 7:超过 7 天强制失效(避免 UI 改版后用旧 cache)backend:单机用 sqlite,集群用 redis
六、Run 6:最关键的假失败
症状:headless 模式第一次跑,115s 后 worker 报"failed"。但子进程的 exit code 是 0(成功)。
排查
bash
# Step 1:子进程日志说"执行成功"
grep "Exit code" backend/logs/vision_star/runs/run_006/cli.log
# 输出:Exit code: 0
# Step 2:但 API 返回的是 None
curl http://localhost:8000/api/v1/runs/6
# {"status": "failed", "error": null}
# Step 3:worker 线程卡在哪里?
py-spy dump --pid $(pgrep -f "vision_star.worker") | head -50
# 输出:卡在记忆服务的 sync_vector_search()
# Step 4:记忆服务的调用栈
SELECT * FROM trace_event WHERE run_id=6 AND event_type='memory_call' ORDER BY ts DESC LIMIT 20;
# 输出:调用 19 次,每次都成功,但 worker 主线程阻塞在第 16 次之后
根因:每跑一步 AI 操作,记忆服务都要同步执行一次"向量检索 + 写入"。等到第 19 步,worker 线程已经被同步阻塞队列彻底拖死。
修复:把同步写入挪到独立后台线程,主流程不等它。
关键代码片段:
python
# 伪代码:异步记忆写入
import queue
import threading
class AsyncMemoryStore:
def __init__(self):
self._add_queue = queue.Queue(maxsize=1024)
self._ensure_async_worker()
def _ensure_async_worker(self):
if not hasattr(self, '_worker') or not self._worker.is_alive():
self._worker = threading.Thread(
target=self._async_add_worker, daemon=True
)
self._worker.start()
def _async_add_worker(self):
while True:
try:
item = self._add_queue.get()
self._do_write(item) # 同步写入,但不阻塞主流程
except Exception as e:
logger.exception(f"memory write failed: {e}")
def add(self, memory):
# 不等待,立即返回
try:
self._add_queue.put_nowait(memory)
except queue.Full:
logger.warning("memory queue full, dropping")
def search(self, query):
# 搜索保持同步(自愈和查询依赖)
return self._do_search(query)
Run 7 验证:114s 跑通,token 54K(cache 完整命中),AI 调用 16 次。
教训(写进团队"反模式清单"):
任何在请求路径上的同步阻塞,都可能让整个平台卡死。
这条值得写进每个 Harness 层的反模式清单。
七、Run 8:headless 模式跑通
耗时 65s,token 进一步下降。
Run 7 是 headed 模式(带浏览器界面),Run 8 是 headless(无界面)。headless 少了一些截图交互开销,所以更省时。
生产建议:
- 本地调试:用 headed,方便观察 AI 行为
- CI 跑:用 headless,省资源
- 真实环境复现:用 headed,更接近用户场景
八、自愈层:让失败不再"卡死"
前面 5 个失败 case 都是"硬失败"------挂了就是挂了,没人救。
Tier 1 收尾阶段我们加了自愈层。原理不复杂:
python
# 伪代码:自愈主流程
def heal(run, case, error):
# Step 1:分类失败
category = classify_failure(error)
# category ∈ {timeout, network, element_not_found,
# assertion_failed, ui_changed, login_expired,
# data_accumulated, deadloop, ...}
# Step 2:根据分类选策略
if category == 'element_not_found':
return deep_locate_retry(case) # 深度重定位
elif category == 'assertion_failed':
return ai_diagnose(case, error) # AI 诊断
elif category == 'network':
return backoff_retry(run) # 退避重试
# ...
# Step 3:生成修复 patch
patch = generate_patch(case, error)
# Step 4:置信度门控
if patch.confidence >= CONFIDENCE_THRESHOLD:
apply_patch(case, patch)
return retry(run)
else:
# 低置信度:弹窗交人工审核
return await_human_review(case, patch, error)
5 个故意失败用例实测救回率:
| Case | 失败类型 | 结果 | 说明 |
|---|---|---|---|
| case1 | network | 不救回 | DNS 不可达(物理失败) |
| case2 | element | 救回 | 元素定位修复 |
| case3 | assertion | 救回 | 断言条件更新 |
| case4 | timeout | 不救回 | 1s 极端超时(测试设计错误) |
| case5 | rename | 救回 | 元素改名修复 |
- 可修复失败救回率:3/3 = 100%
- 总体救回率:3/5 = 60%
case4(1s 极端超时)我们讨论过要不要救------比如让 AI 自动延长等待时间。最后决定不救。因为这就是测试设计错误:用例硬等 1 秒,但页面元素就是需要 3 秒才出现。AI 救了,等于帮开发者掩盖一个本该重写用例的真正问题。
四个字:诚实 > 虚增。
九、记忆系统:知识库注入的具体做法
记忆分三类:
| 类型 | 内容 | 例子 |
|---|---|---|
| Wiki | 页面元素结构化描述 | "登录按钮:密码登录 tab,登录表单底部" |
| Skill | 成功模式蒸馏 | "表单提交前要等 500ms 防止 race condition" |
| 失败记忆 | 失败指纹+修复方案 | "case X 失败原因:Xpath 改了 → 修复:改 aiAct" |
知识库注入(Wiki 知识的具体应用):
yaml
# db_knowledge/sql_console.yaml
elements:
password_login_tab:
desc: "密码登录"
location: "登录页顶部 tab 区域"
phone_input:
desc: "手机号输入框"
location: "登录表单第一行"
password_input:
desc: "密码输入框"
location: "登录表单第二行"
login_button:
desc: "登录按钮"
location: "登录表单底部"
instance_card_connect:
desc: "实例卡片连接按钮"
location: "实例列表每行右侧"
sql_editor:
desc: "SQL 编辑器"
location: "SQL 控制台中央"
execute_button:
desc: "执行按钮"
location: "SQL 编辑器右下角"
注入流程:
makefile
执行 yaml → 静态分析 → 命中元素 → 注入 location 提示
↓
aiAct: 点击登录(位置:登录表单底部)
↓
AI 优先用结构化锚点定位 → 失败回退纯视觉理解
实测结果:
- 冷 cache:token 297K(注入让 prompt 变长)
- 热 cache:token 57K(恢复正常)
- 稳定性:显著提升(解决了之前"密码 vs 密码登录"的混淆问题)
工程判断 :先做单用例深度,跑 2 周看哪些字段真有用,再决定要不要抽象。过早抽象是工程灾难。
十、CI 接入:36% 通过率的故事
第一次接入 CI 时,跑出 36% 通过率。Stakeholder 第一反应:
"这么差,怎么上生产?"
我们的回应是:
"第一次跑出 36% 是正常的。重要的是 exit code 是 0(CI 没阻塞)、KPI 被采集(自愈率/token/耗时)、数据写入 DB(生产环境可以持续观察)。未来 2 周的真实数据积累,才是这次 CI 接入的真正价值所在。"
CI smoke 脚本(脱敏版):
bash
#!/bin/bash
# scripts/ci_visionstar_smoke.sh
set -e
echo "=== TestStar CI Smoke ==="
# 1. 启动服务
./scripts/start.sh --ci-mode
# 2. 等待健康检查
for i in {1..30}; do
if curl -sf http://localhost:8000/api/v1/health > /dev/null; then
echo "Service ready"
break
fi
sleep 2
done
# 3. 跑 smoke 用例
curl -X POST http://localhost:8000/api/v1/vision-star/joint-runs \
-H "Content-Type: application/json" \
-d '{"suite_id": "smoke_suite", "headless": true}'
# 4. 等待结果
sleep 300
# 5. 采集 KPI
curl -s http://localhost:8000/api/v1/vision-star/dashboard/overview > ci_metrics.json
# 6. 验证关键指标
python scripts/verify_kpis.py ci_metrics.json
echo "=== CI Smoke Complete ==="
端到端验证:
- 2 suites / 88 runs
- 36% 成功率
- 自愈控制功能正常,CI 通过(exit 0)
很多团队不理解"36% 成功率"意味着失败。对一个新集成的 AI 测试平台,第一次跑 CI 出现 36% 成功率是正常的。重要的是数据被采集、被持续观察。
十一、可观测性:6 类日志 + SSE 实时流
AI 测试失败时,看到的常常是:
- "assertion failed"
- 一份 JSON 报告
- 一堆 AI 思考过程的文本(但这些文本可能是编造的)
为了让 QA 能真正定位问题,TestStar 把执行过程的日志分成 6 类:
| 日志类型 | 内容 |
|---|---|
agent.log |
浏览器引擎主日志 |
device-task-executor.log |
设备任务执行器日志 |
ai.log |
AI 每一步思考记录 |
plan.log |
AI 规划的过程 |
task.log |
任务执行结果 |
cli.log |
CLI 子进程输出 |
SSE 实时日志流(让 QA 边跑边看):
javascript
// frontend: 订阅 run 日志
const eventSource = new EventSource(`/api/v1/runs/${runId}/logs/stream`);
eventSource.onmessage = (event) => {
const log = JSON.parse(event.data);
appendToUI(`[${log.level}] ${log.message}`);
};
好处:AI 测试和传统 CI 测试不一样------AI 测试适合"边跑边看"。QA 能实时判断 AI 是不是在胡思乱想,必要时人工介入。
十二、经验总结:5 条铁律
Tier 1 跑完,团队把"血的教训"写成 5 条铁律:
铁律一:每条任务必须有"不做会怎样"
防止为了"看起来忙"加任务。每个 TODO 必须能回答两个问题:不做会怎样?做了能避免什么?
铁律二:P1 任务严格串行
每个 P1 完成 + Tier 1 验收 KPI 后才进下一个,避免并发堆债。
铁律三:P2 标记"暂禁"是默认
任何新建议都要回答"为什么不做 Tier 1/2 的核心"。
铁律四:做完 P0/P1 必须更新 tier report
不沉淀等于没做。
铁律五:不使用 TODO/TBD/FIXME 作为任务名
每条要明确"做什么"。
内部金句 :"扩生成能力等于扩垃圾"------Tier 1 主动清到只剩 1 个用例,就是为了不分散精力。
十三、什么场景能用,什么不能用
能用(已实测):
- 表单 + 列表 + 详情的中后台系统(OA、CRM、运营后台、BI 平台、数据查询控制台)
- 有稳定结构化页面的 Web 应用
- 重复执行的回归测试
不建议(已踩坑):
- Canvas / WebGL 应用(游戏、可视化、白板)------ AI 看不懂像素含义
- 强风控页面(验证码、滑块、行为检测)------ AI 没有真人环境
- 极短用例(<5 步)------ cache 命中收益不抵启动开销
- 跨域联邦登录(OAuth、SAML 等多步跳转)------ 状态太复杂
- 强动态 SPA(每次刷新 DOM 完全重排)------ 难以稳定锚点
承认边界不丢人,假装没有边界才丢人。
十四、踩坑速查表
最后给一个速查表,遇到问题按这个顺序排查:
| 现象 | 第一步排查 | 第二步排查 | 第三步排查 |
|---|---|---|---|
| Run 失败无错误 | 看 6 类日志最后 100 行 | 看 AI 思考路径 | 看浏览器截图序列 |
| Run 超时 | 看 agent.log 时间分布 | 看 AI 调用次数 | 看 worker 线程栈 |
| Token 异常高 | 看 cache 命中率 | 看 prompt 长度 | 看 AI 调用重复模式 |
| AI 找不到元素 | 看页面截图 | 看 Wiki 知识库覆盖 | 看元素描述相似度 |
| 自愈改坏用例 | 看 patch diff | 看置信度 | 看错误信号源 |
| CI 通过率突降 | 看 Run 时间分布 | 看环境变量变化 | 看数据快照 |
系列预告 + 互动
这是 TestStar 实战系列的第一篇,后续会写:
- AI UI 测试自愈层实战:16 类失败分类的具体实现
- 记忆系统的工程化:从本地 JSON 到语义检索的演进
- 多端测试接入:Android / iOS / Harmony / Desktop 的实战
- 真实 CI 数据复盘:2 周生产数据的真实失败模式分析
- AI 测试的"诚实 > 虚增"实践:怎么跟 stakeholder 解释 60% 通过率
如果你也在做 AI 测试平台,遇到了类似的坑或者有不同的解法,欢迎评论区交流。每一篇评论我都会认真看,认真回。
如果这篇对你有帮助,点赞、收藏、关注专栏是最大的支持。我们下篇见。
本文为系列文章首发,所有工程数据均来自 TestStar 项目 Tier 1 验证(2026-08)。
原创声明:本文为 TestStar 团队原创,转载请联系并注明来源。