e2e智能体循环源码深读:tool-loop 如何驱动模型-动作循环
【免费下载链接】e2eNext generation e2e testing framework for web and mobile apps.项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e
e2e 是一款面向 Web 与移动端的开源 AI 端到端测试框架:你用一句自然语言描述测试目标,智能体就会自己驱动应用去达成它。这篇文章深读 e2e 智能体的"心跳"——tool-loop 循环底盘,带你看懂模型-动作循环是如何被一轮一轮驱动、被安全收口的。
一、先看懂:一次agent.act背后发生了什么
在测试里写一行agent.act('把套餐升级到 Pro'),框架并不会立刻去"点按钮",而是启动一个智能体步骤(step),内部是一个反复转动的循环:
- 模型读取当前屏幕(语义树 + 可选截图),产出一个工具调用(如
tap、type、scroll); - 循环执行该工具,把"屏幕上发生了什么变化"作为结果送回给模型;
- 模型基于新结果决定下一步,直到它认为目标达成或彻底无解;
- 模型调用专门的
complete_step工具,提交passed / failed / blocked判决,循环结束。
这个"模型 → 动作 → 观察 → 模型"的闭环,就是典型的tool-loop(工具循环)。下面进入源码,看 e2e 如何把它做成一个稳如底盘的机器。
二、循环底盘:tool-loop.ts的主循环
整个循环的核心在 tool-loop.ts,它被作者称为 "chassis"(底盘):
- createToolLoopExecutor 是底盘的入口。你只需提供三样东西:系统提示词(
system)、工具集(tools)、开场提示(buildPrompt),其余的循环纪律全部由底盘继承; - 每个步骤构造一个全新的 LoopRun 实例,持有本轮的全部状态:已用轮数、硬性停止信号、循环守卫状态、转录记录等;
- run() 主循环 是一个
for(;;)死循环,靠stopWhen条件刹车。
// packages/e2e/src/agent/tool-loop.ts 主循环骨架(示意) for (;;) { const loop = this.buildLoop(tools); const result = await loop.generate({ prompt, ... }); // 文本回复、上下文溢出、工具选择被拒……各自有恢复路径 // 都不命中时才 break,进入判决结算 }buildLoop 把循环注册给 AI SDK 的ToolLoopAgent,两条stopWhen就是刹车片:
- 模型自己刹车:
complete_step一旦落判,立即停止; - 预算刹车:累计轮数达到
maxModelCalls(默认 25 轮)即止,重试也共用同一份轮数预算,绝不"偷偷续命"。
一个很妙的设计哲学写在文件头注释里:硬性停止(预算耗尽/超时/取消)由底盘直接终结循环,绝不把控制权交给模型——预算是外边界,模型只有建议权。
三、每轮必过的"策略关卡":prepareTurn
真正让循环"聪明"的,是 prepareTurn——每发送一次模型请求前,底盘都要按固定顺序检查四件事。
🛡️ 循环守卫:3 次重复先警告,5 次强制收场
模型有时会"转圈":同一操作反复发、或在 A-B-A-B 的短周期里打转。loop-guards.ts 是纯检测器,默认阈值 很克制:
| 守卫 | 警告 | 强制收场 |
|---|---|---|
| 同一调用(含参数)连续重复 | 3 次 | 5 次 |
| 2~4 步短周期重复 | 2 轮 | 3 轮 |
| 连续失败的工具结果 | 3 次 | 5 次 |
检测逻辑在 checkLoopGuards:调用身份 = 工具名 + 精确序列化入参,所以"点两次不同按钮"绝不会被误判为重复。守卫的纪律同样写得很清楚——守卫从不替模型编造判决:警告只是往历史里注入一条[SYSTEM NOTICE]让模型改换思路;强制收场则是下一轮只给它complete_step一个工具,判决依然由模型自己写下。
⏰ 收尾通知:剩 5 轮提醒,剩 2 轮只给"交卷键"
常量定义 里藏着两条时间线:
- 轮数线:剩余 5 轮时注入"快收尾了"提醒;剩余 2 轮时强制只提供
complete_step(留两轮而不是两轮强制,是容忍模型交出一个不合规判决后还有机会修正); - 时钟线:剩余步骤时间低于 60 秒(且不超过总预算的 1/4)时,提前要求模型"现在就交最优判决"——因为慢速模型一回合就要几十秒,让模型带着自己的总结离场,远好过一次没有结论的
STEP_TIMEOUT。
四、complete_step:判决工具与封闭式错误码
循环的终点是一把"锁"。createVerdictTool 创建了complete_step工具,规则是封闭式的:
passed= 应用表现符合预期且已验证;failed= 不符合;blocked= 凭据/环境/测试搭建受阻——blocked必须携带一个封闭清单内的 errorCode,否则被拒回给模型重写(判决规则原文);- 第一个被接受的判决是最终的,之后再交卷一律无效;
- 一个真实踩坑的细节:某些模型习惯在 passed 判决上附带
ACTION_FAILED,早期用 schema 拒绝会导致它反复重发直到预算耗尽——现在的策略是丢弃多余码并明确告知,而不是打回。
五、异常恢复:把"翻车"翻译成模型能读懂的话
tool-loop 里最见功力的,是它几乎不给"硬失败"留空间,每种异常都有恢复路径:
- 普通工具失败→ 经 guard() 包装,变成
tap failed: ...文本送回模型,它自己决定换路; - 模型用纯文本回复、没有工具调用→ continueAfterTextReply 注入一条"你上轮什么也没干"的系统提示,在同一轮预算上再问一次;
- 模型拒绝强制工具选择(HTTP 400)或悄悄降级→ freeToolChoiceRetry 把该模型记入进程级记忆,后续改用
auto模式 + "只允许工具调用"规则,代价只是每模型一次往返; - 上下文窗口溢出→ overflowRetry 收缩历史(被取代的整屏省略、超长文本截断)后只重试一次,第二次溢出才真正报错。
此外每个回合都被 recordTurn 逐条记账,既供--debug输出完整转录,也在报告里保留末尾 12 轮的裁剪版——循环的每一步都可审计。
六、内置智能体 = 底盘 + 语法工具集
现在可以回到全貌。内置智能体在 default-agent.ts 中装配,结构小到一眼看穿:
- 底盘(tool-loop.ts):判决、预算、硬停止、循环守卫、收尾通知、转录——文件头注释 说得直白:"换掉大脑,纪律不变";
- 工具词汇表:createGrammarTools 把每个手势做成独立工具——
tap、double_tap、long_press、right_click、hover,外加像素坐标系的tap_at等。作者特意不用"一个工具 + kind 参数",因为实测有模型会给普通按钮乱填kind导致双击——工具必须被"选择",描述才能把边界说清; - 基线规则:BASE_RULES 教会模型核心世界观:屏幕是带稳定 id 的节点树、每个动作结果已含变化(所以动作后不必再 observe)、绝不臆造 id、只有看到证据才判 passed。
而这个底盘是可复用的:设备端执行器、API 执行器都能调用createToolLoopExecutor,换上自己的工具词表,就免费继承整套循环纪律——这正是 executor.ts 定义的"步骤执行器插座"的意图:换执行器只换思考方式,预算、录制、判决语法一概不动。
七、新手源码阅读路线图
如果读完本文想亲自过一遍,建议按这个顺序读,约 1500 行核心代码:
- tool-loop.ts — 主循环、prepareTurn、四类恢复路径(全文重点);
- loop-guards.ts — 228 行的纯函数守卫,最适合入门;
- primitives.ts —
complete_step判决工具与语法工具集; - default-agent.ts — 内置智能体的最终装配;
- executor.ts — 理解"框架管预算、执行器管思考"的信任模型。
📌 一句话总结:e2e 的 tool-loop 把"让模型自由行动"变成了"让模型在有刹车、有护栏、有审计的轨道上行动"——这大概就是智能体测试框架能进入生产 CI 的关键差别。
【免费下载链接】e2eNext generation e2e testing framework for web and mobile apps.项目地址: https://gitcode.com/GitHub_Trending/e2e6/e2e
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考