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

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 在某个边缘场景下崩溃(具体触发条件没完全复现,但和登录态反复切换有关)。

修复

  1. subprocess 加 900 秒硬超时(之前是软超时,AI 卡住不会杀进程)
  2. process.on('SIGTERM') 处理器,确保子进程能优雅退出
  3. 浏览器驱动崩溃后,自动清理残留进程(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 实战系列的第一篇,后续会写:

  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 团队原创,转载请联系并注明来源。

相关推荐
PedroQue991 小时前
Vite 1.2.0发布:自动生成pages.json
前端·vite
ly76891 小时前
React 19 从入门到工程实践:核心原理、新特性与项目实战
前端·react.js·前端框架
tedcloud1231 小时前
diagram-design 怎么安装?用 AI 自动生成更专业的架构图、流程图
linux·运维·前端·人工智能·开源·流程图
何智超1 小时前
React 18 并发更新:高优先级先渲染,为什么最终状态不会算错?
前端
前端小万1 小时前
两个半月的时间如何赚到 3000 块钱。
前端
YIAN1 小时前
端侧大模型:DeepSeek-R1 WebGPU 推理全流程源码深度解析
前端·typescript·deepseek
请你吃div1 小时前
个人 Nuxt4 博客 SEO 优化实战教程
前端·nuxt.js·seo
网安蟹佬霸1 小时前
Android安全攻防实战:从APK逆向到Frida动态Hook全流程详解(附脚本)
android·前端·安全·web安全·逆向·csrf·网安
计算机魔术师1 小时前
芯片不跌反涨?两家公司的赌局正在重塑全球算力格局
前端