前端重构改了一个类名、挪了一个按钮,应用照常工作,一半选择器却不再匹配。传统做法是让人逐个改定位器,改完再跑一遍。运行时自愈提供了另一条路:选择器失效时,让模型从页面实际存在的元素里判断你指的是哪一个,验证通过后继续执行,并把修复缓存下来,下次运行不再调用模型。
这个思路听起来像"让测试不那么脆",但真正的工程难点不在"修好",而在修错了也会绿。如果结算按钮被删掉、模型却自信地点了另一个按钮,套件全绿,没人发现产品出了问题。因此值得拆解的不是模型提示词,而是整套验证、审计与成本控制的机制——它们决定了自愈是提效手段,还是一个新的静默失败源。
一次自愈走完的六步
以 Playwright 生态里一套可运行的实现为例,一次自愈的流程是固定的、可观测的:
- 原定位器先跑。 给它一个很短的超时(默认 1 秒),元素正常出现就直接执行动作,自愈逻辑完全不介入——匹配正常时,套件行为与未接入前一致。
- 查缓存。 之前在同一页面路径、同一动作、同一描述下修好过的定位器,先拿来试。命中就不调用模型。
- 收窄候选。 从页面收集最多 1,000 个可交互元素,按关键词匹配、元素类型、ARIA role 与 test id 打分排序,只把前 40 个送给模型。这一步是为了防止目标元素因 DOM 顺序靠后被截断。
- 模型给出候选策略。 最多返回三条 Playwright 定位策略——test id、role 与 name、label、placeholder、text、alt text、title 或 CSS,每条带置信度;输出必须符合固定 JSON schema,不是自由文本。
- 逐条验证。 一条建议要被采纳,必须同时满足:只匹配到唯一一个元素、该元素可见、置信度不低于
minConfidence。 - 执行并落盘。 生效的定位器写入缓存文件,本次自愈以一行 JSONL 记入事件日志,包含模型的理由、置信度与 token 消耗。
“每个元素只调用一次模型,而不是每次运行调用一次"是成本控制的核心。缓存按动作、页面路径与描述组织,提交进仓库后团队与 CI 共享同一份修复。

从短超时判定、缓存命中、候选收窄到唯一性校验与落盘的完整链路
验证比生成重要,但验证本身不够
第 4、5 步是这套设计里最有价值的部分:模型不是凭空编一个选择器,而是从页面上真实存在的候选元素里选,答案必须能落回一个具体的、可见的、唯一的元素。相比之下,“让模型直接写一个新 CSS 选择器"几乎不可控。
但唯一性与可见性都只保证"这个元素存在且唯一”,不保证"它是你要的那个”。原文把风险说得很直白:删除结算按钮后,模型如果自信地挑了另一个按钮,测试依然通过,缺陷被静默吞掉。对质量保障来说,一个不会误报的自愈机制,比一个会偶尔失败的更危险。
因此需要三道闸门,把"自愈发生过"这件事强制变成可见信号。
闸门一:置信度阈值
minConfidence 的默认值为 0,也就是接受任何分数的建议。生产环境里应显式设到 0.8 一类水平:模型不够确定时,宁可让动作失败,也不要猜。这个开关把"不确定"重新变成红灯。
闸门二:只报告不修复的模式
warn 模式会记录"原本会怎么修",但保留原始失败;缓存里的修复也只报告、不应用。后一点尤其关键:否则一份被提交进仓库的缓存文件,就能把审计运行悄悄刷成全绿,团队再也看不到哪些选择器其实早该更新。
闸门三:让构建为自愈负责
提供运行级汇总接口,统计本次运行发生了多少次自愈,在全局 teardown 里列出待修的选择器,并让构建失败:
| |
这里有个进程模型上的坑:测试跑在 worker 进程,teardown 跑在 runner 进程,内存里的数据不共享,所以每次结果都要额外写进一个 JSONL 汇总文件,由汇总接口从文件读取;并且只统计当前运行——如果 runner 在两次运行之间不退出(UI 模式、watch 模式),要在 globalSetup 里显式重置计数。
三道闸门合起来的语义很清楚:自愈是让构建不中断的临时手段,不是让选择器免于维护的许可。
成本、速度与隐私怎么控住
每次自愈都要调用一次模型,如果放任不管,成本和延迟会随失败选择器数量线性上升。可复用的优化有四条:
- 候选属性压缩:只发送有值的属性,并用短键(如
tid表示 test id、txt表示文本); - 隐藏元素标记而非丢弃:真实应用常把目标元素刻意隐藏(自定义样式的复选框、仅 hover 出现的按钮),因此这类元素带着
hid: true一起送; - 先排序再截断:按关键词、元素类型、ARIA role、test id 打分后只取前
maxCandidates个,默认 40;简单页面可降到 15,密集页面可升到 80; - 限制输出长度:模型最多返回三条策略,token 消耗在每次自愈时打印并记录到事件日志,便于按团队核算。
发送范围也需要明确:给模型的是结构化候选元素列表(标签名、role、ARIA 标签、文本、test id 等)与页面 URL,不发送截图,也不发送完整页面 HTML。若 DOM 可能含个人信息、财务数据或内部地址,可选的路径包括使用本地模型让数据不出机器、指向自建网关、限定只在非生产环境启用,以及定期复核事件日志里实际发出去的内容。

三道审计闸门与成本控制点在流程中的位置
本地模型的实测取舍
在完全离线的本地推理方案上,公开记录过一组对比数据,很适合作为选型参考:
| 模型 | 体积 | 测试结果 | 特点 |
|---|---|---|---|
llama3.2:3b | 2.0 GB | 5/5 通过 | 综合最优:快、准、体积小 |
mistral | 4.1 GB | 5/5 通过 | 最准也最慢,约 1.4 分钟每次运行 |
gemma3:4b | 3.3 GB | 4/5 通过 | 比 mistral 快(约 53 秒),准确率略低 |
1B 到 4B 的小模型在"一排长得几乎一样的复选框"这类场景里会吃力;准确率优先时应选更大的模型。本地方案的代价是每次自愈的模型延迟通常高于云端(云端约 1–2 秒),但换来零 API 密钥、零外发数据、零调用费。
还有一类不走"生成文本"的模型值得单独说:它不写选择器,而是回答带校准概率的判断题——在收集到的候选元素里,哪个是你描述的元素,以及这个元素到底在不在页面上。答案只能是页面上真实存在过的元素,其概率直接作为 minConfidence 检查的置信度,定位器再由代码根据该元素自身属性构造。在一套包含 76 个故意损坏定位器的评测集上(覆盖按钮、链接、输入框、复选框、ARIA 部件与隐藏控件),这种方式全部修复;单次成本约 0.0001 美元,40 个候选的往返不到四分之一秒。当描述与页面上任何元素都不匹配时,它会明确回答"没有",并把最佳猜测连同分数一起放进错误信息,因此描述写得不对也容易被发现。
配置项里最该调的四个
| 配置项 | 默认值 | 调整建议 |
|---|---|---|
quickTimeout | 1000 ms | 刻意设短以便坏选择器快速失败;若应用确实有慢渲染区域要调高,否则会把"慢"误判成"坏",白花一次模型调用 |
minConfidence | 0 | 设为 0.8 左右,让不确定变成失败 |
maxCandidates | 40 | 简单页降到 15,密集页升到 80,直接决定 token 与准确率 |
maxAiTries | 4 | 放弃前最多验证多少条建议,配合三条策略的输出上限使用 |
产物文件要分清职责:缓存文件(修好的定位器,按动作/页面路径/描述组织)建议提交进仓库,让团队与 CI 共享;事件日志(每次尝试的理由、置信度、token 数)与汇总日志加入忽略列表;锁文件与临时文件同样忽略。也可以选择整目录不提交,让每台机器各自维护修复,代价是 CI 每次都要重新调用模型。
已知限制
- 云端模型需要密钥与网络,本地模型两者都不需要,但延迟更高;
- 只搜索主 frame,iframe 内的元素还不是候选,开放 shadow root 可以搜到;
force: true的点击直接派发事件,不带指针坐标,无法覆盖依赖真实指针位置的交互;- 并行 worker 通过锁文件共享缓存,属于 best effort,不是数据库;
- 自愈默认关闭,只有显式打开开关后才生效,因此接入初期套件行为不变,风险可控。
建议的接入顺序
- 保持自愈关闭,先以
warn模式跑一到两周,收集"哪些选择器最常坏"的清单——这份清单本身就是自动化资产的债务表; - 对高频失效的选择器先做人工修复,剩下的才交给自愈,避免把架构问题藏进缓存;
- 打开自愈时同步设置
minConfidence,并在全局 teardown 里启用"有自愈即失败"的规则; - 缓存文件的每一次变更都走代码评审,把它当测试代码而不是构建产物;
- 只在非生产环境启用,涉及敏感 DOM 时改用本地模型或自建端点;
- 定期看事件日志里的 token 消耗与理由分布,判断是"重构变多了"还是"描述写得含糊"。
自愈机制真正解决的,是重构期间大量用例同时红掉、人来不及修的排队问题。它没有解决、也不应该试图解决的是"测试到底在验证什么"。把每一次自愈都变成一条需要被看见、被评审、最终被消除的记录,这个能力才不会退化成把真实缺陷一并抹平的绿灯。
