AI UI 测试平台从 0 到 1:8 次跑通一条 19 步用例的实战复盘

0 阅读9分钟

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_tokensAI 调用次数备注
1headedfailed367s480,32999数据问题(表不存在)
2headedfailed104s74,21318浏览器驱动崩溃
3headedsucceeded178s191,44040修 SQL 后首次跑通
4headedsucceeded122s93,12719cache 部分命中
5headedsucceeded99s57,01917cache 高命中
6headless假失败115s54,46016子进程成功,worker 卡死
7headedsucceeded114s54,46016修记忆后 cache 完整命中
8headlesssucceeded65s记忆异步化后

关键数据

  • Token 节省 88.7%(480K → 54K)
  • AI 调用次数节省 83.8%(99 → 16)
  • 耗时节省 68.9%(367s → 114s)
  • 修后用例稳定率 100%(5/5)

这四个数字不是孤立优化的结果,而是 cache + 工程卫生 + 自愈 的整体效果。下面展开每个 Run 背后的坑。


三、Run 1:第一次跑就挂了,挂在数据上

症状:超时失败,没有任何有用的错误信息。

排查路径(这是很多新人会踩的坑,列出来供大家避坑):

# 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 非零。

排查

# 看错误码
cat backend/logs/vision_star/runs/run_002/agent.log | grep -i "exit\|signal\|crash"

# 看浏览器进程是否还活着
ps aux | grep -i chrom | grep -v grep

根因:Puppeteer 在某个边缘场景下崩溃(具体触发条件没完全复现,但和登录态反复切换有关)。

修复

  1. subprocess 加 900 秒硬超时(之前是软超时,AI 卡住不会杀进程)
  2. process.on('SIGTERM') 处理器,确保子进程能优雅退出
  3. 浏览器驱动崩溃后,自动清理残留进程(pkill -f chrom

关键代码片段(抽象版,可直接套用):

# 伪代码:执行引擎的进程边界
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 不是优化问题,是生产化的生死线

1000 个用例 × 每天跑 2  × ¥3/次 = ¥6000/天 = ¥18万/月
1000 个用例 × 每天跑 2  × ¥0.34/次 = ¥680/天 = ¥2万/月

差 9 倍。任何 AI 测试平台没有 cache 不应该上生产——这不是夸张,是事实。

cache 接入的关键配置(脱敏版):

# 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(成功)。

排查

# 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 线程已经被同步阻塞队列彻底拖死。

修复:把同步写入挪到独立后台线程,主流程不等它。

关键代码片段

# 伪代码:异步记忆写入
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 收尾阶段我们加了自愈层。原理不复杂:

# 伪代码:自愈主流程
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失败类型结果说明
case1network不救回DNS 不可达(物理失败)
case2element救回元素定位修复
case3assertion救回断言条件更新
case4timeout不救回1s 极端超时(测试设计错误)
case5rename救回元素改名修复
  • 可修复失败救回率:3/3 = 100%
  • 总体救回率:3/5 = 60%

case4(1s 极端超时)我们讨论过要不要救——比如让 AI 自动延长等待时间。最后决定不救。因为这就是测试设计错误:用例硬等 1 秒,但页面元素就是需要 3 秒才出现。AI 救了,等于帮开发者掩盖一个本该重写用例的真正问题。

四个字:诚实 > 虚增


九、记忆系统:知识库注入的具体做法

记忆分三类:

类型内容例子
Wiki页面元素结构化描述"登录按钮:密码登录 tab,登录表单底部"
Skill成功模式蒸馏"表单提交前要等 500ms 防止 race condition"
失败记忆失败指纹+修复方案"case X 失败原因:Xpath 改了 → 修复:改 aiAct"

知识库注入(Wiki 知识的具体应用):

# 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 编辑器右下角"

注入流程

执行 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 脚本(脱敏版):

#!/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.logAI 每一步思考记录
plan.logAI 规划的过程
task.log任务执行结果
cli.logCLI 子进程输出

SSE 实时日志流(让 QA 边跑边看):

// 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 实战系列的第一篇,后续会写:

  1. AI UI 测试自愈层实战:16 类失败分类的具体实现
  2. 记忆系统的工程化:从本地 JSON 到语义检索的演进
  3. 多端测试接入:Android / iOS / Harmony / Desktop 的实战
  4. 真实 CI 数据复盘:2 周生产数据的真实失败模式分析
  5. AI 测试的"诚实 > 虚增"实践:怎么跟 stakeholder 解释 60% 通过率

如果你也在做 AI 测试平台,遇到了类似的坑或者有不同的解法,欢迎评论区交流。每一篇评论我都会认真看,认真回。

如果这篇对你有帮助,点赞、收藏、关注专栏是最大的支持。我们下篇见。


本文为系列文章首发,所有工程数据均来自 TestStar 项目 Tier 1 验证(2026-08)。

原创声明:本文为 TestStar 团队原创,转载请联系并注明来源。