我给 UI 自动化提了个新标准:零代码,半探索式

路书(Roadbook) —— 给 coding agent 用的 UI 主流程回归 Skill
https://github.com/ouylzq/roadbook · MIT · v0.1.0

标准一共三条:

  • 期望零代码 —— 用人话写一遍。没有选择器,没有断言代码,没有第二份需要同步的期望值
  • 执行半探索 —— 有路书就照路书开,路况变了才勘路,勘完必须把路书改掉
  • 结论不归执行者 —— 开车的 agent 不许报成绩。独立的裁判去取证,一百多行代码负责记分

前两条决定它有多省事,第三条决定它的结论能不能信。这篇主要讲前两条——第三条是它
存在的理由,我放在后面说。


UI 自动化现在卡在两个极端之间

脚本派(Playwright / Selenium)稳、可重复、能进 CI,代价是写和养。选择器跟界面绑死,
界面一改脚本就红——而那片红里,往往有一半是脚本过时,不是功能坏了。很多团队最后花在
维护测试脚本上的时间,比写功能还多。

AI 派起手成本几乎为零,丢一句话就能跑。代价是结论不可信:它点完看一眼屏幕说「通过了」,
而它恰恰是刚刚亲手把流程走完的那一个。你在让运动员给自己打分。

路书想占的是中间那块:起手像 AI 派一样便宜,结论像脚本派一样能复核。

起手成本维护成本结论能复核吗能进 CI
脚本派高:写选择器 + 断言高:界面一改就得改
AI 派极低:一句话不能,凭它说
路书低:写人话低:agent 自己改路书能,留页面原文还没解决

最后一行那个「还没解决」是真的,不是谦虚,短板那节会说。

零代码:期望只写一处,用人话

先看同一条流程的两种写法。流程是:加一个任务,确认它真被存下来了,而不是只渲染了一下。

脚本派的写法:

test('新增任务后还在', async ({ page }) => {
  await page.goto('http://localhost:4173/');
  await page.click('#reset');

  await page.fill('#title', '发布路书演示');
  await page.click('button[type=submit]');

  await page.reload();                                   // 回读
  await expect(page.locator('#counter')).toHaveText('未完成 1');
  await expect(page.locator('#app li')).toHaveCount(1);
  await expect(page.locator('#app li .title')).toHaveText('发布路书演示');

  await page.goto('http://localhost:4173/?view=stats');
  await expect(page.locator('#stat-total')).toHaveText('1');
  await expect(page.locator('#stat-open')).toHaveText('1');
});

路书的写法:

goal: 在演示应用里加一个任务,确认它是真被存下来了,而不只是渲染出来

steps:
  - go: 打开任务列表 http://localhost:4173/ 并读取列表
    check:
      - 任务列表里正好有一条任务,标题是 context.task_title
      - header 里的计数器显示「未完成 1」

  - go: 打开统计视图 http://localhost:4173/?view=stats
    check:
      - 「总任务数」等于 1
      - 「未完成」等于 1

一份 expect.yaml,一步只有 gocheck 两个键。三个「不」:

  • 不写选择器。 #app li .title 这种东西一个都没有。改版换了 DOM 结构,这份文件不用动。
  • 不写断言代码。 toHaveText / toHaveCount 全没有,期望就是一句中文。
  • 不写第二份期望值。 context.* 只引用 runner 跑的时候才知道的事实(它实际输入的名字、
    新建对象的 id);期望值直接写死在这里,不由 runner 转述——否则裁判就是在按 runner 的
    说法判 runner。

文件本身也极短:左边 15 行代码,右边 13 行配置。真正的差别不在长度,在谁需要懂技术——
右边那份,产品和测试都能写、都能审。

有个意外收获:把 expect.yaml 从「一堆无序的事实声明」改成有序路线之后,裁判的工具调用
从 24 次降到 10 次。它不用自己规划找路了。省 token 是副产品,主产品是判决变稳定了。

半探索:有路书照路书,失配才勘路

这是路书这个名字的来处——拉力赛。领航员手里有一本路书,照着开;路况和路书对不上了才重新勘路,
勘完必须把路书改掉,下一辆车才能用。

两种模式:

什么时候用agent 干什么花不花钱
回放playbook.md照着走,锚点对上就操作,不重新思考"该点哪"
探索没有路书,或回放中途失配自己看画面找路,跑通后把路书写回来

注意那个「半」字——探索不是常态,是降级。默认走录音,只有失配了才花那个钱。这是刻意的:
纯探索式每次都重新找路又慢又不稳定,纯回放式界面一改就废。半探索取的是「平时便宜、
变化时能自愈」。

路书长这样(这是仓库里真实存在的一份,scenarios/todo-persistence/playbook.md):

2. 【参考】add-task
   【目的】添加固定标题的任务
   【锚点】顶部输入框(placeholder「需要做什么?」)和右侧蓝色按钮「添加任务」
   【动作】点输入框 → 输入 intent.md 的固定标题 → 点「添加任务」
   【到位标志】列表出现该标题,header 显示「未完成 1」

3. 【参考】—
   【目的】写操作后回读,确认存下来了而不是只渲染了
   【锚点】当前 URL
   【动作】navigate 到同一 URL,等 2 秒,读页面文本
   【到位标志】列表里仍有该标题;?bug=optimistic 模式下这里会变成「还没有任务。」

每步五要素:【参考】【目的】【锚点】【动作】【到位标志】。它由 agent 写,进 git。

于是有了个副产品:路书的 git diff 就是你的 UI 变更记录。

它为什么会变?因为界面改了。改的是哪一步?diff 告诉你。这比「测试红了,你去看截图猜」
强得多——diff 直接指向那一步。

那结论谁来给

前面两条讲的是省事。但省事的工具很多,这条才是路书存在的理由。

某个下午,同一个产品上,我撞见两次:

  • 保存弹层弹出来说「已保存」,后端还是旧值。
  • 把数量从 4 改成 2,卡片显示 2,概览面板也显示 2,后端却仍是 4。真实数字一直藏到最后
    的支付弹层才露出来——到那时操作者早就按着错的金额点了确认。

这两次,任何相信屏幕的人都会报 PASS。包括 AI。

所以路书把通常被混为一谈的三件事拆开:

谁做怎么做
理解期望模型读一句人话
下判决模型,但受协议约束每条 pass/fail 必须引用读到的页面原文
给结论记分代码只查完备性,绝不碰语义

关键在最后一行。记分器全仓只有一百多行,它从不比对数值——只确认裁判把每一步都走了、
每条都答了、每个答案都有原文兜底。

而裁判的独立性不靠 prompt 恳求,靠文件层面的物理隔离:

runner 读:intent.md、playbook.md、atoms/
runner 从不读:expect.yaml      ← 它不知道验收标准是什么

裁判读:expect.yaml、context.yaml
裁判从不读:playbook.md、atoms/  ← 它不知道路是怎么走出来的

裁判是一个新起的 agent,没碰过方向盘,也没见过路书。它拿到的只是一份「去哪、看什么」的
路线,自己一步步走、自己抄原文。你没法在 prompt 里绕过一道根本没读进来的文件。

三条规定,压缩版:每一屏都是裁判自己加载的(绝不判读一个「到的时候就已经打开着」的页面,
那可能是 runner 留下的乐观渲染);证据必须原文引用(判了 pass 却没证据,记分器判 BLOCK
而不是 PASS);BLOCK 是一等结论(环境坏了 ≠ 功能坏了,退出码 2,而它成立的次数比大多数
团队愿意承认的要多)。

五分钟试一下

仓库自带演示应用,单个 HTML 文件、没有后端,不用指向生产就能看完整闭环:

git clone https://github.com/ouylzq/roadbook.git
cd roadbook && pip install pyyaml
preview_start { "name": "demo" }        # 在 :4173 上跑 demo-app
/roadbook todo-persistence

场景自带 playbook.md,所以你看到的是回放模式:runner 照四步开、写下 context.yaml、交棒;
裁判自己走两步、抄原文,得到 PASS。

更有意思的是跑一次故意的失败:

http://localhost:4173/?bug=optimistic

这个模式下新任务会渲染、header 计数器也会变,但从不写入存储。加完的那一刻一切正常。
裁判照样抓得到——那两屏都是它自己加载的——拿到 FAIL,证据是引用的页面原文「还没有任务。」。

一条命令复现这套设计的全部理由。靠截图断言的话,这次会被判 PASS。

它不擅长什么

我不想把它吹成成熟方案,这几条也原样写在 README 里:

  • 还没有 CI 路径。 全部跑在交互式 agent 会话里。无人值守跑批——Playwright 最大的优势——
    这里没解决。
  • 证据基础很薄。 一个真实产品、两个场景、六次 run、大约一周,加上四个演示场景各跑过一次。
    references/browser-pitfalls.md 里那八个坑全是实测的,但框架的普适性还没被证明
  • 真花 token。 五步流程实测,裁判一次约 70–90k tokens、3–7 分钟。这不是每次提交都能跑的检查。
  • 和工具耦合。 协议里写着 Claude Code 的浏览器工具名。换 Playwright MCP 的移植说明写了,未经验证。
  • 只回答一个问题。 「这条流程还跑得通吗」,不回答「为什么坏」。日志和数据库取证是刻意排除在外的。

这不是一个成品,是一份提案

它现在最缺的不是代码,是接触它没被开发过的界面

这套规范是在一个真实产品上长出来的。它在我熟悉的那个界面上好用,但到了别的产品上还成不成立,
我不知道——很可能不成立,或者需要改。这正是我现在最想知道的。所以如果你在别的产品上跑过,
把那个场景交回来,比帮我改代码有用得多CONTRIBUTING.md)。

回到开头那三条标准。前两条——零代码、半探索——省的是写脚本和养脚本的时间,别人也在做,
路书做得不算特别。难被复制的是第三条:执行的和判决的不是同一个,而且这道隔离写在文件
层面,不写在 prompt 里。

UI 自动化接下来会长成什么样我说不准。但「让开车的给自己打分」这一条,我觉得迟早会被当成
坏味道。这三条标准算是一个早一点的提案。


链接:https://github.com/ouylzq/roadbook

MIT 许可,v0.1.0。装成用户级 skill 之后,在任何项目里用 /roadbook 就能调起。

Logo

一站式 AI 云服务平台

更多推荐