基于LLM与Playwright构建AI测试智能体:实现自动化回归与持续巡检

基于LLM与Playwright构建AI测试智能体:实现自动化回归与持续巡检 1. 项目概述一个每三小时自动测试产品的AI智能体最近和几个做SaaS的朋友聊天大家普遍头疼一个问题产品上线后功能越来越多回归测试的成本高得吓人。每次发版前测试团队都要通宵达旦地跑用例稍微漏掉一个边缘场景线上就可能出问题。更麻烦的是有些偶发性的Bug在测试环境复现不了到了生产环境用户一用就崩搞得团队焦头烂额。我当时就在想能不能让测试这件事自己“动”起来不是简单地用脚本录放而是有一个真正能“理解”产品、能像真实用户一样思考、并且不知疲倦的智能体来持续不断地验证产品的核心流程。于是我花了几周时间捣鼓出了一个能每三小时自动运行、深度测试我自己产品的AI智能体。它不是什么遥不可及的黑科技核心就是利用现有的AI能力比如大语言模型的推理和规划能力加上自动化框架搭建的一个“数字质检员”。这个AI智能体对我而言已经从一个实验性项目变成了研发流程中不可或缺的一环。它不取代人工测试而是把测试工程师从重复、枯燥的回归任务中解放出来让他们能更专注于探索性测试和复杂场景设计。同时它像一个永不间断的哨兵以固定的频率巡检核心链路一旦发现异常比如页面元素丢失、接口返回错误、业务流程卡住能立刻通过钉钉、飞书或者邮件告警让我们在用户反馈之前就定位到问题。这篇文章我就来拆解一下这个AI测试智能体的设计思路、技术实现、以及我在搭建和运行过程中踩过的那些坑。无论你是开发者、测试工程师还是产品经理只要你的产品需要持续交付和稳定保障这套思路或许能给你带来一些启发。2. 整体架构与核心设计思路2.1 为什么是“智能体”而不仅仅是“自动化脚本”传统的UI自动化测试脚本比如用Selenium、Playwright写的本质上是“录放机”。它严格遵循预设的步骤点击这里输入那个检查某个元素是否存在。这种方式的优点是稳定、可预期。但缺点也非常明显脆弱。页面结构稍微一变比如一个按钮的class名改了脚本就挂了死板它无法处理脚本编写时未考虑到的情况维护成本高产品迭代越快脚本需要同步修改的地方就越多。我想要的“智能体”核心区别在于它具备一定的感知、决策和容错能力。它不仅仅是执行步骤而是理解任务目标。举个例子任务目标是“以管理员身份登录并创建一个新用户”。传统脚本会写成访问/login。在#username输入框输入 “admin”。在#password输入框输入 “123456”。点击#submit-btn。验证是否跳转到/dashboard。点击侧边栏的#user-management。点击#create-user-btn。……而AI智能体的“思考”过程更像是目标创建新用户。前提需要管理员权限。步骤1登录。我需要找到登录页面。当前页面是哪里如果是首页通常导航栏会有“登录”链接。我去找找看。它可能会通过分析页面DOM寻找包含“登录”、“Sign In”等文本或常见登录图标的元素。步骤2找到了登录表单。需要输入用户名和密码。我知道管理员的账号是admin密码从安全配置中读取。输入。步骤3提交表单。成功后我应该能看到管理员后台的特定元素比如“管理面板”标题或管理员独有的菜单。步骤4导航到用户管理。页面上可能有“用户”、“成员”、“团队”等相关的导航项。我需要识别并点击它。步骤5寻找创建用户的入口。可能是按钮也可能是“添加”图标。点击。步骤6填写创建用户的表单。表单字段可能动态变化我需要识别出“用户名”、“邮箱”、“角色”等字段并填入符合业务规则的测试数据比如生成一个随机邮箱避免重复。步骤7提交并验证。检查是否出现“创建成功”的提示或者在用户列表中能找到刚创建的用户。你会发现智能体不依赖于固定的CSS选择器或XPath。它通过自然语言理解页面内容根据目标动态规划行动路径。这带来了巨大的灵活性即使按钮位置变了、文本改了只要它的功能语义还在比如还是一个“提交”按钮智能体就有很大概率能识别并操作它。这极大地降低了维护成本。2.2 系统核心组件拆解我的这个每三小时运行的AI测试智能体主要由以下几个核心部分组成它们协同工作形成了一个闭环任务调度与协调中心大脑这是整个系统的指挥官。我使用了一个轻量级的编排框架比如LangGraph或自定义的状态机来定义测试流程。它负责接收“测试产品核心功能”这个高级指令并将其分解成一系列具体的原子任务Task例如“测试登录功能”、“测试下单流程”、“检查数据报表加载”等。它管理着任务队列、执行状态并处理任务之间的依赖关系例如必须先登录才能下单。AI规划与执行引擎核心这是智能体的“思考”器官。对于每个原子任务由一个LLM大语言模型驱动。我主要调用云端API如GPT-4、Claude-3或国内的一些合规大模型它的工作流程是感知Perception接收当前网页的DOM结构、截图可选以及任务描述。规划Planning分析当前状态和目标决定下一步应该做什么。例如“当前在登录页面目标是登录。我需要输入用户名和密码然后点击提交按钮。”行动生成Action Generation将规划出的“意图”转化为自动化框架如Playwright可以执行的具体命令。例如生成一个JSON对象{“action”: “fill”, “selector”: “input[placeholder‘用户名或邮箱’]”, “text”: “test_user”}。这里的选择器可以是基于语义的而不一定是固定ID。执行与观察自动化框架执行该动作然后获取执行后的新页面状态再次交给LLM进行感知进入下一个循环直到任务完成或失败。自动化执行器手脚我选择了Playwright作为底层自动化工具。相比SeleniumPlaywright对现代Web应用尤其是单页应用SPA的支持更好自动等待机制更智能且能轻松处理iframe、文件下载等场景。它忠实地执行AI引擎发出的指令并捕获执行结果、网络请求、控制台日志等丰富信息。状态管理与记忆模块智能体需要有短期记忆。例如在创建订单的流程中系统生成的订单号需要记住以便在后续的查询或取消任务中使用。我使用一个简单的键值存储如Redis或甚至一个内存字典来保存跨步骤的上下文信息。断言与异常处理机制判断任务完成后需要判断是否成功。这不仅仅是检查页面有没有报错。LLM会再次分析最终状态根据任务目标进行“语义断言”。例如对于“登录成功”的断言可能是“页面中出现了用户头像或‘欢迎回来’的文本”而不是死板地检查某个URL。对于异常系统会捕获Playwright的错误、网络超时、以及LLM自身判断的“无法达成目标”状态并进行分类元素未找到、业务流程错误、性能问题等。报告与告警系统输出每一次运行每三小时都会生成一份结构化的测试报告包括执行了哪些任务、成功/失败、耗时、关键步骤截图、错误日志和LLM的推理过程这对调试非常有用。如果出现失败系统会立即通过Webhook触发钉钉机器人告警将错误摘要和链接推送到相关群组。配置与数据工厂管理测试所需的配置如被测环境地址、测试账号、API密钥等。同时包含一个数据工厂用于生成符合业务规则的测试数据例如随机的用户名、邮箱、地址避免测试数据污染和生产数据冲突。2.3 关键技术选型背后的考量为什么用PlaywrightLLM而不是其他组合Playwright vs Selenium/CypressPlaywright由微软开发原生支持多浏览器Chromium, Firefox, WebKit且API设计非常现代。它的auto-waiting功能是巨大优势能自动等待元素可交互减少了测试脚本中大量硬编码的sleep让AI生成的指令更稳定。此外它的codegen和trace viewer工具对调试AI执行过程非常有帮助。LLM的选择我尝试过不同规模的模型。重量级的GPT-4在复杂任务规划和语义理解上表现最佳但成本较高。对于相对固定的流程性能稍弱但成本更低的模型如GPT-3.5-Turbo也可能够用。关键是要给模型提供清晰的指令Prompt和丰富的上下文页面信息。一个重要经验是将业务规则和操作约束明确写入Prompt比如“永远不要使用真实用户的信用卡信息进行测试”、“创建数据时使用test_前缀”。编排框架LangGraph非常适合构建有状态的、多步骤的智能体。它用图Graph来定义工作流节点是任务或LLM调用边是状态流转的条件直观且强大。如果不想引入新框架用Python的asyncio配合一个简单的状态机也能实现核心循环。这个架构的核心思想是“LLM负责思考「做什么」和「为什么」Playwright负责精准地执行「怎么做」”两者结合既拥有了人类般的适应性又具备了机器的精确性与速度。3. 实操搭建从零构建你的AI测试智能体3.1 环境准备与基础框架搭建首先你需要一个干净的Python环境建议3.9以上。我的项目结构大致如下ai_test_agent/ ├── config/ │ ├── __init__.py │ ├── settings.py # 存放环境变量、URL、账号等配置 │ └── prompts.py # 存放给LLM的各种指令模板 ├── core/ │ ├── __init__.py │ ├── agent.py # AI智能体核心类封装LLM调用和规划逻辑 │ ├── actions.py # 将LLM的输出解析为Playwright动作 │ ├── memory.py # 上下文记忆管理 │ └── orchestrator.py # 任务编排器 ├── tasks/ │ ├── __init__.py │ ├── base_task.py # 所有任务的基类 │ ├── login_task.py # 登录任务 │ ├── create_order_task.py # 下单任务 │ └── ... # 其他具体任务 ├── utils/ │ ├── data_factory.py # 测试数据生成 │ ├── reporter.py # 报告生成器 │ └── notifier.py # 告警通知 ├── requirements.txt ├── main_scheduler.py # 主调度入口处理3小时定时触发 └── .env # 环境变量文件切勿提交安装核心依赖。requirements.txt关键内容如下playwright1.40.0 openai1.0.0 # 或其他LLM SDK如 anthropic, dashscope langgraph0.0.5 # 可选用于高级编排 python-dotenv1.0.0 redis5.0.0 # 可选用于分布式记忆存储 schedule1.2.0 # 用于定时任务 pydantic2.0.0 # 用于数据验证和设置管理运行pip install -r requirements.txt安装依赖并安装Playwright浏览器playwright install chromium。注意LLM API密钥是最高机密务必通过环境变量.env文件管理绝对不要硬编码在代码中。.env文件内容类似OPENAI_API_KEYsk-...并在.gitignore中忽略它。3.2 设计核心的Agent类core/agent.py是这个系统的心脏。下面是一个高度简化的核心逻辑展示import asyncio from typing import Dict, Any, Optional from openai import AsyncOpenAI # 示例使用OpenAI from pydantic import BaseModel from playwright.async_api import Page class Action(BaseModel): 定义LLM可以执行的动作 action_type: str # 如 click, fill, navigate, assert selector: Optional[str] None # 可选元素选择器 text: Optional[str] None # 可选输入的文本 description: str # 动作的自然语言描述用于调试 class AITestAgent: def __init__(self, page: Page, llm_client, system_prompt: str): self.page page self.llm llm_client self.system_prompt system_prompt self.memory {} # 简单的内存字典 async def perceive(self) - str: 感知当前页面状态获取可供LLM分析的信息 # 1. 获取页面的简化DOM或可访问性树。全量DOM太大需要精简。 # 使用Playwright获取主要交互元素的简洁表示。 elements await self.page.evaluate( () { const items []; // 收集按钮、输入框、链接等关键交互元素 document.querySelectorAll(button, input, a, [rolebutton], [rolelink]).forEach(el { items.push({ tag: el.tagName, text: el.innerText?.slice(0, 50) || el.value || el.placeholder, ariaLabel: el.getAttribute(aria-label), type: el.type, placeholder: el.placeholder, // 生成一个相对稳定的选择器优先使用data-testid等测试属性 selector: el.getAttribute(data-testid) || (el.id ? #${el.id} : ) || (el.name ? [name${el.name}] : ) }); }); return items.filter(item item.selector || item.text); // 过滤掉无标识元素 } ) # 2. 获取当前URL和页面标题 url self.page.url title await self.page.title() # 3. 将信息格式化成LLM易于理解的文本 perception f当前页面标题{title}\\n当前URL{url}\\n\\n页面中可交互元素\\n for idx, el in enumerate(elements[:20]): # 限制数量避免token超限 perception f{idx1}. 标签{el[tag]} 文本/值{el.get(text)} 选择器参考{el.get(selector)}\\n return perception async def plan_and_act(self, task_description: str, max_steps: int 10) - Dict[str, Any]: 核心循环感知-规划-执行 for step in range(max_steps): print(f步骤 {step 1}) # 1. 感知 current_state await self.perceive() # 2. 规划调用LLM决定下一步动作 prompt f {self.system_prompt} 当前任务{task_description} 当前页面状态 {current_state} 你之前的操作记忆 {self.memory.get(last_action, 无)} 请根据当前状态和任务目标决定下一步做什么。你只能输出一个JSON对象格式必须严格如下 {{ thought: 你的思考过程分析当前情况, action: {{ action_type: click|fill|navigate|assert|complete|fail, selector: 可选CSS选择器或你从页面元素中推断的标识, text: 可选如果是fill动作需要输入的文本, description: 对该动作的自然语言描述 }}, is_task_complete: false }} 如果任务已经成功完成将is_task_complete设为true并且action的action_type设为complete。 如果任务确定无法完成将is_task_complete设为true并且action的action_type设为fail并在thought中说明原因。 try: response await self.llm.chat.completions.create( modelgpt-4-turbo-preview, messages[{role: user, content: prompt}], response_format{type: json_object}, temperature0.1 # 低随机性保证动作稳定 ) decision json.loads(response.choices[0].message.content) except Exception as e: return {status: error, message: fLLM调用失败: {e}} print(f思考{decision[thought]}) print(f动作{decision[action]}) # 3. 执行动作 action decision[action] self.memory[last_action] action[description] if action[action_type] complete: return {status: success, message: 任务完成} elif action[action_type] fail: return {status: failure, message: decision[thought]} # 执行具体的Playwright操作 try: if action[action_type] click: if action[selector]: await self.page.click(action[selector]) else: # 如果LLM没提供选择器可以尝试通过文本内容查找 await self.page.get_by_text(action[description]).click() elif action[action_type] fill: await self.page.fill(action[selector], action[text]) elif action[action_type] navigate: await self.page.goto(action[text]) # text字段存放URL elif action[action_type] assert: # 这里可以扩展为更复杂的断言逻辑 pass # 等待页面稳定 await self.page.wait_for_load_state(networkidle) except Exception as e: return {status: error, message: f执行动作失败: {e}, last_decision: decision} # 4. 短暂暂停模拟人类操作间隔 await asyncio.sleep(1) return {status: timeout, message: f达到最大步骤数 {max_steps}}这个AITestAgent类封装了感知页面、调用LLM决策、执行动作的核心循环。关键在于system_prompt的设计它定义了智能体的角色和行为准则。3.3 编写有效的System Prompt系统指令config/prompts.py中存放着给AI的“宪法”。一个优秀的Prompt能极大提升智能体的表现。我的基础系统指令如下BASE_SYSTEM_PROMPT 你是一个专业的Web应用测试AI助手。你的目标是通过浏览器自动化完成指定的测试任务。 请严格遵守以下规则 1. **安全与合规** - 你测试的是预生产或测试环境绝对不要访问或操作生产环境数据除非明确指定。 - 只能使用提供的测试账号不得尝试猜测或使用其他账号密码。 - 生成测试数据时如用户名、邮箱必须使用“test_”前缀或明显的测试模式避免与真实用户数据混淆。 2. **操作规范** - 仔细分析当前页面状态提供的元素列表后再做决定。 - 优先使用元素中提供的selector特别是data-testid进行操作它最稳定。 - 如果没有稳定的选择器再尝试通过元素的文本内容、标签类型、ARIA标签等属性来定位。 - 每次只执行一个清晰、简单的动作如点击一个按钮填写一个输入框。 - 执行动作后等待页面加载或状态更新系统会自动处理。 3. **任务理解** - 清晰理解最终任务目标。将复杂任务分解为小步骤。 - 如果遇到错误如元素找不到、页面无响应先尝试分析原因是否走错了流程并尝试1-2次替代方案如点击另一个看起来功能相同的按钮。 - 如果多次尝试后任务明显无法推进果断标记为失败并在思考中清晰说明原因。 4. **输出格式** - 你必须且只能输出一个合法的JSON对象包含thought, action, is_task_complete字段。 - action对象必须包含action_type, selector, text, description字段。 现在请开始你的测试工作。记住像一名有经验的测试工程师一样思考谨慎而果断。 这个Prompt明确了角色、安全边界、操作优先级和输出规范是智能体稳定工作的基石。针对不同的任务类型如登录、数据查询、表单提交还可以在具体任务调用时追加更具体的指令。3.4 实现一个具体的测试任务以“管理员登录”任务为例tasks/login_task.pyimport asyncio from core.agent import AITestAgent from config import settings, prompts class LoginTask: def __init__(self, agent: AITestAgent): self.agent agent self.task_description f以管理员身份登录系统。登录页面URL是{settings.LOGIN_URL}。成功登录后应能看到管理员专属的仪表板或菜单。 async def run(self): # 1. 首先导航到登录页面 navigate_action { action_type: navigate, text: settings.LOGIN_URL, description: 导航到登录页面 } # 这里可以简单调用page.goto或也通过Agent执行 await self.agent.page.goto(settings.LOGIN_URL) await self.agent.page.wait_for_load_state(networkidle) # 2. 使用Agent执行智能登录流程 # 将管理员账号密码存入Agent的短期记忆供其填写时使用 self.agent.memory[admin_username] settings.ADMIN_USER self.agent.memory[admin_password] settings.ADMIN_PASSWORD # 构建任务专属Prompt login_prompt prompts.BASE_SYSTEM_PROMPT f 当前具体任务{self.task_description} 已知信息管理员用户名是 {settings.ADMIN_USER}密码已提供。 请找到登录表单填写凭证并提交。 # 临时替换Agent的Prompt original_prompt self.agent.system_prompt self.agent.system_prompt login_prompt # 运行智能体循环 result await self.agent.plan_and_act(self.task_description, max_steps15) # 恢复原始Prompt self.agent.system_prompt original_prompt # 3. 验证登录是否成功也可以让LLM在循环中判断这里做双重校验 if result[status] success: # 检查页面是否包含管理员专属标识 dashboard_text await self.agent.page.text_content(body) if 仪表板 in dashboard_text or Dashboard in dashboard_text or 管理 in dashboard_text: print(登录任务成功检测到管理员页面元素。) return True else: print(登录任务可能失败未检测到管理员页面标识。) return False else: print(f登录任务失败{result[message]}) return False这个任务类封装了登录的完整流程导航、执行智能登录、结果验证。它展示了如何将具体的业务逻辑登录与通用的AI智能体结合起来。3.5 设置定时任务与报告最后我们需要一个调度器来每三小时运行一次完整的测试套件。main_scheduler.pyimport asyncio import schedule import time from datetime import datetime from playwright.async_api import async_playwright from core.agent import AITestAgent from tasks.login_task import LoginTask from tasks.create_order_task import CreateOrderTask from utils.reporter import generate_html_report from utils.notifier import send_alert async def run_full_test_suite(): 运行完整的测试套件 run_id datetime.now().strftime(%Y%m%d_%H%M%S) results [] async with async_playwright() as p: # 启动浏览器建议用headless模式无头模式在服务器运行 browser await p.chromium.launch(headlessTrue, args[--disable-dev-shm-usage]) context await browser.new_context(viewport{width: 1920, height: 1080}) page await context.new_page() # 初始化AI智能体 llm_client AsyncOpenAI(api_keysettings.OPENAI_API_KEY) # 示例 agent AITestAgent(page, llm_client, prompts.BASE_SYSTEM_PROMPT) # 定义要运行的任务序列 test_tasks [ (管理员登录, LoginTask(agent)), (创建测试订单, CreateOrderTask(agent)), # ... 添加更多任务 ] for task_name, task_instance in test_tasks: print(f[{run_id}] 开始任务: {task_name}) start_time time.time() try: success await task_instance.run() elapsed time.time() - start_time result { task: task_name, status: PASS if success else FAIL, duration: round(elapsed, 2), timestamp: datetime.now().isoformat() } results.append(result) print(f[{run_id}] 任务结束: {task_name} - {result[status]} ({elapsed:.2f}s)) if not success: # 任务失败截图保存 screenshot_path f./screenshots/failure_{run_id}_{task_name}.png await page.screenshot(pathscreenshot_path, full_pageTrue) result[screenshot] screenshot_path # 发送实时告警 send_alert(f测试失败告警 - {task_name}, f运行ID: {run_id}\\n请查看截图: {screenshot_path}) except Exception as e: elapsed time.time() - start_time error_result { task: task_name, status: ERROR, duration: round(elapsed, 2), error: str(e), timestamp: datetime.now().isoformat() } results.append(error_result) print(f[{run_id}] 任务异常: {task_name} - {e}) send_alert(f测试异常告警 - {task_name}, f运行ID: {run_id}\\n错误信息: {e}) # 关闭浏览器 await context.close() await browser.close() # 生成并保存报告 report_path generate_html_report(run_id, results) print(f[{run_id}] 测试完成报告已生成: {report_path}) # 如果有失败或错误汇总告警 failures [r for r in results if r[status] in (FAIL, ERROR)] if failures: summary f本次巡检({run_id})发现 {len(failures)} 个问题。 send_alert(f测试巡检摘要 - {run_id}, summary) def job(): 调度任务 print(f[调度器] 开始执行定时测试任务 {datetime.now()}) asyncio.run(run_full_test_suite()) if __name__ __main__: # 立即运行一次 job() # 每3小时运行一次 schedule.every(3).hours.do(job) print(AI测试智能体调度器已启动每3小时运行一次。) while True: schedule.run_pending() time.sleep(60) # 每分钟检查一次这个调度器使用schedule库管理定时任务每三小时触发一次完整的测试流程。它管理浏览器的生命周期按顺序执行任务收集结果生成报告并在发现问题时发送告警。4. 核心挑战与优化策略实录在实际运行中我遇到了不少问题也总结出一些让智能体更稳定、更高效的策略。4.1 挑战一LLM的“幻觉”与不稳定操作问题LLM有时会“想象”出页面上不存在的元素或者生成错误的选择器比如一个完全无效的CSS路径。这会导致动作执行失败循环卡住。解决方案强化感知信息质量不要给LLM完整的HTML那太嘈杂且消耗token。我优化了perceive函数只提取关键的交互元素button, input, a及其最稳定的属性>