本文记录一个 AI 自动化 UI 测试系统的真实架构演进过程。核心不是"最终架构长什么样",而是每一步为什么踩坑、为什么推翻、怎么演变到下一步

注意:文中所有具体 URL、token、项目名均已脱敏。


一、系统不是一次设计出来的——架构演进时间线

V0 盲写脚本        → 让 LLM 直接根据任务描述生成 Playwright 代码
V1 单次 MCP 探测   → 先用 MCP 看一下页面结构,再让 LLM 写代码
V2 Claude CLI 方案 → 用 claude --bare --print 生成脚本(已废弃)
V3 API 直调方案    → 直接调用 LLM API,自己组装 Prompt
V4 执行层补齐      → 加 trace、截图、watchdog、自动修复闭环
V5 断言层演进      → 从"完全信 AI"到"AI + DOM 双保险"
V6 知识库接入      → 记住"这个错上次怎么修的"
V7 启动链路分离    → 认证注入、页面加载不再散落在各脚本里
V8 配置分层        → 系统级 / 项目级 / 用例级三层配置

下面按版本展开,重点讲每个版本的架构决策失误


二、V0:盲写脚本——让 AI 瞎猜选择器

2.1 当时的设计思路

“AI 不是挺聪明的吗?直接告诉它’打开某某页面,点击登录按钮’,它应该能写出对的代码吧?”

于是最早的流程是:

task.txt(自然语言描述)
    ↓
直接扔给 LLM → 生成 Python 脚本
    ↓
保存为 script_v1_0.py

2.2 踩的坑

坑 1:AI 没有页面信息,选择器全是猜的

LLM 生成的代码里有 //button[@id='submit'],但目标页面实际用的是 //button[@class='ant-btn-primary']。LLM 没见过页面,只能根据常识瞎猜,猜中的概率极低。

坑 2:生成的代码无法直接运行

  • 没有 try/finally 关闭浏览器,进程泄漏
  • 没有等待逻辑,元素还没渲染就操作
  • 没有异常处理,一步失败全盘崩

2.3 结论

不给 AI 看页面就让它写代码,等于闭着眼射箭。

必须让 AI"先看一眼"页面结构,再写选择器。


三、V1:单次 MCP 探测——看了,但没看透

3.1 当时的设计思路

“既然盲写不行,那让 AI 先通过 MCP(Model Context Protocol)获取页面结构,拿到无障碍树和 DOM 元素列表,再写代码总该准了吧?”

task.txt
    ↓
提取 URL → MCP 获取页面结构(无障碍树 + 交互元素列表)
    ↓
把页面结构塞进 Prompt → LLM 生成脚本
    ↓
保存 script_v1_0.py

3.2 踩的坑

坑 3:MCP 返回的结果没有充分利用

虽然拿到了页面结构,但生成的脚本仍然只依赖单一选择器。为什么?因为 Prompt 里没明确要求"生成多策略降级定位"。LLM 拿到信息后,还是习惯性地写一个最像 XPath 的选择器就完事了。

坑 4:只依赖单一选择器,SPA 一重构就崩

前端用的是 React/Vue,元素嵌套层级、class 名随时可能变。脚本里只写了一个 //div[3]/div[1]/button,前端加了个 wrapper div,脚本第二天就挂。

坑 5:MCP 探测的时机和错误处理没做好

  • 页面还没加载完就探测,无障碍树只有 <html> 一个节点
  • MCP 返回 isError=True 时,代码没有中断,继续往下执行,生成一堆基于错误信息的垃圾代码
  • 某些自定义组件(如自定义 textbox)直接 type() 不生效,需要 click() 后再 type(),但 MCP 探测不到这种行为层面的信息

3.3 结论

看了页面不够,还要让 AI 生成"备胎选择器";探测页面不够,还要处理探测失败的情况。


四、V2:Claude CLI 方案——一条死路

4.1 当时的设计思路

“既然要组装 Prompt 调 LLM,不如直接用 Claude Code 的 CLI 模式:claude --bare --print,把任务描述和页面结构传进去,让它直接输出代码。这样不用自己管理 API Key、模型选择,Claude 自己搞定。”

4.2 踩的坑

坑 6:Windows 下子进程卡死

Python 里用 subprocess.Popen(['claude.cmd', '--bare', '--print'], ...),在 Windows 上直接 hang 住,永远拿不到输出。根本原因是 claude.cmd 内部依赖 git-bash 环境,而 Python subprocess 的 PIPE 重定向和它冲突。

坑 7:中文 Prompt 被编码搞坏

Windows 的 cmd.exe 默认用 GBK 编码,直接把 UTF-8 的中文 Prompt 传给 CLI,中文字符全部乱码,Claude 收到的 prompt 是残缺的,输出的代码也是残缺的。

坑 8:特定 trigger 词导致空响应

Prompt 里如果包含 AIVisualAssertionassert_business_goal03_ai_assertion_input 这类和项目代码相关的词,Claude CLI 会返回空响应(0 字符)。原因不明,猜测是 CLI 内部的安全过滤或上下文冲突。

4.3 结论

依赖外部 CLI 工具做核心链路,等于把命脉交给不可控的第三方。Windows 编码 + 子进程 + 第三方 CLI = 三重不稳定。

彻底废弃 CLI 方案,改为直接调用 HTTP API。


五、V3:API 直调方案——Prompt 架构混乱

5.1 当时的设计思路

“直接用 requests 调用 Claude/GPT/Qwen API,自己组装 system prompt + user prompt,总该稳定了吧?”

5.2 踩的坑

坑 9:Prompt 内容三层重复

最早的设计里,同时存在:

  • generate_script.md skill 文件(生成脚本的规范)
  • fix_script.md skill 文件(修复脚本的规范)
  • system prompt 里的通用编码规范
  • user prompt 里的项目特定配置

结果是同一个"要关闭浏览器"的要求,在 4 个地方各写了一遍。改一处,另外三处忘改,AI 的行为前后不一致。

坑 10:project.config.md 的双重身份危机

project.config.md 既想让机器读(YAML frontmatter 格式的配置),又想让人读(Markdown 文档说明)。结果是:

  • 代码解析 frontmatter 时,把说明文字也当成配置读进去
  • 人看文档时,被 YAML 的 --- 分隔符搞糊涂
  • 一些值(如 screenshot.skip_font_waitai_vision.*)写进去了但代码从来不读,成了"僵尸配置"

坑 11:System Prompt 在修复时丢失

生成脚本时传了 system prompt(编码规范、关闭浏览器等要求),但修复脚本时走的是 _call_with_images(带截图的 API 调用),这个函数没传 system prompt。结果是:修复后的脚本经常忘记 try/finally,选择器质量也下降。

坑 12:AI 返回的代码被截断或为空

  • 代码跑到一半,类定义没闭合(检测末尾字符重试解决)
  • 返回 0 字符,生成空文件(加空内容检查解决)
  • 修复前缀带中文解释(如"我来分析这个错误并修复脚本。"),导致 Python SyntaxError: invalid character '。'

5.3 结论

Prompt 不是越详细越好,是分层的:System 管通用规则(只写一次),User 管动态内容(每次不同)。配置文件的机器部分和人读部分必须分开。

最终方案:合并成单一 system prompt skill,删掉 generate_script.mdfix_script.mdproject.config.md 只保留项目特定配置,通用规则全部进 system prompt。


六、V4:执行层补齐——从"跑完拉倒"到"可观测可修复"

6.1 当时的设计思路

“脚本生成了,直接 python script_v1_0.py 运行,成功就成功,失败就失败。”

6.2 踩的坑

坑 13:没有过程回溯,失败了完全不知道发生了什么

最早只有最终成功/失败的结果,没有:

  • 每步执行后的截图(不知道页面长什么样)
  • Playwright trace(无法回放执行过程)
  • 详细日志(不知道卡在哪一步)

脚本失败了,只能看终端的 traceback,但很多错误(如元素定位失败)光看代码行号根本定位不了问题。

坑 14:trace 目录创建了,但根本没录

代码里创建了 traces/ 目录,但 tracing.start() 根本没调用。目录是空的,只是"看起来有 trace 功能"。

坑 15:脚本卡住拖垮整体

某一步 page.click()page.wait_for_selector() 卡住 10 分钟不返回,整个用例被拖死。没有 watchdog 机制,没有超时强制终止。

坑 16:Python stdout 缓冲导致"假死"

子进程执行脚本时,stdout 连接 PIPE,Python 默认缓冲输出。脚本其实在正常运行,但父进程看不到任何输出,以为卡死了。

坑 17:修复时改坏没出错的步骤

第 3 步报错,让 AI 修复,结果 AI 把整个脚本重写了一遍,第 1 步、第 2 步也改坏了。原因是 Prompt 没约束"只修改出错步骤,其他原样保留"。

坑 18:修复无限循环耗光额度

同一类错误反复修反复挂,没有限制修复轮数。理论上可以无限循环下去,把 API 额度烧光。

6.3 结论

执行层不能只是"运行脚本",必须是一个闭环:运行 → 观测(截图+trace+日志)→ 诊断 → 修复 → 再运行。没有观测就没有诊断,没有诊断就没有修复。

最终补齐的组件:

  • 每步截图(screenshots/时间戳/
  • Playwright trace(失败时保留 zip)
  • 视频录制(失败时保留 webm,成功时自动清理)
  • Watchdog(120 秒无输出强制杀进程)
  • 修复轮数限制(最多 2 轮)
  • 修复 Prompt 强制约束"只改出错步"

七、V5:断言层演进——从"完全信 AI"到"AI + DOM 双保险"

7.1 当时的设计思路

“传统断言太脆弱了,文案改一个字就崩。让 AI 看截图判断’任务完成了吗’,多灵活。”

7.2 踩的坑

坑 19:完全信任 AI 断言,结果被幻觉骗了

AI 说"页面显示正常,任务已完成",但实际上关键按钮根本没渲染出来。AI 的"正常"是基于它对截图的理解,不是基于 DOM 状态。

坑 20:AI 断言截图看不清

字体没加载完就截图,截图里的文字是方框或模糊,AI 根本无法正确判断。等加了 page.wait_for_timeout(3000) 等字体加载后才解决。

坑 21:AI 置信度虚高

AI 返回 confidence=0.5 但说"通过"。如果不设最小置信度阈值,低置信度的错误判断会被当成正确结果。

坑 22:断言 Prompt 里包含 trigger 词导致空响应

和 CLI 方案类似,断言相关的 Prompt 如果包含某些项目代码里的类名或函数名,API 也会返回空。解决方法是 Prompt 里不写具体代码引用,只写自然语言描述。

7.3 结论

AI 断言是"柔性判断",DOM 断言是"刚性校验"。两者互补,缺一不可。

最终方案:

  • AI 断言先看截图判断业务目标是否达成
  • 通过后追加 DOM 验证(input.valuepage.url、元素数量等)
  • 设置最小置信度阈值(如 0.7),低于则判失败

八、V6:知识库接入——让系统"越跑越聪明"

8.1 当时的设计思路

“同一个任务,上次成功了,下次应该也能成功。把成功案例存下来,下次直接用。”

8.2 踩的坑

坑 23:知识库只存成功,不存失败

早期只记录"这个任务+这个页面结构=这个脚本可以成功"。结果同一个任务换个描述、或者页面结构稍有变化,又失败了,系统从零开始修,上次修好的经验完全没复用。

坑 24:知识库没有和修复链路打通

即使存了失败案例,修复时也没有自动去查"上次这个错误怎么修的"。知识库成了摆设,AI 修复时还是全靠猜。

8.3 结论

失败比成功更值得记住。知识库存的不是"正确答案",而是"错误模式 → 修复方案"的映射。

最终方案:

  • 成功案例:任务描述 + 页面结构 + 脚本
  • 失败案例:任务描述 + 错误信息 + 修复后的脚本 + 修复结果
  • 修复时自动搜索相似错误,把历史方案附加到修复 Prompt 中

九、V7:启动链路分离——别让每个脚本重复造轮子

9.1 当时的设计思路

“每个脚本都需要认证注入和页面加载,那就每个脚本里都写一遍呗。”

9.2 踩的坑

坑 25:认证信息分散在 N 个脚本里

每个生成的脚本里都有:

page.add_init_script("window.localStorage.setItem('token', 'xxx')")
page.goto("about:blank")
page.goto(TARGET_URL)
page.reload()

Token 过期时,要改 N 个文件。加载逻辑也不统一,有的等 3 秒,有的等 10 秒。

坑 26:启动脚本的 import 路径混乱

提取了 startup_flow.py 后,脚本计算的是 project_root,但 startup_flow.py 实际在 project_folder(脚本的父目录)。脚本运行时找不到模块,需要手动加 sys.path

坑 27:add_init_script 时机不对

错误地在 page.goto() 之后才调用 add_init_script(),结果 token 没注入成功,页面跳转到 login.html。add_init_script 只在新页面加载时执行,已经打开的页面不生效。

9.3 结论

启动链路(认证 + 加载 + 等待)是横切关注点,必须抽离成独立模块,所有脚本统一调用。

最终方案:startup_flow.py 统一封装认证注入、页面加载、等待策略,脚本第一步调用 run_startup_flow(page)


十、V8:配置分层——别让配置散落在代码里

10.1 当时的设计思路

“配置直接写在代码里,或者写在一个文件里,简单省事。”

10.2 踩的坑

坑 28:API Key 写死在代码里,差点提交到 git

早期直接把 api_key = "sk-xxx" 写在 Python 文件里,.gitignore 差点没拦住。

坑 29:超时设置一刀切

简单页面和复杂页面用同一个超时,要么简单页面等太久,要么复杂页面超时失败。

坑 30:无头模式调试难

headless=True 跑正式用例,出问题了看不见浏览器在干嘛,只能猜。

10.3 结论

配置要分层:系统级(全局)、项目级(每个项目不同)、用例级(每个任务不同)。敏感信息必须隔离出代码。

最终方案:

  • 系统级:config.yaml(LLM provider、API Key、超时、headless)
  • 项目级:project.config.md(base_url、认证信息、浏览器类型)
  • 用例级:task.txt(自然语言任务描述)

十一、完整执行流程(最终版)

开始
  │
  ├─ 环境健康检查(Playwright / MCP / LLM API 是否可用)
  │   └─ 不健康 → 尝试修复 → 仍失败 → 终止
  │
  ├─ 扫描 tasks 目录,整理散落 txt
  │
  ├─ 对每个用例:
  │   │
  │   ├─ 读取 task.txt
  │   │
  │   ├─ 有现成脚本且未失效?
  │   │   ├─ 是 → 直接执行
  │   │   └─ 否 → MCP 页面探测 → AI 生成脚本 → 保存
  │   │
  │   ├─ 执行脚本(每步截图 + trace + watchdog)
  │   │   ├─ 成功 → AI 业务断言(截图判断 + DOM 双保险)
  │   │   │           ├─ 通过 → 记录成功案例到知识库
  │   │   │           └─ 不通过 → 记录失败原因
  │   │   └─ 失败 → 收集截图 / 日志 / trace
  │   │       │
  │   │       ├─ 查知识库(历史解决方案)
  │   │       │
  │   │       ├─ 第 1/2 轮修复:
  │   │       │   ├─ 组装修复 Prompt(错误 + 截图 + trace + 历史方案)
  │   │       │   ├─ 调用 AI 修复(强制只改出错步)→ 新脚本
  │   │       │   └─ 重新执行
  │   │       │
  │   │       └─ 第 3 次仍失败 → 终止,记录失败到知识库
  │   │
  │   └─ 下一个用例
  │
  └─ 生成最终报告(成功率、耗时、失败模式)

十二、核心口诀(用血换来的)

脚本生成

先看页面再写码,选择器要验过才能用。

执行修复

失败了给截图,修代码只改出错步。

断言验证

AI 断言看截图,DOM 验证防幻觉。

定位策略

SPA 别单吊一个选择器,role/text/class 多备几个。

认证注入

init_script 在导航前,跳 login 就是注入败。

过程观测

没有截图和 trace,失败就是黑盒。

Prompt 工程

System 管规则,User 管内容,别在四个地方写同一条规则。

知识库

失败比成功更值得存,下次修得更快。


十三、架构决策检查清单

如果你也在搭类似的系统,每做一个设计决策前,对照一下:

检查项 如果回答"否",你迟早会踩坑
生成脚本前,AI 是否见过页面结构? 盲写 = 瞎猜选择器
每个选择器是否有 2-3 个备选? SPA 一重构就崩
执行过程是否有截图 + trace + 日志? 失败无法诊断
脚本卡住是否有 watchdog 强制终止? 一步卡死拖垮整体
AI 断言后是否有 DOM 二次验证? AI 幻觉导致假通过
修复时是否限制只改出错步骤? 修一步坏两步
修复是否有轮数上限? 无限循环耗额度
知识库是否记录了失败案例? 重复踩同一个坑
启动链路是否抽离成独立模块? 认证逻辑分散、不一致
Prompt 是否分层(System / User)? 同一规则四处重复、前后矛盾
敏感配置是否隔离出代码? API Key 泄露风险

十四、建议动手验证的架构假设

  1. 盲写 vs 探测后生成:故意不给页面结构,让 AI 生成脚本,观察选择器准确率
  2. 单选择器 vs 多策略:用一个选择器跑一周,观察 SPA 重构后的稳定性差异
  3. 无 trace vs 有 trace:故意制造一个失败,对比有/无 trace 时的诊断效率
  4. 单保险 vs 双保险断言:构造一个 AI 会误判的截图,验证 DOM 二次校验能否拦住
  5. 有/无知识库修复:同一个错误出现两次,对比有/无历史方案时的修复质量和速度
  6. 集中启动 vs 分散启动:token 过期时,对比修改一处 vs 修改 N 处的工作量
  7. CLI 方案稳定性:在 Windows 上连续调用 20 次 CLI 生成,观察卡死率和空响应率

最后更新:2026-05-06

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐