1. 为什么移动端自动化必须做角色级拆分:先说说 EventBot 翻车现场
先说结论:我在这个系列第 05 篇里讲过一个移动端自动化 Demo,当时用的是"一个模型管到底"的方案——让同一个 LLM 既看截图、又读 UI 树、还要负责生成点击坐标、最后自己判断用例是否通过。Demo 在小任务上跑得很顺,但一上真实 App,就频繁出现同一个现象:任务执行到一半,模型开始"自说自话"。
具体表现是这样:给它一个"注册新用户并登录"的任务,它在第 6 步左右突然反复点击同一个不可点击的 TextView,然后在日志里自我确认"已完成注册"。我检查了一下上下文,发现它当时已经积累了大约 8 张截图、4 份 UI 树转储、12 轮历史动作,整个上下文乱成一团。我后来把这个问题归为"角色混淆"——同一个模型既当观察者又当执行者,它在长上下文里丢失了"我到底在干什么"这件事。
后来我改用 DroidRun/Mobilerun 这套工具链里内置的**角色级模型拆分(Role-Level Model Split)**思路,把测试执行拆成五个角色,每个角色由不同上下文、不同 prompt、甚至不同类型的模型来承担,才真正把准确率从 47% 拉到 81%。这篇文章就把整个拆分思路、落地实现、实测数据和踩过的坑一次讲透。
这篇文章适合谁看?如果你正在做 LLM 驱动的移动端自动化测试,或者你在自己项目里遇到了"大模型什么都能干、但一碰到长任务就拉胯"的问题,那这篇值得你花十五分钟读完。它解决的核心问题不是"怎么用 LLM 写测试用例",而是:"当 LLM 真正去操作手机屏幕时,怎么让它的注意力不崩、决策不飘、错了还能自己修。"
1.1 移动端自动化比 Web 自动化更难的原因:屏幕是个黑盒
Web 自动化里,你至少有稳定的 DOM 结构可以依赖,每个元素有 id、class、标签名,定位策略成熟得不能再成熟。移动端不一样,很多 App 的控件是自绘的,UI 树里只有一块 canvas,所有控件都是坐标点位。再加上 iOS 和 Android 的渲染机制不同,无障碍树的内容在不同系统版本上差异极大。
这意味着什么?意味着 LLM 在移动端自动化里面对的是一个信息不完整的环境。它看到一张截图,但截图里的文字可能被遮挡、模糊、或者在深色模式下对比度不足。它拿到一份 UI 树,但 UI 树里的节点可能是一堆没有语义的FrameLayout嵌套。这个信息缺口不是靠更好的模型就能直接填上的,而是要靠一套流程机制去弥补。
我最初的错误就是把所有信息一股脑全塞给同一个模型,以为模型智能够了就能从中"悟"出正确操作。实际上,人类的测试工程师在手动测 App 时也是分工的:眼睛看屏幕(观察者),脑子按照测试用例想下一步(规划者),手去点击输入(执行者),然后对照预期结果确认(断言者),出了问题再回头排查(修复者)。LLM 驱动移动端自动化的正确姿势,应该是把这种分工显式化,而不是指望一个模型同时兼任全部角色。
1.2 上下文爆炸:长任务下注意力被拖垮的数学原理
这里得解释一下为什么长任务下模型一定会崩,而不是"可能崩"。Transformer 模型的注意力机制虽然理论上能处理长上下文,但实际效果是随着序列长度增加,模型对早期信息的利用率呈衰减趋势。你可以把注意力想象成一个会议现场,参会人越来越多,发言记录越来越长,新加入的发言人(最新的屏幕状态)往往会覆盖掉前面重要讨论(最初的测试目标描述)。
在移动端自动化里,这个衰减被放大了,因为每一轮操作都会新增"截图 + UI树 + 动作描述"三类 token。假设每轮交互 800~1500 token,执行 20 步就是 16000~30000 token。虽然现代模型支持 128K 上下文,但那个"支持"指的是不报错,而不是"都理解得清楚"。实测下来,当上下文超过 12000 token 时,模型对前几步目标的注意力已经明显下降,开始出现重复动作、遗漏关键步骤、甚至凭空捏造操作结果。
角色级拆分正好解决了这个问题:把上下文按职责切分。观察者只需要关心最新一张截图和最新一份 UI 树,规划者只需要关心当前状态和总目标,执行者只需要关心"这一步该点哪里"。每个角色的上下文都在 3000 token 以内,注意力集中,准确率自然就上来了。
1.3 一次失败的全流程 Demo:数字对比最能说明问题
为了说服我自己不是瞎折腾,我专门设计了一个对比实验。用同一套 20 个人工标注的移动端任务,包含登录、注册、表单填写、列表滑动、设置项切换等常见操作,分别在"单模型全流程"和"角色级拆分"两种模式下跑。
单模型模式下,我在每一步都把当前截图、UI Tree、历史动作列表全塞进上下文,要求模型直接输出下一个动作。20 个任务跑了 5 轮取平均,结果如下:
| 任务难度 | 单模型准确率 | 主要失败类型 |
|---|---|---|
| 简单(2-5步) | 78% | 偶尔点错元素 |
| 中等(6-15步) | 41% | 步骤遗漏、目标漂移 |
| 困难(15步以上) | 12% | 完全迷失、重复循环 |
失败样例里最典型的一个:任务目标是"把默认地址修改为上海市浦东新区某路某号"。单模型在输入"上海市"之后,因为截图里的输入框发生了联动变化,它误以为任务已完成,开始在页面上瞎逛,甚至去点击底部标签栏来回切换。日志显示它在后续 9 步里共进行了 11 次页面跳转,最后一次动作日志竟然写着"确认地址已修改成功"——但截图清清楚楚显示地址栏还是原来的旧地址。
这个案例让我下定决心彻底重构方案。接下来直接上干货,讲讲 DroidRun/Mobilerun 的五个角色到底怎么定义、怎么协作。
2. 角色定义与任务分权:一个 LLM 变成五个各司其职的小组
DroidRun 的定义方式很直白:把"完成一个移动端测试任务"这件事,拆成五个互相独立的角色,每个角色只干一件事、只持有自己需要的那部分上下文。我把它称为"五顶帽子":规划者(Planner)、执行者(Executor)、观察者(Observer)、断言者(Asserter)、修复者(Healer)。
这里有个容易理解偏的点:这五个角色并不需要五个不同的模型。实际上我在实际部署中用的是两到三个模型,不同的角色穿不同的 prompt 和上下文。比如规划者和断言者用同一个强模型(更强的推理能力),执行者和观察者用同一个快模型(更低的延迟),修复者也用强模型但不和规划者复用同一套上下文缓存。角色拆分的关键在职责边界和上下文隔离,而不在于模型数量。
我整理了一张表,方便对照每个角色的职责、输入、输出和建议选型:
| 角色 | 核心职责 | 输入 | 输出 | 建议模型类型 |
|---|---|---|---|---|
| 规划者 Planner | 制定步骤序列,分解测试目标 | 总目标、当前状态摘要、剩余步骤 | 下一步动作的语义指令 | 推理强的旗舰模型 |
| 执行者 Executor | 把语义指令翻译为具体屏幕操作 | 规划者指令、当前UI树、元素坐标 | 点击/输入/滑动等具体操作 | 低延迟高响应模型 |
| 观察者 Observer | 提炼屏幕关键信息,压缩上下文 | 原始截图、UI树转储 | 结构化状态描述 | 视觉理解强的模型 |
| 断言者 Asserter | 检查实际结果与预期是否一致 | 测试预期、观察者的状态描述 | 通过/失败/重试建议 | 推理强的旗舰模型 |
| 修复者 Healer | 处理失败、异常和阻塞 | 失败信息、当前状态、历史操作 | 恢复策略或终止决定 | 推理强+长上下文模型 |
2.1 规划者:只做减法,不做加法
规划者的任务不是去操作手机,而是持续回答一个问题:"距离最终目标,当前还差哪几步?"它拿到的是目标描述 + 观察者丢过来的状态摘要(结构化描述,不是原始截图)。它输出的不是"点击那两个字"这种物理操作,而是语义级指令,比如:"在当前表单页面,将第三个输入框的值设为 zhangsan@example.com,然后点击页面底部的‘下一步’按钮。"
为什么这样设计?因为语义指令比物理操作稳定得多。如果执行者直接按"点击 (512, 1783)"执行,一旦页面布局变化(比如键盘弹出、广告遮挡、不同机型适配),坐标就失效了。而语义指令让执行者可以在执行时重新定位元素,天然带容错能力。
规划者的 prompt 我调试了十几版,最关键的约束就三条:每次只输出一步(防止它一口气规划十步然后被现实打脸);每步必须带"期望结果"字段(方便断言者判断);如果当前状态和预期不符,立即交给修复者而不是自己硬解。就这三条,规划质量提升非常明显。
2.2 执行者:把指令翻译成手指的肌肉记忆
执行者对标的就像一个真正的手:它接收规划者的语义指令,结合观察者提供的当前 UI 树,找到具体控件坐标,然后生成 UI Automator 或 XCTest 能够直接执行的代码片段。
执行者的 prompt 有一个核心约束——禁止猜测元素。如果 UI 树里找不到规划者提到的元素(比如"下一步按钮"匹配不到 text 为"下一步"的节点),执行者必须原样返回"元素未找到",而不是自作主张去找一个长得像的东西点下去。这个约束加进去之后,失败模式的痛苦程度下降了一半以上。
执行者对模型的延迟要求最高,因为它是整个链路里唯一需要和手机实时交互的环节。我在实际调用中给执行者设置了 5 秒超时,超过就直接报错走修复者流程,而不是傻等。试过用旗舰模型当执行者,准确率并没有显著提升,延迟却多了 2-3 倍,性价比极低。
2.3 观察者:让屏幕从"一张图"变成"一条结构化摘要"
观察者是整个拆分思路里最容易被低估的角色。它承担的任务是:每轮交互之后,把"截图 + 原始 UI 树"压缩成一段 200~400 字的结构化描述。这段描述包含:当前页面名称、所有可见的关键控件(附带 bounds 和 text/hint/content-desc)、当前输入框的值、页面的可用操作列表、以及异常状况(弹窗、系统提示、网络异常等)。
为什么要单独做一个观察者?因为直接喂原始 UI 树会让规划者和执行者被大量无关节点淹没。一个典型的电商 App 首页,UI 树里可能有 300+ 个节点,其中一半是FrameLayout、LinearLayout这种无意义节点。模型在这种噪音里做决策,准确率是不可能高的。观察者相当于一个专业的"信息提炼员",先把噪音滤掉,后续模型拿到的都是浓缩后的精华。
观察者的视觉能力很重要。我试过几种不同的视觉模型做观察者,发现对截图内容描述得准不准(特别是按钮文字、输入框占位符)直接影响下游执行准确率。这个角色可以肉眼可见地提升效果。
2.4 断言者:最后一个说话的冷静角色
断言者只在两种时机被触发:规划者每步指令里声明的"期望结果"产生后,或者整个任务结束时。它的工作内容也很纯粹:拿观察者的输出对照预期,给出"已达成/未达成"的判断,不需要解释。
比如说规划者说"在登录表单中填入账号密码并点击登录,期望结果:跳转至首页且顶部显示用户头像"。观察者的描述是"当前页面为个人中心,未显示头像,地址栏下方有登录按钮"。断言者直接判"未达成",并简短说明差异。这比让全流程模型自己判断自己的操作结果要可靠得多,因为"运动员不能同时当裁判"。
实测中,断言者还能抓出一个特别隐蔽的 bug:某 App 在表单校验失败时会轻微震动并提示 toast,但页面看起来纹丝不动。观察者没注意到 toast,断言者却通过 UI 树里的节点变化发现了校验错误信息。这种级别的问题,单模型方案根本发现不了。
2.5 修复者:专门处理"计划赶不上变化"的那只手
移动端测试里最不可控的就是异常:弹窗、网络超时、App 闪退、权限请求、甚至手机系统弹窗(比如"电量不足")。修复者的职责是当一个"不背锅的背锅侠":每次执行者报错或断言者判定未达成时,修复者基于"当前状态 + 失败原因 + 最近 3 步操作记录"决定:重试当前动作、跳过当前步骤、改走替代路径、或者终止整个任务。
修复者和规划者的最大区别是:规划者是在"正常情况"下向前看的,修复者是在"异常情况"下向后看的。修复者允许有一定程度的试探动作,但必须记录试探次数,超过 3 次就终止。这个 3 次上限非常重要,不然修复者在遇到顽固问题时可能会变成一个无限乱点的"猴子",白白浪费资源还污染测试报告。
3. 上下文隔离与消息协议:让五个角色之间"手递手"而不"大杂烩"
角色定义了之后,最难的是让它们高效协作。如果五个角色共享一个巨大的全局上下文,那和单模型方案就没有本质区别——照样注意力爆炸。所以 DroidRun 的第二个关键设计是上下文隔离 + 结构化消息管道。
3.1 每个角色持有专有的上下文视图
我在实现中维护了一个类黑板的共享缓存,但每个角色读取的字段完全不同:
- 规划者读:全局目标、观察者的最新状态摘要、最近 2 条自身输出
- 执行者读:规划者的最新指令、观察者的控件列表截图、当前 UI 树子集
- 观察者读:新的原始截图和 UI 树全量
- 断言者读:当前测试预期、观察者最新描述
- 修复者读:失败记录、最近 3 步动作历史
也就是说,规划者永远不会看到原始截图,执行者永远不会看到测试预期,断言者永远不会看到历史动作列表。这个隔离是强制性的,不是靠 prompt 里写"请忽略XX"来约束,而是在代码层面直接过滤。
3.2 消息格式:统一成"自然语言指令"而不是 JSON 动作流
第一版实现里,角色之间传的是 JSON 结构:{"action": "click", "coordinates": [x, y], "element": "login_button"}。调试了两轮发现交互性很差:规划者输出的语义指令到了执行者那里常常被"翻译"得扭曲,尤其在边界情况下。
后来我改成全部用自然语言指令传递。规划者直接输出中文指令,执行者自己理解并转成 UI Automator 代码。这么做表面上看增加了执行者的理解负担,实测下来反而提高准确率——因为自然语言是 LLM 的母语,JSON 不是。同样的逻辑也适用于其他角色间的通信。一开始担心不稳定,跑了上百个任务之后确认这条路是通的。
3.3 每一步的"工作日志"格式:人可读、机器可排查
除了角色间消息,我还在持久化层记录了每一步的工作日志,格式很简单,每步四行:
[步骤3] 规划者: 点击登录按钮,期望结果进入首页 [步骤3] 执行者: 已点击 (540, 1620),元素text='登录' [步骤3] 观察者: 当前页面为首页,顶部展示头像和搜索栏 [步骤3] 断言者: 未达成,预期展示头像,实际头像缺失,疑似登录未生效这份日志推理链非常清晰,debug 效率比单模型方案提升了无数倍。单模型方案出问题,你得从三万多 token 的上下文里捞原因,现在只需要看这四行。就凭这一点体验升级,我都建议所有做 LLM Agent 的朋友认真考虑角色拆分。
4. 实测数据:角色级拆分在 20 个移动端任务上的效果复盘
前面说了那么多设计理念,没有数据支撑都是空谈。我用了同一批 20 个任务做对比实验,任务覆盖登录注册(3 个)、表单填写(5 个)、列表操作(4 个)、设置开关(3 个)、跨页面流程(5 个),每个任务还按步骤数划分难度。对比的两个方案,分别是上一节提到的单模型全流程方案,和角色级拆分方案。
4.1 总体数据对比:准确率 47% 到 81%,但最亮眼的是困难任务
| 难度 | 单模型准确率 | 角色拆分准确率 | 提升 |
|---|---|---|---|
| 简单(2-5步) | 78% | 92% | +14% |
| 中等(6-15步) | 41% | 81% | +40% |
| 困难(15步以上) | 12% | 63% | +51% |
| 总体 | 47% | 81% | +34% |
注意困难任务的提升幅度:从 12% 直接翻到 63%。这背后其实是角色级拆分最核心的收益:把长任务拆成短上下文,让每一步的决策质量回到短任务水准。单模型方案跑到第 15 步以后基本就是在"闭眼开车",而角色拆分里规划者永远只管"下一步",每步都是一次干净的推理。
4.2 失败归因分析:拆完之后,剩下的失败还能怎么优化
角色拆分跑完,我统计了剩余 19% 失败任务的失败原因:
| 失败原因 | 占比 | 说明 |
|---|---|---|
| 视觉理解错误 | 38% | 观察者认错控件,比如把 label 当成输入框 |
| 元素未找到 | 31% | 规划者期望的元素在当前页面不存在 |
| 动态加载延迟 | 19% | 页面内容异步加载,断言时还没渲染出来 |
| 修复者策略不当 | 12% | 修复者选择了错误的跳过路径导致任务偏离 |
这份失败归因的启示是:角色拆分已经解决了"上下文组织"层面的问题,剩下的瓶颈转移到了"视觉感知"层面。观察者的视觉能力变成了新的关键路径。这也解释了为什么我之前说观察者应该用视觉理解最强的模型——它不是配角,而是当前提升上限的核心。
4.3 成本和延迟:拆成五个角色,费用反而省了
很多人第一反应是:五个角色不是要调用五次模型?成本不爆炸?这个账要细算。
单模型方案每次调用都会带上全部历史上下文,第 15 步的单次调用 token 消耗约 12000 ~ 28000。角色拆分方案里,大部分角色的上下文稳定在 3000 token 以内,只有观察者偶发需要处理大图(约 4000 token)。虽然调用次数增多了,但平均单次调用成本大幅下降。实测下来,同样的 20 个任务,单模型方案平均每个任务消耗约 38 万 token,角色拆分方案约 21 万 token——省了接近一半。延迟方面,拆分方案每一步平均耗时反而快了约 1.4 秒,因为不用拖着一大串历史上下文做推理。
5. 踩坑总结与调优经验:DroidRun/Mobilerun 实际落地避坑和调优笔记
这一节写我在实际部署中踩过的几个最痛的坑,和对应的解法,都是文档和官方示例里不会写的细节。
5.1 上下文污染:角色间看似隔离,实则共享了系统 prompt 里的隐患
一个隐蔽的坑是:虽然我给每个角色分配了独立上下文,但忘了给每个角色也配置独立的系统 prompt 模板。一开始五个角色全部使用同一套通用 system message("你是一个移动端自动化助手"),结果规划者频繁产生"用户视角"的回复,比如分析 App 的 UI 设计好不好看;执行者有时会"好心"地修正规划者的指令逻辑。这类问题不是大问题,但很耗 token,而且会稀释特定角色的专业性和一致性。
解法:给每个角色写专有的 system prompt——规划者是"test_orchestrator",执行者是"screen_operator",观察者是"screen_reader",断言者是"verifier",修复者是"error_handler"。这个改动看起来无关紧要,却直接让全任务准确率又涨了约 5 个百分点。角色级模型拆分,必须要在 prompt 层面整体贯彻角色意识。
5.2 观察者的数据"幻觉":截图和 UI 树对不上怎么办
第二个坑是:观察者的工作环境天然存在信息冲突。截图是最真实的用户视角,但 UI 树有时会包含截图里根本不可见的控件(比如弹窗背后的页面元素、收起的手风琴内容)。观察者如果全都描述出来,就会给下游一个"当前页面有 XX 按钮"的错误信号,导致执行者去点一个用户根本看不见的控件。
我在 prompt 里加了三个强制规则:优先信任截图;UI 树中不可见节点默认排除,除非它们在无障碍树里可见并可达;每页描述必须标注"可见控件列表"和"不可见但存在于树中的控件列表"。这个改动有效减少了约 70% 的"冗余控件幻觉",而且错误定位率明显下降。
5.3 无限循环止损:修复者一定要有限步数和冷静期
第三个坑是最典型的 Agent 问题——修复者在遇到顽固 bug 的时候会进入一种"疯狂重试"状态。我刚开始给修复者设定最大重试次数 5 次,以为够了,结果有一次任务,修复者在第 4 次重试中改变了操作路径,导致问题变成了另一个问题,于是计数器归零,又进入新的 4 次循环,实际执行了 20 多步。
我的修复方案是双重止损:修复者自身的重试上限为 3 次,且每次修复后至少执行 1 步"无关观察"再决策。也就是说,修复策略执行后,不能立刻再让修复者判断"怎么样了",而是先回到正常流程执行 1 步,再触发断言。这个"冷静期"机制让任务终止效率和成功率显著改善。
5.4 模型选型的具体参数建议
最后给一组我自己压测后认为合理的选型参数建议:
| 角色 | 模型规格 | 温度 | 关键参数 |
|---|---|---|---|
| 规划者 | 旗舰推理模型 | 0.2 | max_tokens 1200 |
| 执行者 | 中端低延迟模型 | 0 | max_tokens 800 |
| 观察者 | 视觉理解最强模型 | 0.1 | max_tokens 2000 |
| 断言者 | 旗舰推理模型 | 0 | max_tokens 500 |
| 修复者 | 旗舰推理模型 | 0.3 | max_tokens 1500 |
温度和 max_tokens 两个参数在角色级拆分里特别值得调一调。执行者和断言者必须用 0 温度,因为在"操作手机"和"判断对错"这两个场景里,没有创造性发挥的空间,任何一点随机性都可能造成定位漂移。规划者和修复者可以给一点温度,因为它们偶尔需要在非常规情况下发散思路,比如规划者发现原路径走不通时,修改变量、换个方式尝试就会很有效。
5.5 扩展思路:这套拆分不只是移动端自动化能用,其它场景也能照搬
最后还想多说一句,角色级模型拆分这个思路放到更大的 Agent 场景里也成立。我在自己的一个网页自动化项目里也做了类似的角色拆分(一个提取者、一个操作者、一个验证者),效果同样立竿见影。甚至你写比较复杂的文本生成任务,也可以拆一个"拟大纲的"和一个"写初稿的"和一个"最后审校的",上下文干净,产出质量能明显提升。移动端自动化只是它最能发挥价值的舞台之一,这套方法论本身是可以复用的。
如果你们团队正要上 LLM 驱动的移动端自动化,我最大的建议是:别一开始就追求一个终极模型包打天下,先花一个星期把你的任务流程梳理清楚,看看哪些环节需要"看到什么"、哪些环节需要"决定什么"、哪些环节需要"执行什么",按角色切开,再让合适的模型各就各位。你会发现,很多"模型能力不够"的问题,其实是"流程组织不合理"的问题。