☰
智能构建:用LLM Agent实现自动化测试脚本的生成与失败修复闭环
2026/9/30 4:32:35 网站建设 项目流程

上周我在排查一套UI自动化脚本时,发现一件很讽刺的事:自动化测试脚本本身,恰恰是全团队自动化程度最低的东西。80多条核心用例,两位测试开发在维护,系统一改版,选择器红一大片,大部分时间不是花在写脚本,而是花在"读懂用例、追踪页面、修复断言"这些重复劳动上。所以当团队让我调研"智能构建"时,我意识到它不是又一个新AI玩具,而是应该先把这套最耗人的流程用大模型重新做一遍。这篇文章就是这次落地的完整记录。

这里所说的"智能构建",我指的是一个很具体的工程路径:让大模型扮演一个会写脚本的测试开发工程师,从测试用例出发,自动完成脚本生成、执行验证、失败修复的闭环。它和你可能见过的"AI补全代码""根据截图生成脚本"不一样,后者只是单点辅助,前者是一个可持续运转的自动化系统。文章会先讲清楚为什么非做不可,再给出技术选型、核心代码链路,最后是我在实测中踩过的一组坑。适合正在做自动化测试、想用LLM Agent改造现有测试基建的团队参考。

1. 智能构建不是玄学:自动化脚本从手写走向AI生成的必然逻辑

1.1 传统自动化脚本的真正瓶颈

先别急着谈大模型,我们把旧方案为什么累拆开看。手写自动化脚本有三个隐性成本。

第一是编写成本。一条中等复杂度的UI用例,从阅读需求、定位元素、设计断言到跑通,一个熟练的测试开发大概需要半天。如果你面对的是一百条用例,光首轮开发就是几十人天。第二是维护成本,而且这个成本是持续发生的。前端工程师改了某个按钮的class,脚本就红了;产品改了按钮文案,断言又挂了。每一条失败都要人去看日志、截图、DOM结构,才能判断是产品bug还是脚本该修。第三是用例到代码的语义断层。测试用例通常是自然语言写的,比如"用户点击收货地址列表中的编辑按钮,页面弹出地址编辑弹窗,修改手机号后点击保存,提示保存成功"。人读这句话没有歧义,但代码里"编辑按钮"到底是哪个元素、"保存成功"出现在哪里,全靠人来翻译。一旦写脚本的人和写用例的人不是同一个,这个翻译过程还会进一步失真。

我们组当时的真实数据是:74条核心用例的手写成本大约24人天,但之后每个月平均要花6到8人天去维护。也就是说,三个月之后,维护成本就超过了最初编写成本。这个换手率太扎心了。

1.2 智能构建的定义与边界

我见过不少人对"智能构建"的理解停留在"让AI自动写代码",这其实窄了。如果只是自动写代码,那市面上早就有代码生成插件,为什么自动化测试团队还是没有质的改变?因为真正的成本大头不在"写",而在"理解用例"和"失败后修复"。

所以我在项目里对智能构建的定位是这样的:以LLM为推理核心,把测试用例解析、脚本生成、执行验证、失败修复整合成一个自动化闭环的工程方法。它包含四个能力:

  • 理解:读取自然语言或表格形式的测试用例,抽取步骤、数据、断言。
  • 生成:根据页面结构和用例语义,输出可执行的UI自动化脚本。
  • 执行:调用Playwright等工具真实跑一遍,收集结果、截图、页面快照。
  • 修复:执行失败时,把报错信息送回到模型,由模型判断是脚本问题还是产品问题,并生成修复方案。

这个定义有一个好处:它把所有环节都纳入同一个Agent循环,而不是把AI当成一个偶尔帮忙的代码补全工具。也正因为如此,它才配叫"构建",而不是"生成"。

1.3 为什么现在才敢做这件事

可能在五六年前也有人想过"用模板自动生成脚本",甚至有人做了关键字驱动的框架,把常用操作封装成关键字,然后从Excel用例自动映射成脚本。那种方式的问题在于:它只能处理严格格式化的用例,一旦用例写法稍微口语化,映射就断了;更致命的是,它不具备失败后的推理修复能力。脚本一红,还是得人来看,所以最后大部分团队又退回手写。

现在不一样。大模型给了我们两个关键突破:一是能处理语义模糊的自然语言,二是具备诊断修复的推理能力。这两点恰好补齐了传统模板方案的短板。所以"智能构建"能落地,不是因为它听起来高级,而是模型能力刚好跨过了那条门槛。接下来要做的,就是把这些能力严谨地组装成工程系统。

2. 技术选型:LangChain管大脑,Playwright管手脚,剩下交给平台

2.1 Agent框架:为什么用LangChain而不是裸调API

最初我做POC时,直接在代码里反复调用模型API,很快发现撑不住。因为一个完整的脚本Agent至少有五个环节要处理:解析用例结构、生成代码、校验代码、执行、根据失败反馈再次推理。如果用裸API,这些环节之间的状态传递、条件分支、重试逻辑全都要自己手写,代码量非常大,而且容易乱。

LangChain的价值在于把"Agent循环"这个抽象做得比较完整:你可以定义工具、结构化输出、多轮对话、记忆,甚至通过LangGraph把整个流程建模成有向图。我们最终选型用了LangGraph来编排状态流,但对外统一叫LangChain体系,因为它的Agent概念大多数人更熟悉。选它还有一个现实原因:团队里其他同学以后接手时,社区资料多、踩坑经验容易搜到,不会出现一个人写完别人看不懂的情况。

如果你现在不想引入框架,也可以自己实现一个极简循环:调模型、提取JSON、执行工具、把结果拼进下一轮上下文。前期POC完全可以这样做,等确认流程稳定了再迁移到框架。没必要一上来就被框架绑死。

2.2 执行器:Playwright比Selenium和Cypress更合适

执行层我几乎没犹豫就选了Playwright。不是说Selenium不行,而是智能构建对执行器有两个独特要求,Playwright匹配得更好。

第一个要求是选择器的语义化。模型生成代码时,如果靠CSS选择器或XPath,大概率会生成又长又脆的路径。Playwright的locator API支持getByRole、getByText、getByTestId,这些语义化查询对模型来说更友好。模型只需要说"我要点一个名为'编辑'的按钮",代码上就对应page.getByRole('button', name='编辑'),语义几乎可以直译。第二个要求是可观测性。Playwright自带Trace Viewer和自动截图,执行失败后能拿到完整的页面快照、网络请求和源码位置,这些信息就是喂给模型做修复的"病历",价值非常大。

下面是我在选型时做的简单对比:

能力SeleniumCypressPlaywright
locator语义化查询较差一般强
自动等待弱,需手动处理内置内置且策略丰富
失败诊断产物截图有限截图+视频截图+Trace+网络快照
多浏览器支持好受限好
对LLM生成友好度低中高

2.3 上层平台:为什么还要接一个智能体平台

Agent本身跑在命令行里没问题,但要交付给团队成员用,就差得远了。你需要一个界面让他们输入用例、查看历史记录、一键触发运行、接收失败通知。这些能力自己写也能写,但重复造轮子不值。

我当时是在AI Studio这类智能体平台上收口的,它解决了几件事:可视化对话界面、知识库挂载、运行历史、权限管理。热词里提到的"在AI Studio上搭建智能体应用",本质就是这个玩法。你可以把本地LangChain Agent封装成API服务,再把它挂到平台上变成一个"自动化脚本助手";也可以直接在平台里配置大模型、知识库和工具,让Agent跑在平台托管环境里。两种方式我们都验证过,各有优劣,后面第四章会详细说。

到这里,整套架构可以这样理解:平台负责接待用户、管理知识、控制权限,LangChain负责动脑编排,Playwright负责动手执行,大模型负责把两边串起来。用户在平台上说一句"帮我生成登录模块的自动化脚本",最终落到的是真实可运行的Playwright代码。

3. 从测试用例到可执行脚本:Agent的核心链路实现

3.1 用例结构化:先让机器读懂测试用例

智能构建的第一步,不是让模型直接写代码,而是先让模型把自然语言用例翻译成结构化数据。这一步不做,后续生成代码时模型只能靠"猜",稳定性完全没有保障。

我们团队内部使用Flywheel(一个内部管理工具)管理用例,导出以后是Excel表格。每一行大致包含:模块、用例标题、前置条件、操作步骤、预期结果。为了让模型能稳定解析,我定义了一个Pydantic模型,把用例内容固定成如下结构:

from pydantic import BaseModel from typing import List class TestStep(BaseModel): action: str # 操作动作,如 click / fill / assert target: str # 操作目标,如“编辑按钮”“手机号输入框” value: str = "" # 输入值,如手机号 remark: str = "" # 补充说明 class TestCase(BaseModel): module: str # 所属模块 title: str # 用例标题 preconditions: List[str] # 前置条件 steps: List[TestStep] # 操作步骤 expected: List[str] # 预期结果

解析时,我让LangChain的OutputParser把模型回复转成JSON:

from langchain.output_parsers import PydanticOutputParser from langchain.prompts import PromptTemplate from langchain_openai import ChatOpenAI parser = PydanticOutputParser(pydantic_object=TestCase) prompt = PromptTemplate( template="你是测试用例分析专家。请将下面的用例内容解析成结构化JSON。\n{format_instructions}\n\n用例内容:\n{raw_text}", input_variables=["raw_text"], partial_variables={"format_instructions": parser.get_format_instructions()}, ) llm = ChatOpenAI(model="gpt-4o", temperature=0) chain = prompt | llm | parser parsed_case = chain.invoke({"raw_text": raw_test_case_text})

这里有一个很关键的细节:temperature=0。生成代码和解析结构化文本不是创意任务,温度一旦调高,模型就开始"发挥",输出格式时好时坏。我踩过这个坑,后面只能说浪费了一下午。

3.2 Prompt模板设计:让模型输出能直接执行的Playwright代码

结构化用例拿到手之后,才能进入真正的脚本生成环节。这里Prompt的设计直接决定成败,我把经验总结成"四段式"。

第一段是角色设定,我会非常明确地告诉模型:"你是一名资深测试开发工程师,熟悉Playwright,请根据以下结构化测试用例生成完整的Python自动化测试脚本。"第二段是输入约束,把之前解析出来的TestCase以JSON格式放进去,同时附上当前页面可能用到的关键DOM信息。第三段是输出格式,这是最容易翻车的地方——你必须规定模型返回JSON,且JSON中包含imports、setup、actions、assertions四个字段,actions和assertions都必须是代码字符串列表,而不是整段Markdown代码块。用JSON包裹代码,后面解析时会省非常多事。第四段是规则清单,我会把不能做的事显式写进去,例如"不允许删除用例中的任何断言""必须使用语义化locator""不得访问外网域名"。

一段精简版的System Prompt大概长这样:

system_prompt = """ 你是一名资深测试开发工程师,擅长Playwright。 你的任务是根据结构化测试用例生成可直接运行的Python自动化脚本。 规则: 1. 只准使用 page.get_by_role / get_by_testid / get_by_text 这类语义化locator。 2. 必须完整保留用例中的所有断言,禁止删除或弱化。 3. 不需要处理登录前置,脚本开头预留 login() 函数调用。 4. 输出格式必须是JSON: { "imports": "from playwright.sync_api import Page", "setup": "def login(page: Page): ...", "actions": ["page.get_by_role('button', name='编辑').click()", "..."], "assertions": ["expect(page.get_by_text('保存成功')).toBe_visible()"] } 5. 不要输出任何解释性文字,不要输出Markdown代码块。 """

这里最容易被忽视的是第5条。模型默认很喜欢输出带```python的Markdown代码块,看起来好看,但解析的时候你要去剥壳,而且代码块内可能还混着解释文字。直接用JSON约束输出,一行json.loads就搞定。

3.3 生成后的静态校验与试运行:AI写的代码不能盲目执行

模型生成的代码,第一版能不能跑通,取决于用例描述质量和页面结构复杂度。哪怕模型写得很合理,我也强烈建议做一个"安全闸门"再让它真正执行。

我做的静态校验包含三层:第一层用Python的ast库检查语法,跑不过语法就根本没资格进浏览器;第二层检查脚本里有没有危险调用,禁止出现os.system、subprocess、requests这类模块,防止模型生成一个会请求外部服务的脚本;第三层校验落地域名白名单,只允许访问被测系统的测试环境域名。下面是我写的一个简化版本:

import ast def validate_script(code: str) -> list: errors = [] try: tree = ast.parse(code) except SyntaxError as e: return [f"语法错误: {e}"] allowed_modules = {"playwright.sync_api", "re", "time", "json"} for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: if not alias.name.startswith(tuple(allowed_modules)): errors.append(f"禁止导入模块: {alias.name}") elif isinstance(node, ast.ImportFrom): if not node.module or not node.module.startswith(tuple(allowed_modules)): errors.append(f"禁止导入模块: {node.module}") return errors

校验通过的脚本,我会先让它跑一个"headless冒烟",也就是不截图不录视频,单纯验证脚本能完整走完。如果冒烟挂了,就进入下一节的修复循环。对于被拒绝的网络请求,我还会在产物里用红色标记出来,方便复查是不是被测系统自身的问题。

3.4 失败反馈与自我修复:智能构建最有价值的一环

这是我认为整个体系最值钱的地方,也是"智能构建"区别于普通代码生成的关键:失败之后能自己看、自己诊断、自己修。

执行失败后,我会把这些信息全部打成一个"诊断包"送回到模型:

  • 原始测试用例JSON
  • 生成的完整脚本
  • Playwright报错的traceback
  • 失败时的页面截图(转成base64)
  • 页面关键区域的可访问性快照(accessibility snapshot)

然后让模型判断失败原因。我定义了一个枚举:SELECTOR_CHANGED(选择器失效)、ASSERTION_TIMEOUT(断言超时)、PRODUCT_BUG(疑似产品缺陷)、ENV_ISSUE(环境问题)、UNKNOWN(无法判断)。要求模型必须给出一种判定,并输出修复后的代码补丁。如果是PRODUCT_BUG,模型不会乱改脚本,而是返回一个"疑似产品缺陷报告",由人来跟进。

修复循环的简化流程是这样的:

MAX_REPAIR_ROUNDS = 3 async def build_and_run(test_case, page_snapshot): code = await generate_code(test_case, page_snapshot) for round in range(MAX_REPAIR_ROUNDS): errors = validate_script(code) if errors: code = await repair_code(code, "\n".join(errors)) continue report = await run_playwright(code) if report.is_success: return code, report code = await repair_code(code, report.build_diagnosis()) return None, report # 降级给人工处理

这里有个经验之谈:修复循环不是越多越好。我们在项目里试过把最大重试数从3提到5,成功率并没有明显上升,成本却涨了不少。因为模型连续两轮修不对的时候,第三轮大概率是在"硬编"一个掩盖问题的新脚本,而不是真正理解报错。最终我把阈值定在3轮,交给人来处理反而更快。

在我们这个项目里,首次生成的直接通过率只有六成左右,但加上修复循环后,最终能稳定跑到85%。那剩下的15%,大多是产品端真的改了业务流程、需要产品经理拍板的问题。

4. 在智能体平台上收口:部署成可用的自动化助手

4.1 为什么还需要一个平台层

命令行能跑通的Agent,离团队可用还有不少距离。团队成员不一定想看你启动一个Python进程,他们想要的是:在界面里输入一条用例,点击执行,然后收到一份"脚本已生成并运行成功"的报告。所以我们需要一个收口层。

这个收口层目前最省事的做法就是用AI Studio这类智能体平台。它把几件本来很麻烦的事直接做掉了:

  • 可视化对话:成员可以用自然语言和助手交互。
  • 知识库挂载:把你们的测试规范、命名约定、历史踩坑文档放进去,Agent回答和生成会明显更符合团队习惯。
  • 权限与审计:谁能触发脚本、谁能看报告、谁可以修改Prompt,都有人管。
  • 定时触发:可以设定每天凌晨自动跑一遍冒烟用例,把结果推到群里。

4.2 接入方式一:把LangChain Agent封装成API

如果你的Agent逻辑已经很完善,比如你在LangChain/LangGraph里做了很多自定义状态流,最稳妥的接法就是把它封装成一个独立服务,再把服务作为一个"工具"挂到平台上。

这里给一个FastAPI封装的极简示例:

from fastapi import FastAPI from pydantic import BaseModel from your_agent import build_automation_script app = FastAPI() class AgentRequest(BaseModel): raw_case: str target_env: str = "staging" class AgentResponse(BaseModel): code: str = "" status: str = "success" report: dict = {} @app.post("/agent/build_script") def build_script(req: AgentRequest): result = build_automation_script(req.raw_case, req.target_env) return AgentResponse(**result)

封装好之后,在平台侧配置一个"技能/工具",指向这个API。用户说"生成脚本",平台就帮用户调用这个接口。这种方式最大的好处是Agent逻辑不受平台限制,换平台只需要重新配置工具地址。

4.3 接入方式二:把RAG知识库挂给Agent

第二种常用方式,是直接用平台自带的能力,把核心逻辑改成"知识库检索+Prompt编排"。这种方式特别适合不需要强工具调用的场景,比如热词里提到的制度条例学习助手。

这类助手和脚本生成Agent共用同一套框架:用户提问,系统先从知识库检索相关制度条款,再把检索结果和问题拼到一起交给模型,最后输出答案。我当时做试验时,只用了很简单的三步:上传制度文档到平台知识库、配置一个检索分段阈值、设置"只依据提供资料回答"的Prompt约束。效果意外地好,同事问"请假三天以上需要什么流程"这类问题,回答比之前翻文档快太多。

对比下来,我建议团队这么选:如果你的核心流程需要操作浏览器、执行代码、处理复杂状态流,用第一种封装API;如果只是"文档问答、知识检索、内容生成"这类轻场景,直接在平台原生搭建更省事。

4.4 让这个助手真正"被用起来"的细节配置

部署好只是开始,真正被团队高频使用,还需要一些容易被忽略的配置。我列一下我们团队的落地清单:

  • 运行白名单:只允许在staging环境触发脚本,生产环境一律禁止,防止智能体误操作。
  • 失败分级通知:脚本失败后,根据模型判定的原因分通道发送——PRODUCT_BUG发到测试群,SELECTOR_CHANGED发到自动化维护群,ENV_ISSUE发到运维群。
  • 历史记录归档:每次运行的输入、脚本、截图、诊断结果都归档,方便复盘模型到底在哪些环节持续犯错。
  • 提示语规范化:在平台里预设几个命令模板,比如"生成登录模块脚本""修复最近失败的脚本",用户不用组织措辞,照着点就行。

5. 实测记录:五个让脚本Agent翻车的坑与处理方案

5.1 模型自作主张删掉了断言

第一个坑是模型在生成脚本时,会把用例里的断言"优化"掉。比如用例写着"断言页面出现'保存成功'四个字",模型生成的代码却只检查了某个按钮是否存在。代码能跑,但测试强度大打折扣。

我后来分析,原因出在训练数据里自动生成的脚本往往都只做冒烟级别的检查,模型默认感觉断言越少越稳定。解决办法有两个:一是在Prompt里用高优先级规则声明"禁止删减断言",并把原用例的expected字段逐条映射到脚本里;二是在静态校验时做一次断言数量对比,生成的脚本里expect调用次数少于用例断言条数就报警,不让执行。

5.2 选择器生成得太"聪明"反而更脆弱

模型很喜欢用get_by_text,因为文本描述最直观。问题是业务文案一旦修改,脚本立刻挂掉。我们遇到过一个典型case:促销活动把"立即购买"改成"立即抢购",一整批脚本全部失败。

后来我在Prompt里加了一个选择器优先级规则,并用表格固定下来:

优先级选择器类型适用场景
1>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询