本节摘要:本节回答「固化」的资格问题与落地管道。先立分界线:测试是概率性趋零的工程形态——断言确定性、可在 CI 里无限次回归、每次运行零边际成本;Agent 是目标驱动的——容忍概率路径、适应页面变化、但每一步都消耗 token。两者不是对立而是光谱:第 3.3 节的 observe 缓存在光谱中间(省掉模型调用但保留语义目标),本章的固化走到右端(连语义目标也换成代码)。然后给五维决策表:频次、稳定性、变异度、可逆性、合规压力——高频、稳定、零变异、可逆、合规无虞的任务固化收益最大;任何一维亮红灯都该留在 Agent 侧。固化管道三步:Agent 探索(自然语言跑通,产出轨迹)、产物整理(从轨迹提炼元素语义、动作序列与断言口径)、固化为 Playwright 测试(零 token 成本,进 CI)。回归任务集把零散测试组织成有运行节奏的资产。示例双语,打在靶场。
| 维度 | 测试(固化产物) | Agent(探索形态) |
|---|---|---|
| 路径 | 确定性,每次一样 | 概率性,可能每次不同 |
| 断言 | 确定性判定(第 6.1 节结构断言为主) | 目标达成判定(结果等价) |
| 对页面变化 | 脆(选择器断则失败) | 韧(模型适应改版) |
| 成本 | 每次运行零边际成本 | 每步一次模型调用 |
| 在工程里的位置 | CI 回归、发布门禁 | 任务执行、探索新流程 |
两行的哲学差异要读透:测试的价值恰恰在于它不聪明——同样的输入永远得到同样的路径与判定,这才配当回归的尺子;Agent 的价值在于它聪明——页面改版后还能找到路。固化因此不是「升级」而是搬家:把一段已经稳定的流程从「需要聪明」的部门搬到「必须死板」的部门,让聪明留给真正未知的地方。
这个分界与第 3.3 节不固化清单的口径完全一致:A/B 页面、动态内容、低频长尾、他人页面不该固化——它们的「不稳定」不是缺陷而是工作需求。固化清单与不固化清单合起来,就是光谱上的选型表。
| 维度 | 问题 | 加分(趋向固化) | 减分(留在 Agent) |
|---|---|---|---|
| 频次 | 每天跑多少次? | 高频(几十次以上) | 低频长尾 |
| 稳定性 | 页面结构多久变一次? | 自家页面、发布节奏可控 | 频繁改版、A/B 实验 |
| 变异度 | 每次执行的路径差异? | 零变异(流程固定) | 路径本来就该随机 |
| 可逆性 | 动作可逆吗? | 可逆(读、查、填草稿) | 不可逆(提交、支付) |
| 合规压力 | 高频化的访问合规吗? | 自有系统 | 他人站点(第 8 章红线) |
使用方法:五维各自打一到五分,总分高者进固化管道;任何一维触到红线(不可逆、他人站点高频)直接否决——不可逆动作固化后脱离了第 2.2 节的确认门语境,他人站点固化等于把访问压力焊死(第 3.3 节末尾的警告)。可逆性维度的细分处理:不可逆流程可以固化到「最后一步之前」——提交动作保留在 Agent 或人工手里,固化段负责所有准备与验证。
第一步 Agent 探索 第二步 产物整理 第三步 固化落地 ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ 自然语言跑通流程 │ ───▶ │ 轨迹里提炼: │ ───▶ │ 写成 Playwright │ │ 产出完整轨迹 │ │ 元素语义(角色+名) │ │ 测试文件 │ │ (审计留痕,8.3) │ │ 动作序列 │ │ 结果等价断言 │ │ │ │ 断言口径 │ │ 进 CI 回归任务集 │ └──────────────────┘ └──────────────────┘ └──────────────────┘ token 成本一次性 人工评审一次 之后零 token
第一步的探索直接用前半本的工具(browser-use 或 Stagehand 都行):
# explore_todo.py —— 用 Agent 探索流程,产出固化依据(写法示意,以官方文档为准) import asyncio from browser_use import Agent from langchain_openai import ChatOpenAI async def main() -> None: agent = Agent( task=( "在 http://127.0.0.1:8080/demo_todo.html 上添加待办 买牛奶," "并确认列表出现该条目。完成后逐条报告你执行过的动作。" ), llm=ChatOpenAI(model="gpt-4o"), ) result = await agent.run() print("最终结果:", result.final_result()) print("动作历史条数:", len(result.action_results())) asyncio.run(main())
第三步的产物(探索轨迹整理后写死):
# test_todo_flow.py —— 固化产物:Playwright 测试(写法示意,以官方文档为准) from playwright.sync_api import expect, sync_playwright def test_add_todo() -> None: with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto("http://127.0.0.1:8080/demo_todo.html") page.get_by_label("新待办").fill("买牛奶") page.get_by_role("button", name="添加").click() expect(page.get_by_role("listitem")).to_contain_text("买牛奶") # 结果等价断言 browser.close()
// test_todo_flow.mjs —— 固化产物的 JS 形态(写法示意,以官方文档为准) import { expect, test } from "@playwright/test"; test("添加待办后列表出现该条目", async ({ page }) => { await page.goto("http://127.0.0.1:8080/demo_todo.html"); await page.getByLabel("新待办").fill("买牛奶"); await page.getByRole("button", { name: "添加" }).click(); await expect(page.getByRole("listitem")).toContainText("买牛奶"); // 结果等价断言 });
第二步「产物整理」是三步里唯一必须人来做的环节,也是质量所在:从轨迹里提炼的不是「模型点了第几个元素」,而是元素的业务语义(角色加可访问名:get_by_role 加 name)——这与第 1.2 节定位三层谱系的结论一致,语义定位是选择器谱系里最耐改版的一档。整理时顺带把断言口径从探索时的「页面看起来对」升级成第 6.1 节的结构断言。轨迹即证据(第 8.3 节的事件流)在这里兑现第二重价值:固化评审的依据不是「我觉得页面是这样的」,而是探索期留下的完整记录。
固化产物不该散落在仓库里各自为战,要组织成回归任务集:
| 分层 | 内容 | 运行节奏(示意) |
|---|---|---|
| 冒烟集 | 核心路径的最小验证(登录、首页、主流程一条) | 每次提交 |
| 全量集 | 所有固化测试 | 每日定时 |
| 发布集 | 冒烟集加发布敏感项(支付、权限) | 发布前必跑 |
运营要点三条:其一,任务集与第 11.3 节上线清单挂钩——发布集不过不许发版;其二,失败分类接第 11.2 节 taxonomy——测试挂了先分类(网络还是选择器还是业务),再决定是修测试还是修产品;其三,固化覆盖率(固化任务执行次数除以总任务执行次数,第 3.3 节定义)按月回顾,决定下一段固化候选。任务集本身也是活的资产:新增固化测试走评审(决策表打分留档),废弃的测试显式删除并记录原因——一个从不变更的任务集,很快就会和现实脱节。
| 坑 | 现象 | 排查与处理 |
|---|---|---|
| 照抄轨迹坐标 | 测试脆到分辨率一变就挂 | 整理成语义定位(角色加名),别抄坐标与序号 |
| 固化不可逆动作 | 支付进了 CI 每天真扣款 | 决策表红线;固化到提交前一步 |
| 一次固化永不再看 | 半年后测试集大面积失效 | 按月回顾覆盖率与失败分类 |
| 探索轨迹不留档 | 评审时说不清当时页面状态 | 第 8.3 节事件流,轨迹即证据 |
| 测试与 Agent 争地盘 | 简单任务也上 Agent | 重读光谱:能死板的别聪明 |
固化把「值得死板」的任务搬走了,但光谱中间还有一大段:值得进测试体系、又不值得写死成纯代码的任务——比如断言「页面看起来正常」。下一节讲 Midscene 的 E2E 桥:自然语言用例怎么进 CI,AI 断言的误判怎么兜,以及与纯 Playwright 的三层混合策略。