先说个最近我反复跟团队强调的场景:UI 自动化脚本能自动生成,但没人愿意自动维护。业务页面两周一改版,测试脚本跟着改选择器、调等待、补分支,这套流程消耗的精力比当初手写脚本还多。后来我把 Agent Skills 这套“技能封装”的思路和 Playwright 组合起来,加了一个大模型推理层,做出一套能让智能体自己看页面、自己写脚本、自己修脚本的流程,实测下来把日常回归用例的维护成本降了至少一半。这篇文章就把我踩过的坑、验证过的方案和核心实现完整拆给你,适合已经在用 Playwright 写自动化、但对 Agent 落地还停留在“调 API 聊天”阶段的测试开发同学。
1. 内容整体设计与思路拆解
很多人一听到“让 AI 写 UI 自动化脚本”,第一反应是让大模型直接生成一段完整的 Playwright 代码,然后丢到命令行跑。我一开始也是这么干的,结果很惨:模型生成的代码看起来很完整,一跑就挂在定位器上——不是 class 名字猜错,就是页面元素是异步渲染出来的它压根没等。于是我把方案改成了三层结构:Agent Skills 负责把页面操作抽象成可复用技能,Playwright 负责稳定执行,大模型只负责拆解任务和做决策。
1.1 为什么选 Agent Skills 而不是直接生成代码
直接生成代码最大的问题在于,模型对被测系统的认知是“一次性”的。它不知道登录按钮在真实页面上长什么样,不知道某个表格是虚拟滚动还是分页加载,这些信息只有在真实浏览器里获取才有意义。Agent Skills 的思路是反过来:先把高频、稳定的操作封装成技能模块,比如“打开页面”“输入文本”“点击按钮”“读取表格数据”“等待元素出现”,每个技能内部用 Playwright 的最佳实践写好,大模型的任务只是根据用户需求去组合这些技能。
这个设计本质上就是把“写代码”变成了“编排动作”。对于大模型来说,填空式和选择题式的任务成功率远远高于从零生成一段复杂脚本,这一点在真实工程里非常关键。
1.2 Playwright 在这个方案里的角色定位
Playwright 是这个体系里最可靠的地基。它能同时提供三层能力:第一是跨浏览器一致性,Chromium、Firefox、WebKit 都能跑;第二是自动等待机制,元素不可操作时会自动重试,不需要脚本里手动写一堆 sleep;第三是 Trace Viewer,可以完整录下每一步操作和页面快照,这三点恰好是“智能体需要观察→行动→反馈”闭环里缺一不可的组件。
我特别看重的是 Trace Viewer。智能体执行完一轮操作之后,如果断言失败,我可以把 trace 文件直接喂给大模型,让它看到失败时页面上到底发生了什么,而不是靠文字猜测。这个能力是 Selenium 时代完全不具备的,也是 Agent 方案能够真正落地的关键支撑。
1.3 整体架构:模型、技能、执行器三层协作
整个方案可以分成三个层次:
- 决策层:大模型接收自然语言目标,拆解成有序的技能调用序列,并处理执行过程中返回的异常信息。
- 技能层:一组用 Playwright 封装的动作函数,每个函数都有清晰的功能描述、参数列表和返回结构,作为工具暴露给模型。
- 执行层:真实浏览器环境,负责跑动作、采集页面状态、生成 trace 和截图。
用一张对比表来看传统写脚本方式和 Agent Skills 方式的核心差异:
| 对比维度 | 传统手写脚本 | Agent Skills + Playwright |
|---|---|---|
| 编写方式 | 人工逐行写定位器和操作 | 自然语言描述目标,模型编排技能 |
| 页面改版应对 | 人工定位问题并改选择器 | 模型读取失败信息,调整动作序列或定位策略 |
| 维护成本 | 随用例数量线性增长 | 技能复用率提高,新增用例成本大幅下降 |
| 调试手段 | 看日志、看截图 | Trace Viewer + 模型辅助分析 |
| 失败恢复 | 人工介入修复 | 智能体尝试自愈或给出修复建议 |
这套结构还有一个隐藏优势:技能层是稳定接口,底层 Playwright 版本升级或者 API 调整时,只需要改动技能层内部实现,决策层的提示词和模型行为完全不需要跟着变。工程上这种“变更隔离”是很值钱的。
2. 核心实现:从环境搭建到技能封装
这一节直接进入实操。我以最常用的 Python 技术栈为例,把完整的架构搭建过程一步步拆开讲,包括依赖安装、工程目录设计、技能函数怎么写,以及最关键的大模型调用协议设计。
2.1 环境准备与工程目录设计
先说依赖。基础运行环境是三件套:Python 3.10+、最新的 Playwright 包、一个能调用 OpenAI 兼容接口的大模型客户端库。安装命令很简单:
pip install playwright playwright install chromium第二行命令是安装 Chromium 浏览器内核,这一步千万别漏。我在 Windows、macOS、Linux 上都跑过这套方案,Linux 服务器上还需要额外装一些系统依赖库,如果报 Missing dependencies 直接跑playwright install-deps就能解决。
工程目录我建议这样组织:
agent_ui_project/ ├── agent/ │ ├── llm_client.py # 大模型接口封装 │ ├── planner.py # 任务拆解与异常处理循环 │ └── prompts.py # 提示词模板 ├── skills/ │ ├── __init__.py # 技能注册表 │ ├── browser.py # 技能实现:所有 Playwright 操作 │ └── schema.py # 技能参数与返回值定义 ├── target_app/ # 被测应用相关数据、页面配置 │ └── app_config.py └── main.py # 入口:接收目标并启动 Agent这样分的核心原因是让“技能实现”和“模型决策”完全解耦。skills 目录里不出现任何大模型相关代码,agent 目录里不出现任何 Playwright 相关代码,中间通过 schema 文件传递结构化的技能定义。
2.2 技能函数的设计原则与示例代码
封装 Playwright 操作时,一个最容易犯的错是把技能函数写得过于原子化,比如“移动鼠标到坐标 (x,y)”。这种技能对模型来说太难编排了,因为它根本没有语义。我总结的经验是:技能粒度要匹配自然语言指令的颗粒度。
举一个落地的技能定义,以“打开页面并等待加载完成”为例:
# skills/browser.py from playwright.sync_api import Page, expect def open_page(page: Page, url: str, wait_selector: str = "body") -> dict: """ 打开指定 URL,并等待页面出现某个关键元素。 参数: url: 目标页面地址 wait_selector: 用于判断页面加载完成的 CSS 选择器 返回: 页面标题和当前 URL """ page.goto(url, wait_until="domcontentloaded") page.wait_for_selector(wait_selector, timeout=15000) return {"title": page.title(), "url": page.url}再看一个带自动重试的点击技能:
def click_element(page: Page, selector: str, retry: int = 3) -> dict: """ 点击页面上的指定元素,带重试机制。 参数: selector: 目标元素的 CSS 选择器 retry: 失败后重试次数 返回: 是否成功以及失败原因 """ for attempt in range(retry): try: page.locator(selector).click(timeout=5000) return {"success": True, "attempts": attempt + 1} except Exception as e: page.wait_for_timeout(1000) return {"success": False, "error": str(e)}这类技能函数有几个共同特征:参数尽量少、返回结构固定、内部做好异常捕获。模型不需要理解 Playwright 的 API 细节,它只需要知道“调用 click_element,传什么参数,拿到什么结果”。
技能的注册机制我用一个简单的字典表搞定:
# skills/schema.py SKILL_REGISTRY = { "open_page": { "description": "打开指定 URL 并等待页面加载完成", "parameters": { "url": {"type": "string", "required": True, "description": "目标页面地址"}, "wait_selector": {"type": "string", "required": False, "description": "页面加载完成标志"}, } }, "click_element": { "description": "点击页面上指定选择器的元素,带重试", "parameters": { "selector": {"type": "string", "required": True, "description": "CSS 选择器"}, } }, # ... 更多技能 }这个注册表有两个用途:一是拼进系统提示词,让模型知道有哪些工具可用;二是做参数校验,防止模型传入非法参数。实际运行中,我在这一层拦下了大量格式错误。
2.3 智能体主循环:规划、执行、反馈、修正
技能层搭好之后,核心的智能体循环代码并不复杂,关键在于把“反馈”这一步做扎实。我的实现流程是:
- 把用户目标和技能注册表一起发给大模型,让它返回一个 JSON 数组,表示有序的技能调用序列。
- 按顺序执行技能,每个技能执行后都会返回结构化结果。
- 如果某一步执行失败,把错误信息、当前的 trace 文件路径、页面截图一起追加到上下文里,再发给模型,让它决定是换一种定位策略还是调整后续动作。
- 循环执行直到任务成功或达到最大轮次限制。
主循环的关键代码骨架如下:
def run_agent(target: str, max_rounds: int = 5): context = [{"role": "system", "content": build_system_prompt()}, {"role": "user", "content": target}] with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() for round_idx in range(max_rounds): response = llm_client.chat(context, response_format="json") actions = parse_actions(response) # 解析模型返回的动作序列 results = [] for action in actions: result = execute_skill(page, action) results.append(result) if not result.get("success"): break if all_actions_succeeded(results): return summarize_success(results) # 把失败信息写入上下文,进入下一轮修正 trace_path = page.trace_path() context.append({"role": "user", "content": format_failure_feedback(results, trace_path)}) browser.close()其中有几个容易被忽视的细节:
- 每一轮对话的上下文长度会快速增长,我通常会把前面轮次成功的执行结果做一些摘要压缩,只保留“动作类型、关键返回值、耗时”三要素,避免把几千行日志全塞给模型。
- trace 路径要提前开启
page.trace.start(),并且在每轮结束后page.trace.stop()生成一次独立的 trace 文件,这样反馈里能引用具体是哪一轮失败。 - headless 模式只适合批量回归,调试阶段建议开 headed,否则模型看不到真实页面状态,遇到诡异问题时排查效率会低很多。
2.4 定位器选择策略:给智能体一双“可靠的眼睛”
整个方案里,最容易让智能体翻车的地方就是定位器选择。模型对页面的理解完全来自 DOM 描述和截图,一旦页面是异步渲染的,或者元素属性是动态生成的,模型很容易给出一个看起来正确但实际脆弱的 selector。
我做了两层策略来对抗这个问题。第一层是在技能层内部实现“多候选定位”:比如点击一个按钮时,同时尝试按文本、按 role、按>def extract_candidate_elements(page: Page, keyword: str) -> list: """ 在当前页面中查找所有与关键词相关的可交互元素。 """ candidates = [] elements = page.query_selector_all("button, input, a, [role='button'], [data-testid]") for el in elements[:50]: text = el.inner_text()[:50] if el.inner_text() else None data_testid = el.get_attribute("data-testid") classes = el.get_attribute("class") candidates.append({ "tag": el.evaluate("node => node.tagName"), "text": text, "data_testid": data_testid, "classes": classes }) return candidates
模型拿到这个候选列表后,就能结合用户意图选出正确的目标元素。这一步很有必要,它把“生成代码”这种开放性问题,转化成了“从候选里挑一个”的闭式问题,成功率提升非常明显。
3. 实操过程与核心环节实现
理论知识说完了,下面走一个完整的实操案例。我以一个模拟的某电商运营后台作为被测对象,目标是从登录页开始,完成一个订单数据核验任务,并截取结果图。这个案例覆盖了登录态管理、动态表单填写、表格数据提取、截图保存四个核心场景。
3.1 任务定义与初始规划
给智能体的目标描述如下:
登录“某电商运营后台”(账号 admin / 密码 Test@12345),进入“订单管理”页面,筛选昨天创建的订单,统计状态为“已完成”的数量,并截取筛选后的订单列表截图,保存到 outputs/order_done.png。
第一轮模型规划出的动作序列大致是:
[ {"skill": "open_page", "params": {"url": "https://xxx.ops.example.com/login", "wait_selector": "#login-form"}}, {"skill": "fill_input", "params": {"selector": "#username", "value": "admin"}}, {"skill": "fill_input", "params": {"selector": "#password", "value": "Test@12345"}}, {"skill": "click_element", "params": {"selector": "button[type='submit']"}}, {"skill": "wait_for_navigation", "params": {}}, {"skill": "click_text", "params": {"text": "订单管理"}}, {"skill": "fill_input", "params": {"selector": "#create-date-start", "value": "昨天日期"}}, {"skill": "click_text", "params": {"text": "查询"}}, {"skill": "read_table", "params": {"selector": ".order-table"}}, {"skill": "count_by_column", "params": {"column": "状态", "value": "已完成"}} ]这个规划整体合理,但里面有几个地方需要在执行阶段动态修正。比如“昨天日期”需要由代码动态计算,而不是模型写死;再比如“订单管理”菜单在真实页面上的文本可能是“订单管理”加一个角标数字,导致click_text精确匹配失败。这些问题正好是执行阶段要处理的核心场景。
3.2 登录态与动态参数处理
登录模块是整个流程里稳定性要求最高的环节。我的做法是:登录动作单独封装成一个技能,并且在技能内部加入 3 次重试机制,每次失败后自动截取当前页面图并记录原因。
def login(page: Page, url: str, username: str, password: str, success_selector: str) -> dict: """ 执行登录流程,成功标准是出现 success_selector 指定的元素。 """ page.goto(url, wait_until="domcontentloaded") for attempt in range(3): page.fill("#username", username) page.fill("#password", password) page.click("button[type='submit']") try: page.wait_for_selector(success_selector, timeout=8000) return {"success": True, "attempt": attempt + 1} except Exception as e: page.screenshot(path=f"logs/login_fail_{attempt}.png") page.wait_for_timeout(2000) return {"success": False, "error": "login failed after retries"}动态日期参数不能交给模型去算,因为这个计算过程完全确定,不需要模型推理。更稳妥的做法是在执行层做参数预处理器,识别动作序列里面形如{yesterday}或{today}这种占位符,在执行前自动替换为真实值。这也可以算作一个小的技能扩展点。
预处理的代码逻辑大概是:
def preprocess_actions(actions: list) -> list: from datetime import date, timedelta date_map = { "{today}": date.today().strftime("%Y-%m-%d"), "{yesterday}": (date.today() - timedelta(days=1)).strftime("%Y-%m-%d"), } for action in actions: for key, value in action.get("params", {}).items(): if isinstance(value, str) and value in date_map: action["params"][key] = date_map[value] return actions这类“确定性逻辑代码化,不确定性逻辑模型化”的原则,是整个智能体工程里最重要的一条工程经验。它直接把模型出错的概率空间缩小了一大截。
3.3 “看”表格数据并做统计
订单列表是一个典型的动态渲染表格,数据区域是在接口返回之后才真正渲染出来的,直接读取时经常会拿到空表或者“加载中”状态。我封装了一个带自动等待的表格读取技能,先等表格行数大于 0,再逐行提取指定列的数据。
def read_table(page: Page, selector: str, column_name: str = None, timeout: int = 15000) -> dict: """ 读取 table 中指定列的文本内容。 """ rows = page.locator(f"{selector} tbody tr") rows.first.wait_for(timeout=timeout) count = rows.count() if count == 0: return {"row_count": 0, "data": []} columns = page.locator(f"{selector} thead th") column_idx = None if column_name: for i in range(columns.count()): if column_name in columns.nth(i).inner_text(): column_idx = i break data = [] for i in range(count): cells = rows.nth(i).locator("td") if cells.count() > 0: value = cells.nth(column_idx).inner_text() if column_idx is not None else cells.nth(0).inner_text() data.append(value.strip()) return {"row_count": count, "data": data}统计数量这个动作不需要单独开新的浏览器页面,直接在前一步的读取结果上做本地统计就行。我在技能层单独放了一个count_occurrences工具,它接收一个列表和过滤条件,返回出现次数。这样做的好处是,模型不需要自己写 Python 代码去统计,减少了出错的可能。
还有一个细节:动态表格在翻页后会销毁旧的 DOM 节点,如果读取完第一页后还需要翻页继续读,必须重新获取 locator,而不能复用之前拿到的对象。这个坑在实操中几乎每个人都会踩一次,后面会在常见问题部分详细展开。
3.4 截图的正确打开方式
截图保存到文件这件事,看起来简单,实际上的坑在于路径规范和失败现场的保存策略。我的做法是定义一个固定输出目录,并且用小写字母加下划线命名,避免中文路径和空格带来的跨平台问题。
def take_screenshot(page: Page, filepath: str, full_page: bool = False) -> dict: """ 截取当前页面,支持整页截图。 """ from pathlib import Path Path(filepath).parent.mkdir(parents=True, exist_ok=True) page.screenshot(path=filepath, full_page=full_page) return {"success": True, "path": filepath}对于“昨天创建的订单已完成数量”这种目标,我发现边做边截图比最后补一张全局截图更有用。比如登录成功后截图 login_success.png,筛选完成后截图 filter_applied.png,最后再截图最终结果页。这样如果最终断言语义出了问题,还能根据中间步骤快速定位是哪一步的理解偏了。
我在实际项目中还加了一道“截图自检”步骤:截图完成后用像素差异算法判断页面是否真的切换到了预期状态,比如判断某块区域是否出现了预期的文字渲染痕迹。这个能力对复杂前端页面尤其好用,因为 SPA 的路由变化经常不体现在 URL 上,只体现在视图渲染上。
3.5 完整跑通后的输出与校验
当一轮动作全部执行成功后,智能体不会直接结束,而是会做完整性检查。我的检查点包括:
- 是否成功登录:以
success_selector是否出现为准。 - 是否产生了统计数据:表格行数必须大于 0。
- 是否生成了截图:文件存在且文件大小大于某个阈值,比如 10KB,因为空白截图基本都小于这个值。
- 每一步的 trace 文件是否完整:这个主要是留作回归排查的证据链。
如果这些条件不满足,智能体会启动自我修正流程,最多可以向模型反馈三次。三次都失败的话,整个任务标记为失败,并且把失败的 trace 文件路径打印出来,方便人工接手。
这个自校验逻辑听起来简单,但它是整个系统从“可用”走到“可信”的分水岭。没有它,模型偶尔成功时你都不敢确认结果是真的对还是碰巧对。
4. 常见问题与排查技巧实录
把方案跑通不难,难的是在真实环境里稳定运行。下面这部分是我不间断使用了几个月之后整理出的问题清单和排查方法,每一类都是实战里真实碰到过的。
4.1 定位器脆断问题
现象是最典型的:第一轮生成脚本能跑通,页面版本一更新,脚本大面积失败。根因在于模型生成的定位器偏好使用动态变化的 class 名称或者 index 序号,这些属性在真实项目里极不稳定。
我的排查方式很直接:把失败时的 trace 文件和截图拉出来,和成功时的 DOM 结构做 diff。大部分情况不是元素消失了,而是 class 名加了前缀、或者 DOM 层级里多包了一层。
解决方案分三层:
- 提示词层面,强制要求模型优先使用 role、文本内容、data-testid 定位,只有在这些都不存在时才使用 class。
- 技能层面,封装一个
find_element工具,内部按优先级逐个尝试候选定位方式,把“定位”这个动作从模型手里抢过来。 - 工程层面,推动被测系统在常用组件上补充>rows = page.locator(".order-table tbody tr").all()
在翻页后的场景里,这个写法比保存 locator 对象要稳得多。还有一点要注意:断言表格行数时不要用
count()大于 0 来判断成功,因为虚拟滚动表格可能只渲染视口内的一部分行,count 值会随着滚动变化,这个场景需要额外处理。4.3 Token 消耗失控与上下文膨胀
智能体方案最容易被忽视的成本问题是 token。每一轮失败修正都会把错误信息、页面状态、trace 描述追加进上下文,这个量会迅速膨胀,几轮下来上下文长度直接超过模型窗口限制。
我的经验是设立“信息压缩管道”:进入下一轮修正前,把上一轮的中文日志摘要成两到三行,只保留失败技能名、错误类型、候选元素数量。原始日志全部落盘保存,但在模型上下文里是压缩后的精炼版本。实测下来,单任务 token 消耗能减少约 40%,而任务成功率没有下降。
一个更极致的方案是把页面截图交给多模态模型去理解,而不是传输大段 DOM 文本。不过这个方案的工程复杂度会高出不少,适合有专门 GPU 资源或者调用多模态 API 成本可控的场景。
4.4 iframe 和 Shadow DOM 的处置不当
现在的管理系统里 iframe 出现的频率并不低,最常见的是对接第三方登录或者富文本编辑器。直接用常规 locator 是定位不到 iframe 内部元素的。Playwright 提供了
frame_locator语法,但我发现很多模型不知道这个能力,生成的脚本里会直接拿普通选择器去选 iframe 内部元素。我干脆把 iframe 场景直接封装成了两个独立技能:
switch_to_frame和switch_to_parent_frame,模型只需要在动作序列中合适的位置插入这两个技能即可。技能内部的实现方式很简单,用 Playwright 的 frame 定位来做:page.frame_locator("#editor-frame").locator(".toolbar-button").click()Shadow DOM 也是同理,我把
page.locator("css=#host").shadow_locator(".inner-btn")的调用封装进技能层。核心思路仍然不变:把易错的底层细节留在技能层,大模型只需要做高层编排。4.5 多账户场景的上下文隔离
如果你的业务场景需要并行跑多个账户,比如同时处理多个店铺的后台操作,直接用同一个浏览器上下文会把登录态混在一起。Playwright 的解决方案是 Browser Context 隔离,每个账户一个 context,cookie 和 localStorage 天然隔离。
我的架构里这一层是放在技能层之外的浏览器管理模块处理的。每个任务启动时分配一个独立 context,任务的 trace、截图、日志也都按 context 维度落盘到独立目录。这样不仅账户不会串数据,排查问题时也能按单账户维度完整回放它的所有操作。
这个设计在真实项目里帮我解决了一个很头疼的问题:之前用同一个上下文跑多个账号时,偶尔会出现“A 账号的操作被记录了 B 账号的登录态”这种诡异 bug,排查起来极其费劲,换成 context 隔离之后就再没出现过。
4.6 问题排查工具清单
整理一个速查表,方便你在现场快速对号入座:
问题现象 常见根因 排查手段 解决方案 元素定位超时 页面有异步渲染,class 动态变化 打开 trace 看页面 DOM 改用多候选定位策略,优先 role/text/data-testid 脚本执行随机失败 存在动画或懒加载 观察失败时的截图 在技能中加入 wait_for_selector和可配置超时翻页后拿不到新数据 旧 locator 绑定已销毁节点 打印 rows.count()对比每次读取前重新查询 locator 上下文过长 失败信息无限制追加 记录每轮 token 数 引入日志摘要与上下文压缩管道 iframe 内元素点击失败 未切换到对应 frame 查看 trace 中 frame 列表 使用 frame_locator封装专用技能多账号数据串乱 共享了浏览器上下文 检查 cookie 隔离 每任务独立 browser.new_context()5. 成本、局限与后续演进方向
这套方案不是银弹,用了小半年之后,我对它的收益边界和成本结构都有了更清醒的认识。这里把真实的数据和思考写出来,供你评估时参考。
5.1 直接收益:人力成本从“线性”变成“对数”
拿我最常用的读数据类用例来算账。传统手写脚本,一个中等复杂度的用例大概需要 0.5 到 1 天;页面改版之后,修复时间通常在 1 到 2 小时。接入 Agent 方案后,新用例的编写时间压缩到 15 分钟左右,因为大部分时间都花在人工确认业务规则上,而不是写定位器。改版修复时间则取决于技能层是否受影响,如果只是页面内部元素的调整,修复可能只需要在反馈里描述一句,下一轮模型自己就改好了。
但这不意味着测试开发同学就“没事干了”。真实的收益是时间被释放出来,用在了更值得投入的地方,比如建立更细粒度的业务断言、优化回归用例的颗粒度、以及把手动页面巡检逐渐改造成半自动的智能巡检。更大的价值是,过去因为脚本维护成本太高而不敢覆盖的长尾页面,现在可以用较低成本把它们纳入自动化的覆盖范围。
5.2 隐性成本:模型调用、基础设施与调优周期
最大的显性成本当然是 token 调用费。非多模态模型跑一个完整任务,平均消耗在 3 万到 6 万 token 之间,失败重试时会更高。批量跑几十上百条用例时,这个费用不是可以忽略的。我的应对方式是批量任务时全部换成更经济的模型,只有复杂纠错场景才切回强推理模型。
隐性成本里更需要注意的其实是“评估成本”。你需要一套能自动判断“智能体这次跑得对不对”的标准,否则模型自行发挥成功时,你根本分不清它是真做对了还是在表演。我这里有一套轻量级方案:用确定性脚本判断关键动作的副作用,比如登录后验证某个元素出现,筛选后验证 URL 带上了筛选参数。这些判断逻辑写成独立断言模块,不依赖模型自己报喜。
5.3 未来的演进:从“执行脚本”到“理解业务”
我现在在探索的下一个方向,是让智能体不只是会执行动作,还能理解业务规则。比如读完表格后,不是机械地统计数量,而是能结合上下文判断“这个数量比上个周期少了 30%,是不是筛选条件设置错了”。这需要把规则库和知识库也接入上下文,让模型在做判断时有依据可查,而不是纯粹靠猜测。
另一个方向是把这套能力变成 CI 流水线的标准环节。每次前端代码合并后,自动拉取 Playwright 智能体去跑一轮冒烟回归。和传统跑固定脚本不同,智能体可以根据这次代码改动的描述,主动生成针对性的回归用例,而不是每次都跑一成不变的全量用例。这个想法还在验证阶段,但初步结果是积极的,尤其是对那类经常改动局部页面的项目,冒烟覆盖率提升效果很明显。
这套方案真正值得投入的地方,不在于让模型写得更多,而是让工程体系里那些重复的、确定性的工作被稳定地、正确地自动化。技术会一直演进,但把“易变的模型行为”和“稳定的执行层”解耦的理念,我觉得会比其他具体实现活得更久。