news 2026/10/7 3:28:35

为什么e2e的act步骤可缓存而assert必须实时?设计原理剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
为什么e2e的act步骤可缓存而assert必须实时?设计原理剖析

为什么e2e的act步骤可缓存而assert必须实时?设计原理剖析

【免费下载链接】e2eNext generation e2e testing framework for web and mobile apps.项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e

e2e 是一个面向 Web 和移动应用的下一代端到端测试框架:用自然语言描述目标,AI 智能体(agent)就会驱动应用去完成它。在 e2e 中,agent.act()步骤支持回放缓存——录制一次后,后续运行零模型调用直接重放;而agent.assert()则永远实时执行。这种不对称不是偶然,而是深思熟虑的设计。本文带你从新手视角剖析其背后的原理。

先搞清楚:e2e 的两种 agent 步骤

在 e2e 里,一个测试通常长这样:先让 agent 做事(act),再检查做对了没有(assert / expect)。

  • agent.act()(行动步骤):驱动应用完成操作——点击、输入、导航。它会改变应用状态,比如"把工作区升级到 Pro 计划"。
  • agent.assert()(断言步骤):判断当前屏幕是否满足某条件,比如"发票预览显示按比例计费金额"。它是只读的,绝不能改变应用状态。

这个"一个动手、一个看结果"的分工,正是缓存设计差异的根源。

e2e 的 act 步骤回放缓存是怎么工作的

官方文档 docs/cache.mdx 描述的完整生命周期分四步:

  1. 录制:首次运行时,agent 用模型逐步操作应用,每一步动作(点了哪个控件、输入了什么值)被记录成一条"录制"(recording)。
  2. 确认:act结束后不会立刻写盘。只有当后续的验证步骤通过(如agent.assert或expect(...)断言),录制才在尝试(attempt)结束时被正式写入.e2e/cache/目录。
  3. 重放:下次运行时,框架按缓存条目重放动作——匹配路由、逐个找回控件并重复操作、再核对最终状态。全程零模型调用。
  4. 自愈:如果控件找不到、或最终状态没复现出来,重放会中途"移交"(hand-off)给 agent,从当前屏幕继续执行并自动重新录制。

运行结束后,报告里会看到类似这样的摘要:

AI 4.1k tokens · 2 model calls Cache 4 replayed · 1 handed off · 1 missed

为什么只有 act 可缓存:三条设计原理

原理一:assert 的本质是"测量当下",结果无法预先存储

agent.assert回答的问题是"此刻的屏幕上是不是出现了 X"。答案完全取决于应用当下的状态——数据可能刚被修改、横幅可能刚弹出来、倒计时可能刚结束。

如果把上次运行得到的"通过"结论缓存起来,下次测试就会"闭着眼"通过:断言根本没看一眼屏幕,就宣称一切正常。那断言就失去了存在的意义。判断类步骤唯一正确的做法,就是现在看、现在判。

原理二:assert 正是 act 录制的"验收人"

这是最精妙的一点。e2e 的缓存写入有一个前提:后续验证步骤必须通过。源码注释(packages/e2e/src/agent/act.ts)写得直白:

An assert is a verification step: its passing is what confirms the traces staged before it at attempt end.

也就是说,act的录制先被"暂存"(staged),要靠后面assert的实时通过来盖章确认,才允许写入缓存。两条因果链在这里咬合:

  • assert的判定是act录制是否可信的依据;
  • 如果assert的判定本身也能来自缓存,那么一个过期的"通过"就会永远确认一条可能早已失效的录制——缓存变成自洽闭环,错误永远无人发现。

所以assert必须实时,它是整个缓存体系的信任锚点。

原理三:assert 不改变状态,本来就没有"动作"可录

回放缓存的对象是动作序列(点了什么、填了什么)。而assert一个操作都不做,它的"动作轨迹"天然为空——重放一段空轨迹毫无意义,连查一次缓存都嫌浪费。源码在 packages/e2e/src/agent/act.ts 中直接写死了这个规则:

// Only act steps are cacheable: an assert must not change state, so its // trace would be empty — nothing to replay, nothing worth a read. const cache = spec.kind === 'act' && agent.executor.cache !== 'off' ? runtime.cache : undefined;

同理,agent.waitFor和agent.extract也永远实时执行(见 docs/cache.mdx 开头一句:agent.assert、agent.waitFor和agent.extractalways run live)。

缓存的安全网:应用变了会怎样

既然缓存的是"过去成功的操作序列",应用一变怎么办?e2e 的答案是宁可慢,不可错:

  • 路由不匹配(wrong-context):起点页面变了,缓存不生效,agent 从头执行。
  • 找不到控件(target-not-found):UI 改版了,等待 15 秒仍找不到则移交 agent,下次通过的运行会重新录制。
  • end-mismatch:所有动作都重放成功,但录制里"应该出现的控件"没出现——说明录制已不能产出预期效果。这条录制会被驱逐(evict),下次运行录一条干净的新流程。
  • cache.strict模式:已提交的录制失效时直接以REPLAY_STALE报错、拒绝静默重录,强迫团队显式地重新录制并提交变更。

所有缓存决策(暂存、保留、驱逐)都集中在 packages/e2e/src/agent/step-cache.ts 的StepTraceSession类中,conclude()方法里的注释把"什么情况下保留、什么情况下删除"写得清清楚楚。

新手最佳实践:三步用好回放缓存

  1. 每个act后面紧跟一次验证——agent.assert(...)或expect(screen.getByRole(...))。没有验证,录制不会被确认,缓存就永远建不起来。
  2. 每次运行都变化的值(时间戳、随机邮箱)用unique()包裹,这样重放时会自动替换为当前值,不会因为数据不同而每次都缓存未命中。
  3. 把.e2e/cache/提交进仓库共享重放,CI 默认保持只读(read-only),本地开发用read-write负责重新录制。

一个完整的示例项目可以直接参考:examples/with-next/,下面是它的运行截图。

关键源码与文档路径

想深入源码,从这几个入口开始:

  • 缓存核心(按键派生、重放尝试、暂存/驱逐决策):packages/e2e/src/agent/step-cache.ts
  • 步骤分发与"只有 act 可缓存"的闸门:packages/e2e/src/agent/act.ts
  • 回放执行与终态校验:packages/e2e/src/agent/replay.ts
  • 缓存官方文档(重放细节、失效原因表、strict 模式):docs/cache.mdx
  • Agent 与多角色配置:docs/agents.mdx

总结

一句话记住 e2e 缓存设计的核心逻辑:

act 改变世界,所以它"改变世界的方式"可以被录制下来;assert 审判世界,而审判必须来自"此刻"。

act 步骤可缓存,是因为它是一段可重复的机械动作序列,且录制经过了实时验证的盖章;assert 必须实时,是因为它的输出是对当下状态的判断——缓存一个判断,就等于取消了判断本身。理解了这一点,你就理解了 e2e 如何在不牺牲测试可信度的前提下,把模型调用成本降到近乎为零。

【免费下载链接】e2eNext generation e2e testing framework for web and mobile apps.项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 3:28:19

PP-OCRv4模型转换部署实战:从ONNX到RK3588 NPU加速

上个月帮客户做一批基于RK3588的边缘识别终端,OCR模块要识别设备铭牌上的型号序列号。最开始在服务器上用PaddleOCR的PP-OCRv4跑得很顺,一到嵌入式平台就卡壳:Paddle Inference在ARM板的部署依赖太多,算子支持得逐个验证&#xff…

作者头像 李华
网站建设 2026/10/7 3:28:19

C/C++任意长整数加法实现:从存储结构到进位逻辑

简介:这是一份面向数据结构与C/C初学者的课程设计资源,围绕「任意长整数加法」这一经典链表应用题展开。程序要求利用双向循环链表存储超长整数,实现两个任意长度整数的求和运算,并按每四位一组、组间以逗号分隔的格式完成输入与输…

作者头像 李华
网站建设 2026/10/7 3:28:19

VMware虚拟机忘记密码?十分钟离线重置Windows/Linux登录密码

前阵子有位同事找我救急:他在VMware Workstation Pro里建了一台Windows 10虚拟机,开机密码存在系统便签里,结果便签被清理,脑子里的记忆也跟着“清理”了。虚拟机里是整整一天的编译环境和一堆工程,重装一遍至少损失一…

作者头像 李华
网站建设 2026/10/7 3:27:23

Allegro 8层板Gerber光绘导出全指南:模板复用与错误排查

又到了项目交板的节点,群里照例有人开始问:Allegro光绘怎么设置?film为什么要配那么多层?为什么我导出的Gerber板厂说打不开?作为一个从四层板一路画到十六层板的老工程师,说实话,光绘本身不难&…

作者头像 李华
网站建设 2026/10/7 3:27:10

OSI七层模型实战:从数据封装到网络排障的完整指南

1. 为什么学了七层协议,遇到真实网络问题还是经常懵先讲个我自己的经历。刚入行那阵子,我把七层协议背得滚瓜烂熟,物理层、数据链路层、网络层、传输层、会话层、表示层、应用层,口诀都编了好几个。结果第一次独立处理一个"网…

作者头像 李华
网站建设 2026/10/7 3:25:10

MOS管替代二极管实现高效电源自动切换

1. 为什么不用二极管而选MOS管做电源自动切换?你手头有个带USB接口的便携设备,比如一个自制的蓝牙音箱、数据采集盒子,或者一块带屏幕的STM32开发板。它既要能插USB线供电调试,又要能装上锂电池实现移动使用——但你绝不想每次换电…

作者头像 李华