e2e 智能体重放缓存完全指南:验证过的步骤如何零模型调用极速回放
【免费下载链接】e2eNext generation e2e testing framework for web and mobile apps.项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e
e2e 是一个面向 Web 和移动端应用的下一代 AI 端到端测试框架(AI E2E testing framework),你只需一句自然语言描述目标,智能体(Agent)就会驱动应用完成任务。而它最被低估的杀手锏是重放缓存(Replay Cache):第一次运行由模型完成的agent.act()步骤,只要被后续断言验证通过,就会被记录成一份"动作收据";下次运行直接零模型调用回放(zero model call replay)——更快、更稳、更省钱。本文不深入代码,用一张图、一张表就能看懂的篇幅,讲透它从"录制 → 命中 → 回放 → 降级"的完整链路。
一、为什么需要重放缓存:快、稳、省 🚀
AI 智能体测试有个绕不开的成本三角:
| 维度 | 每次都调用模型 | 启用重放缓存后 |
|---|---|---|
| 速度 | 每步等待模型思考,秒级到十秒级 | 机械回放,接近原生测试速度 |
| 成本 | 每次运行都消耗 token | 命中回放时 0 token |
| 确定性 | 模型每次可能给出不同操作路径 | 回放路径与上次验证时完全一致 |
重放缓存的核心设计哲学是"先验证,后记录":
智能体步骤执行完毕后,必须有一个后续的验证步骤(
expect断言、agent.assert等)证明结果正确,运行器才会把这份操作轨迹写入磁盘。没有验证的步骤,录了也白录。
这意味着缓存里存的每一条目,都曾是"被证明能产生正确结果"的完整流程——这是它敢跳过模型直接回放的底气所在。
二、一次回放如何发生:4 步流程
缓存命中后,运行器并不会盲信录制文件,而是按 replay.ts 中的零轮回放流程逐步推进:
- 核对起始屏幕:回放前比对当前"路由"(origin + 路径 + 查询参数)。路径里的记录 ID、时间戳会被当作占位符,所以
/orders/42和/orders/7算同一路线;但?mode=safe与?mode=unsafe是不同屏幕,会判为wrong-context直接放弃回放。 - 逐个重定位控件并执行:按角色、名称、test id、周边上下文在全新的页面快照里重新找到每个录制控件,然后重复它的动作——点击、输入、勾选、滚动、拖拽都走与模型完全相同的"动作语法"。找不到会按 100ms → 3s 的退避节奏重试(见 replay.ts 的退避表),15 秒内仍找不到就交还给模型。
- 校验终点状态:这是最关键的一步。回放跑完所有动作只证明"点完了",还要证明"真的生效了"(详见下节"终点锚点")。
- 收尾或降级:全部通过 → 步骤"自我终结"(self-finalized),零模型调用结束;任何一步不符 → 携带"已执行到哪、为何停下"的记录,把步骤交还模型,从当前屏幕继续。
三、三个核心机制,决定回放能否命中
3.1 缓存键:命中条件比你想的更严格
一条缓存条目只属于"特定的测试 + 目标 + 指令 + 参数 + 智能体"。改测试名、改指令文本、改任一普通参数、换配置的智能体、升级引擎主次版本,都会导致 miss;换模型反而不影响(见 identity.ts 参与缓存键推导的身份定义)。
两个高频坑:
- 动态值会导致每次 miss:比如用时间戳命名公司。解决办法是用
unique()包裹——缓存键和录制里留一个占位槽,回放时填入当次的最新值:
const name = `E2E ${Date.now()} Company`; await agent.act('create a company named {name}', { params: { name: unique(name) } });- 重试永不回放:重试意味着本次尝试已经出过错,必须重新走模型并重录,避免在"坏状态"上叠加坏回放。
3.2 目标重定位:宁缺毋滥的精确匹配
录制时,每个动作的目标都不存"当次快照的临时 ID",而是一份持久化描述符(role / name / testId / placeholder / 所在容器等,见 trace.ts 的TraceTargetDescriptor)。回放时由 relocate.ts 的relocateDescriptor在新快照中重新匹配,策略是"保守派":
- 两级匹配:先试"严格级"(所有身份字段全等);若该应用每次渲染都重新生成 testId,会降级到"语义级"(去掉 testId 再试)——控件没变,只是"标签"变了。
- 匿名控件按位置定位:没有名称的图标按钮,靠"它是同类控件中的第几个"来认,但只有当它独一份、或位于有名称的容器(行、卡片、分组)内才允许——否则行序一变就会点到别人的按钮。
- fail-closed 契约:恰好一个匹配才执行,两个匹配(歧义)或零匹配(缺失)都立即交还模型。没有打分、没有模糊匹配、没有视觉猜测——宁可交还给模型,也绝不猜。
3.3 终点锚点:防止"点完了但没生效" 🎯
这是重放缓存区别于"录制-回放工具"的灵魂。录制时,运行器对比步骤开始与通过时的两帧屏幕,把**差异(delta)**存进条目(见 anchors.ts):
endAnchors:步骤让哪些控件出现了(最多 8 个,公告类优先);goneAnchors:哪些控件消失了(删除项、关闭的弹窗)。
回放结束时做双重校验:
- deltaHolds:出现过的都要回来,消失掉的都要保持消失,而且不能弹出录制时从未见过的 alert(弹了新错误,说明这次的结果不一样)。
- deltaEvidenced:变化必须是本次回放亲手产生的。如果屏幕在动作开始前就已经显示 "Submitted",回放完一切"符合预期"——但这什么都证明不了,判定失败。
另外,日期、倒计时、纯数字徽标这类"每次运行都不同"的文本会被自动排除在锚点之外;而"3 条记录已导入"这种点名所数对象的计数,若本步骤让它出现,则必须回放时读出一模一样的数字。慢应用也有照顾:回放会按录制时实测的耗时 + 余量(endWaitMs)等待终点状态落定。
四、回放失败怎么办:自适应降级,永不致命
重放缓存的设计原则是"任何原因导致回放完不成,都只是把步骤交还给模型,而不是让测试挂掉"(运行时超时、取消这类"硬性停止"除外):
- 中途移交(hand-off):执行了 3/8 个动作后控件消失?运行器记录"已执行的动作摘要 + 停止原因",模型拿到的是带上下文的接力棒,不会从头再来一遍。
- action-uncertain:某个输入"可能已提交、可能没有"时,运行器绝不自己重发(避免重复下单这类事故),而是明确告诉模型"先验证再行动"。
- 自动清除污染条目:回放消耗了条目、但步骤最终失败的,该条目会被驱逐(evict),防止"坏前缀"在之后的每次运行中反复重演。
- --strict-cache 让 CI 大声失败:默认情况下过期的录制会静默回落到模型(CI 悄悄烧 token)。开启严格模式后,存在但不再可回放的录制会直接以
REPLAY_STALE让测试失败且不调用模型,逼团队尽快重录(过期原因清单见 step-cache.ts)。
三种缓存模式配合不同环境(详见 docs/cache.mdx):
| 模式 | 回放 | 录制 | 默认场景 |
|---|---|---|---|
read-write | ✅ | ✅ | 本地开发 |
read-only | ✅ | ❌ | CI(未显式配置时) |
off | ❌ | ❌ | --no-cache单次排除缓存因素 |
五、运行报告速读:3 个状态 + 常见原因 📊
每次运行的汇总里,缓存与 AI 用量是分开的两行:
AI 4.1k tokens · 2 model calls · anthropic/claude-sonnet-4.5 Cache 4 replayed · 1 handed off · 1 missed| 运行摘要 | step.cache.mode | 含义 |
|---|---|---|
replayed | self-finalized | 录制完整回放,零模型调用 |
handed off | agent-concluded | 回放启动后中途移交模型 |
missed | missed | 缓存未命中,模型全程执行 |
没回放时,报告还给出step.cache.reason,最值钱的几个:no-entry(首次运行,下次验证通过就会录上)、wrong-context(起始屏幕不对,打开录制的同一屏幕再跑)、target-not-found(UI 变了,下次通过运行会自动重录)、gap(步骤里含模型临时从屏幕上"读出来"的值,该段永远走模型)、end-mismatch(动作都跑了但效果没回来——可能是残留数据或真回归)。排查时可以用npx e2e cache ls/stats/clear查看条目,用npx e2e run --no-cache排除缓存本身的嫌疑。
下图是框架自带的示例应用界面(examples/with-next):智能体正是在这类表单上完成"输入 → 点击 → 验证"并录制为可回放的步骤。
六、让命中率更高的 5 个实操技巧 ✍️
- 每个
agent.act()后面紧跟一条验证:expect(...)断言、locator.waitFor()才算验证;没有验证,步骤不会被写入缓存。 - 动态数据一律
unique():时间戳、随机邮箱、自动递增编号,包起来才能跨运行命中。 - 保持稳定的起始屏幕:步骤要么从录制时的同一屏幕开始,要么让录制以
navigate开头(自带起点,无需前置屏幕)。 - 提交
.e2e/cache/目录:本地读写的运行会生成录制,提交后 CI 默认 read-only 直接回放;重录产生的变更走 PR 评审,像测试数据一样管理。 - CI 加
--strict-cache:让过期录制立刻红灯,而不是让每次 CI 悄悄为模型买单。
七、写在最后
一句话总结 e2e 重放缓存的四条设计铁律:验证后才落盘(不录侥幸通过的流程)、fail-closed 回放(宁可移交也不猜)、降级永远可用(回放失败 = 换模型继续,而非测试失败)、过期自动清理(坏条目活不过一次失败)。理解这四点,你就掌握了"零模型调用回放"的全部精髓。
延伸阅读📚
- 官方文档:docs/cache.mdx(回放缓存完整参考)、docs/quickstart.mdx(快速上手)
- 回放核心源码:packages/e2e/src/agent/replay.ts(零轮回放引擎)、packages/e2e/src/agent/step-cache.ts(每步缓存会话)
- 缓存子系统:packages/e2e/src/cache/relocate.ts(目标重定位)、packages/e2e/src/cache/anchors.ts(终点锚点)、packages/e2e/src/cache/decide.ts(回放决策)、packages/e2e/src/cache/trace.ts(trace-1 条目格式)、packages/e2e/src/cache/store.ts(文件存储与原子写)
- 项目总览:README.md
【免费下载链接】e2eNext generation e2e testing framework for web and mobile apps.项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考