ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI智能体+Playwright:让UI自动化脚本自己写自己

AI智能体+Playwright:让UI自动化脚本自己写自己

1. 当UI自动化脚本开始自己写自己

UI自动化测试这件事,做过的人都懂——写脚本的时间往往比跑脚本的时间还长。页面改一个按钮的ID,几十个用例集体报红;新来一个需求,光是定位元素、封装Page Object就要耗掉大半天。我身边不少做测试开发的朋友都吐槽过:自动化脚本的维护成本,有时候比手工测试还高。

但最近半年,我在几个内部项目里尝试了一条新路子:让AI智能体来接管UI自动化脚本的编写工作。具体来说,就是把Agent Skills(智能体技能)和Playwright这套浏览器自动化框架结合起来,搭建一个能"自己看页面、自己想步骤、自己写脚本"的智能体。你只需要用自然语言描述测试意图,比如"验证登录失败三次后账号被锁定",它就能自动打开浏览器、分析页面结构、生成可执行的Playwright脚本,甚至跑一遍验证结果。

这套方案解决的核心问题是:把UI自动化的门槛从"会写代码"降到"会说需求"。适合谁参考?测试开发工程师、前端团队里兼做E2E测试的同学、以及想用AI提效但不想从头造轮子的独立开发者。读完你至少能搞清楚三件事:Agent Skills的机制怎么和Playwright对接、脚本自动生成的关键环节在哪里、以及实际跑起来会踩哪些坑。

需要先说明的是,下面涉及的具体实现细节,部分是基于我在模拟项目中的实践总结,部分是基于当前主流技术方案的合理推演。我会尽量把"为什么这么做"讲透,而不是只丢一堆代码。

2. 拆解这套智能体的能力边界与核心组件

2.1 Agent Skills到底是什么,为什么不是简单的Prompt

很多人第一次听到"Agent Skills"会以为就是给大模型写一段系统提示词。不完全是。Prompt是"告诉模型该做什么",而Skill是"给模型一套可调用的能力包"。打个比方:Prompt像是给新员工一份岗位说明书,Skill则是给他配了一台电脑、一套工具软件和一本操作手册。

在这套方案里,Agent Skills至少包含三类东西:

  • 工具定义(Tool Definition):告诉智能体有哪些函数可以调用,比如open_browser、get_page_dom、click_element、generate_script。每个工具都有明确的入参和出参格式。
  • 执行逻辑(Execution Logic):工具被调用后实际执行的代码,这部分是确定性逻辑,不依赖模型推理。比如打开浏览器就是调Playwright的chromium.launch()。
  • 上下文管理(Context Management):智能体在多轮交互中需要记住当前页面状态、已执行步骤、失败原因等信息。这部分决定了它能不能"连续思考"而不是每次从零开始。

为什么不能只用Prompt?因为纯Prompt方案下,模型只能"说"不能"做"。你让它分析页面,它只能根据你粘贴的HTML猜;你让它验证结果,它只能根据你描述的现象判断。而有了Skill,它可以主动去获取真实页面信息,形成"感知-决策-执行"的闭环。

2.2 Playwright在这个架构里扮演什么角色

Playwright是微软开源的浏览器自动化框架,支持Chromium、Firefox、WebKit三大引擎。相比Selenium,它的优势在于:自动等待元素可交互、内置网络拦截、支持多标签页和iframe穿透、API设计更现代。

在这套智能体架构里,Playwright不是被替代的对象,而是被"封装"的对象。具体来说:

层级组件职责
决策层大语言模型理解需求、规划步骤、生成脚本
技能层Agent Skills提供工具调用接口、管理上下文
执行层Playwright实际操控浏览器、获取页面状态
验证层断言引擎比对预期结果与实际结果

关键点在于:Playwright的API被包装成Skill工具后,模型不需要知道page.click(selector)的具体写法,只需要知道"我要点击某个元素"。这降低了模型生成错误代码的概率,因为底层调用是确定性的。

2.3 为什么选Playwright而不是其他方案

市面上浏览器自动化工具不少,我选Playwright主要基于三个实际考量:

第一,自动等待机制。Selenium时代最头疼的就是sleep和WebDriverWait的取舍,Playwright内置了actionability检查,元素不可点击时会自动重试,这减少了智能体生成"等待逻辑"的负担。

第二,选择器策略丰富。Playwright支持CSS、XPath、文本、角色(role)、测试ID等多种定位方式。智能体在分析页面时,可以根据元素特征选择最稳定的定位策略,而不是死磕某一种。

第三,Trace Viewer。这个功能对调试智能体生成的脚本极其有用——每一步操作都有截图、DOM快照和网络记录,出问题时能快速定位是模型决策错了还是页面本身有问题。

注意:Playwright虽然支持多语言(Python、JavaScript、Java、.NET),但在这套方案里建议统一用Python或TypeScript,因为Agent Skills的生态在这两个语言下最成熟。

3. 让智能体"看懂"页面的关键机制

3.1 页面信息怎么喂给模型才有效

这是整个方案里最容易被低估的环节。很多人以为把整个HTML丢给模型就行了,实测下来效果很差——一个中等复杂度的页面HTML动辄几万字符,模型要么超上下文限制,要么被大量无关标签干扰。

我的做法是分层提取页面信息:

第一层是可交互元素清单。通过Playwright的page.locator()配合角色选择器,提取所有按钮、输入框、链接、下拉框等可操作元素,输出结构化列表:

# 提取可交互元素的简化逻辑 interactive_elements = [] for role in ['button', 'textbox', 'link', 'combobox', 'checkbox']: locator = page.get_by_role(role) count = locator.count() for i in range(count): el = locator.nth(i) interactive_elements.append({ 'role': role, 'text': el.inner_text()[:50], 'visible': el.is_visible(), 'enabled': el.is_enabled() })

第二层是页面语义摘要。用无障碍树(Accessibility Tree)代替原始DOM,因为无障碍树本身就是为"理解页面结构"设计的,节点数量通常只有DOM的十分之一。

第三层是视觉截图。对于布局复杂或需要视觉判断的场景,把截图传给支持多模态的模型,让它结合视觉信息做决策。

这三层信息组合起来,模型既能知道"页面上有什么",又能知道"长什么样",还能知道"能不能点"。

3.2 元素定位策略的自动选择逻辑

智能体生成脚本时,定位器选得好不好直接决定脚本的稳定性。我给它设定了一套优先级规则:

  1. 测试ID优先:如果元素有>{ "steps": [ {"action": "navigate", "target": "login_page"}, {"action": "fill", "target": "username", "value": "test_user"}, {"action": "fill", "target": "password", "value": "wrong_pass"}, {"action": "click", "target": "login_button"}, {"action": "repeat", "times": 3, "steps": ["fill_password", "click_login"]}, {"action": "assert_text", "target": "page", "expected": "账号已锁定"} ] }

    这样做的好处是:步骤先结构化,再生成代码,中间多了一层校验。如果模型拆解的步骤有问题,可以在生成代码前就发现。

    4.2 脚本生成:模板填充还是自由生成

    这里有个关键选择:是让模型自由生成Playwright代码,还是用预定义模板填充?

    我两种都试过。自由生成的代码灵活,但容易出语法错误、API误用、定位器不稳定。模板填充稳定,但遇到模板没覆盖的场景就抓瞎。

    最终我采用的是混合策略:

    • 常见操作(导航、点击、填表、断言)用模板生成,保证正确性。
    • 复杂逻辑(条件分支、循环、动态等待)让模型自由生成,但生成后必须通过语法检查和试运行。
    • 所有生成的代码都要经过一个"静态检查"环节,用AST解析确认没有调用不存在的API。

    模板的粒度我控制在"单个操作"级别,而不是"整个用例"级别。比如点击操作有专门的模板:

    def generate_click_code(locator_strategy, locator_value): return f"page.{locator_strategy}({repr(locator_value)}).click()"

    这样既保证了单步正确性,又保留了组合的灵活性。

    4.3 试运行与自动修复

    脚本生成后不能直接交付,必须试运行。我的流程是:

    1. 在隔离的浏览器上下文中执行生成的脚本。
    2. 捕获所有异常和断言失败。
    3. 如果失败,把错误信息、当前页面截图、相关DOM片段回传给模型。
    4. 模型分析失败原因,生成修复方案。
    5. 修复后重新试运行,最多重试3次。

    这个自动修复环节是整套方案里最有价值的部分之一。实测中,大约60%的首次失败能通过自动修复解决,剩下的40%通常是需求本身有歧义或页面确实有问题,需要人工介入。

    提示:自动修复一定要设重试上限,否则模型可能陷入"改了错、错了改"的死循环。3次是我的经验值,超过3次基本说明问题不在脚本层面。

    5. 实测中那些文档不会告诉你的坑

    5.1 动态加载页面让智能体"看走眼"

    现代前端大量使用懒加载和异步渲染。智能体打开页面时,如果立即提取元素清单,很可能拿到的是空列表或不完整列表。我踩过的坑是:模型看到页面上没有登录按钮,就生成了一个"等待登录按钮出现"的脚本,但实际上按钮只是还没渲染出来。

    解决方案是在Skill里强制加入页面稳定检测:

    def wait_for_page_stable(page, timeout=5000): page.wait_for_load_state('networkidle') # 额外等待DOM变化停止 page.evaluate("""() => { return new Promise(resolve => { let timer; const observer = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(() => { observer.disconnect(); resolve(); }, 500); }); observer.observe(document.body, {childList: true, subtree: true}); setTimeout(() => { observer.disconnect(); resolve(); }, 3000); }); }""")

    这段逻辑的意思是:等网络请求安静下来后,再监听DOM变化,如果500毫秒内没有新变化,就认为页面稳定了。这个等待策略比单纯用wait_for_timeout靠谱得多。

    5.2 模型生成的定位器"看起来对但跑不通"

    这是高频问题。模型根据页面文本生成了get_by_text('提交'),但页面上实际有两个"提交"按钮——一个是表单提交,一个是弹窗里的确认。模型不知道这个歧义,生成的脚本就会报"strict mode violation"。

    我的应对方式是在工具描述里明确要求:当定位器可能匹配多个元素时,必须先用count()检查,再用nth()或更精确的属性缩小范围。同时,在页面信息提取阶段,我会把每个元素的唯一特征(如ID、name、位置索引)都带上,让模型有足够信息做精确匹配。

    另一个常见问题是iframe。如果目标元素在iframe里,模型生成的page.get_by_role('button')会找不到。我在Skill里加了iframe自动检测和穿透逻辑,模型只需要说"点击提交按钮",底层会自动处理frame切换。

    5.3 断言写得太"死"导致误报

    模型倾向于生成精确匹配的断言,比如expect(page.get_by_text('登录成功')).to_be_visible()。但实际页面上可能是"登录成功!"或者"登录成功,正在跳转..."。这种精确匹配很容易误报。

    我的经验是:在Skill里预设断言策略,优先用包含匹配而非精确匹配。对于文本断言,用to_contain_text代替to_have_text;对于URL断言,用正则而非全等。同时告诉模型:断言的目标是"验证核心意图",不是"逐字比对"。

    5.4 Token消耗比预期高得多

    这是成本问题。一个中等复杂度的测试用例,从需求解析到脚本生成再到试运行修复,可能消耗几万到十几万token。如果模型选的是高价位版本,单个用例的成本可能达到几块钱。

    控制成本的手段有几个:

    • 页面信息压缩:前面提到的分层提取,能把页面信息从几万字符压到几千字符。
    • 缓存常用操作:登录、导航这类高频操作,第一次生成后缓存起来,后续直接复用。
    • 小模型做粗活:需求解析、元素提取这类任务用便宜的小模型,只有脚本生成和修复用大模型。
    • 限制重试次数:前面说的3次上限,也是成本控制手段。

    5.5 生成脚本的可维护性被忽视

    智能体生成的脚本能跑通,不代表好维护。我见过模型生成的脚本里,定位器全是nth-child,变量名全是element1、element2。这种脚本跑一次没问题,页面一改就全废。

    我的做法是在Skill里加入代码规范约束:

    • 变量命名必须语义化,如login_button而非btn1。
    • 定位器必须遵循前面说的优先级规则。
    • 重复操作必须提取成函数。
    • 每个步骤必须加注释说明意图。

    这些约束会作为生成脚本时的"系统指令"传给模型,虽然不能100%保证,但能显著提升生成代码的可读性。

    6. 这套方案适合什么场景,不适合什么场景

    6.1 最适合的三类场景

    第一类:需求频繁变动的项目。传统自动化脚本改起来慢,智能体方案下,改一句自然语言描述就能重新生成脚本,响应速度快很多。

    第二类:页面结构相对规范的项目。如果项目本身有良好的测试ID体系、语义化的HTML结构,智能体的定位准确率会高很多。反之,如果页面全是div堆砌、没有语义标签,智能体也会抓瞎。

    第三类:探索性测试和冒烟测试。这类测试不需要覆盖所有边界,重点是快速验证核心流程。智能体生成脚本的速度优势在这类场景下最明显。

    6.2 暂时不建议用的场景

    高精度断言场景。比如金融系统里"金额必须精确到分"的验证,智能体生成的断言可能不够严谨,还是人工写更靠谱。

    复杂业务逻辑编排。涉及多系统交互、消息队列、数据库校验的端到端测试,目前智能体还难以独立完成,更适合作为辅助工具。

    性能敏感场景。智能体方案本身有模型推理开销,如果测试本身对执行速度有要求,这套方案反而会拖慢。

    6.3 和传统自动化方案的成本对比

    维度传统方案智能体方案
    首次编写成本高(人工写脚本)低(自然语言描述)
    维护成本高(页面改动需改脚本)中(重新生成或自动修复)
    执行成本低(纯代码执行)高(含模型推理)
    覆盖灵活性受限于已写脚本可动态扩展
    结果可靠性高(确定性逻辑)中(依赖模型判断)

    这张表的核心结论是:智能体方案把成本从"编写和维护"转移到了"执行"。如果你的项目页面变动频繁、编写人力紧张,这套方案划算;如果页面稳定、执行频率极高,传统方案可能更经济。

    7. 我在这套方案上积累的几条实操心得

    先说一个反直觉的体会:不要追求让智能体一次性生成完美脚本。我一开始也是这个思路,结果发现模型在"一次做对"的压力下反而容易过度设计,生成的脚本又长又复杂。后来我改成"先生成能跑的最小版本,再迭代优化",成功率高了很多。这就像写代码,先让测试通过,再重构。

    第二条心得是关于工具描述的写法。Skill里的工具描述不是写给人类看的文档,而是写给模型看的"使用说明"。我踩过的坑是:描述写得太简略,模型不知道怎么用;写得太详细,又占用了宝贵的上下文。我的经验是每个工具描述控制在三句话以内:这个工具做什么、什么时候用、关键参数是什么。细节放在参数说明里,不放在描述里。

    第三条是关于错误处理。模型生成的脚本里,错误处理往往很粗糙——要么没有try-catch,要么catch了什么都不做。我在Skill里强制要求:每个可能失败的操作都必须有明确的失败处理策略,要么重试,要么记录并继续,要么中止并报告。不允许"静默失败"。

    最后一条,也是我觉得最重要的:保留人工审核环节。智能体生成的脚本,在正式纳入回归测试集之前,必须经过人工review。不是不信任AI,而是UI自动化脚本一旦进入CI流程,出问题的排查成本很高。人工审核花的那几分钟,能省下后面几小时的排查时间。

    这套方案我还在持续迭代,目前最大的瓶颈是复杂页面的元素定位准确率。下一步打算尝试把视觉理解和DOM分析更深度地结合,让智能体不仅能"读"页面,还能"看"页面。如果你也在做类似尝试,欢迎交流踩坑经验。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进