
近两年做移动端自动化我身边不少测试开发朋友都有类似困惑手工脚本还能写但一旦功能版本迭代起来维护成本直接压垮人想让大模型来接管又觉得“给一个提示词就全自动跑UI回归”更像Demo而不是能落地的工程方案。我手里这套DroidRun/Mobilerun的迭代项目也一样早期把所有任务都塞给同一个LLM角色结果开会演示时很惊艳真放到持续集成里跑一周各种失控、漏断言、上下文错乱全冒出来了。这篇系列第六篇我打算把当时逼出来的“角色级模型拆分”方案讲透为什么需要拆成规划、执行、观察、汇报等独立角色模型拆分后DroidRun怎么编排它们完成移动端自动化测试以及我在真机、模拟器上踩过的那些坑。想把这个方向做成工程的或者已经在试LLM辅助移动端回归的这篇应该都能给你一些能直接抄作业的思路。1. 为什么移动端自动化会走到“角色级模型拆分”1.1 传统脚本模式的死穴传统移动端自动化测试最成熟的一套思路是录制回放加Page Object维护。Appium、Maestro、XCUITest本质上都是帮你在代码里把“找元素—做操作—断言状态”这三件事固定下来。这套玩法的问题不在于不好用而在于它把测试意图和UI细节绑得太死。举个例子回归测试里有一条“用户从首页进入商品详情页添加到购物车”。脚本写起来很简单找到商品卡片、点击、找到加购按钮、点击、断言购物车角标。可一旦界面改版卡片改成宫格布局加购按钮从详情页底部挪到侧滑菜单脚本里所有定位表达式都得跟着改。更麻烦的是很多移动端功能存在多种等价路径比如可以点按钮也可以左滑呼出操作栏脚本往往只能覆盖那一条固定的交互路径测试覆盖存在明显盲区。在DroidRun的早期版本里我们也是按这个模式堆脚本。脚本数量到两千多条的时候每次发版前跑一次全量回归脚本修复花掉的时间比新增功能的测试设计时间还长。那时候我就意识到真正值钱的不是怎么找控件而是把“测试要验证什么”和“具体操作怎么执行”分开让剩余的部分可以由新的自动化引擎自动补全。1.2 把整个流程都交给一个LLM问题更大看到LLM能力变强之后第一反应很简单把截图、页面结构、测试目标一起丢给模型让它自己生成操作自己判断结果这不就把维护成本干掉了吗DroidRun演示阶段确实这么干过一个Agent模型同时承担思考、决策、执行、断言的所有职责。Demo版跑通很快因为场景就那么三五个。但一旦进入完整业务回归问题立刻出现责任混在一起上下文非常臃肿。系统要把页面XML、历史操作、业务规则、断言条件全部塞进同一份提示词经常逼近上下文窗口上限。同一个模型既想探索页面又要精确执行点击输出动作格式不稳定有时输出JSON多一点注释、少一个字段工具解析直接失败。模型在生成动作时会出现自我证明偏差——它自己执行、自己观察、自己判定成功缺少外部校验于是模型会认为“我点了所以生效了”根本没去看页面是否真的切换了。问题定位困难。一批用例挂掉说不清是决策判错、控件定位错还是执行时序错。后来我重新看整个测试过程发现它天然就不是一个单一智能体场景。做测试的团队里有测试设计、测试执行、结果确认、测试报告这些不同角色人对复杂任务的拆解方式天然适应这种分工。那我为什么不让模型也按角色拆开呢这就是角色级模型拆分在DroidRun里产生的原因。1.3 角色级模型拆分解决的核心问题拆分的核心思想只有一句话不要训练或配置一个“什么都会”的大模型单元来完成整条测试链路而是把测试链路拆成多个语义清晰、职责单一的角色模型每个角色只接特定的输入、输出和约束通过一套稳定的协作协议组合起来。在DroidRun/Mobilerun里我们把这些角色模型定义成了四条流水线Planner规划者、Executor执行者、Observer观察者、Reporter汇报者。这套拆法既保住了LLM的语义理解能力又让每一个环节都可独立调试、可单独替换、可精确控制。移动端自动化最头疼的“动作粒度控制”和“结果可信校验”也正是通过拆出来一个专门角色才真正做到可控。2. DroidRun/Mobilerun的设计角色有哪些各管什么事2.1 四个角色模型怎么切分角色级模型拆分的第一步不是急着写提示词而是把测试场景里的职能彻底划清楚。我们当时按下面这个表格梳理了分工角色职责输入输出Planner理解测试目标生成高层任务序列测试需求、业务约束、可用页面列表有序任务图不涉及具体控件坐标Executor把高层任务转换为低层UI操作当前页面结构、单个任务目标稳定格式的动作指令点击/滑动/输入Observer观察页面变化判断操作结果前后页面状态、操作日志结果断言、页面状态摘要Reporter汇总各步骤结果产出报告并反馈控制循环各角色的运行日志与断言结构化的测试报告、失败原因摘要注意我特意把Observer和Reporter分开。很多LLM测试框架把“观察结果”和“生成报告”合并省事但不好排查。真正跑起来你会发现页面状态摘要的格式需求和报告里写给人看的结论是完全不同两类输出硬放在一起提示词里的指令互相干扰最后模型两边都做不精。Planner是唯一能看到全局目标的角色。它不需要关心“这个按钮在当前屏幕的哪个位置”只需要判断“这个测试目标应该经过哪些页面、哪些业务动作”。这样设计的好处是页面改版最频繁的部分被隔离开只要业务交互逻辑没变Planner输出的任务图就不用改回归测试的稳定性直接上了一个台阶。Executor是动作执行层。它接收Planner给出的一个高度聚焦的子任务比如“进入商品详情页”再结合当前屏幕的真实UI结构生成具体操作。这个角色一次只处理一个子任务上下文窗口压力也小。我们需要它输出可解析的动作序列所以对输出格式的约束非常严格Alignment相关的内容我会在第4章细说。Observer理论上不需要知道测试目标它就是一个“无感情的监控探头”。每次Executor执行完动作Observer会把执行前后的页面状态拉出来做对比输出结论页面有没有变化、关键元素是否出现、状态是否符合预期。Observer的输出不直接决定用例通过与否它只提供事实判定交给上游编排逻辑。Reporter负责最终呈现和记录。它在Observer事实的基础上结合Planner的任务目标组织一个给人看的结果。它也会主动标记哪些步骤有异常把失败路径摘要回传给Planner让Planner决定是换一条路径继续尝试还是终止这条用例。2.2 角色之间的协议状态数据流怎么走角色拆分之后最怕的一件事是各角色之间“对话”过于自由。让LLM角色直接互相发自然语言消息看起来灵活跑起来极易失控因为模型会在对话里把职责边界模糊掉。DroidRun的做法是给角色之间定义一个稳定的中间数据结构角色之间不直接传导自然语言统一通过结构化传递。这里我用一个简化的伪JSON来描述这条链路{ task_id: TC_1024, goal: 验证用户可以从首页完成加购并查看购物车数量变化, plan: [ {step_id: 1, action: navigate_to_product_detail, target: 首个商品卡片}, {step_id: 2, action: add_to_cart, target: 详情页加购按钮}, {step_id: 3, action: open_cart_and_verify, target: 购物车页面} ], executions: [ { step_id: 1, output_actions: [ {type: tap, coordinates: [340, 620], description: 点击首屏商品卡片} ], result: {page_changed: true, new_screen: product_detail} } ], assertions: [ {step_id: 3, type: element_exists, target: cart_badge, status: pass} ], report: PASS: 用户加购后购物车角标从0变为1 }这个结构的价值在于Planner产出的是一个计划数组Executor每完成一步就往executions里追加记录Observer按step_id把结果填进去Reporter最后读取所有内容生成汇总。每个角色读到的只是它任务范围之内的那一段而不是整段对话历史。这一步是角色拆分能真正落到工程基础上的关键。一开始我们也想过用自然语言“任务卡”在各角色间传递后来发现Executor偶尔会把Observer的观察结果当成自己该完成的目标跑出完全不该有的操作。改成结构化数据后这类“角色串扰”基本消失了。2.3 这种拆法到底解决了什么问题用角色级模型拆分之后原先单一大模型方案里的几个核心痛点都有了对应解法第一上下文可控。每个角色不再需要看到整条测试链路执行角色只需要当前页面结构加一个子任务几百上千tokens就足够成本也低了很多。第二可观测性大幅提高。因为在角色之间引入结构化中间结果任何一步失败都可以通过数据定位是计划错了、执行错了还是断言错了。第三提示词工程从“一个大而全的提示词”变成“四个小而精的提示词”每个提示词都可以单独迭代。第四角色模型可替换将来如果某个环节用更小的专有模型或规则引擎更合适可以整块换掉而不影响其他部分。移动端自动化里我最看重的其实是可观测性。因为设备碎片化、系统版本不同、动画时序不一致很多测试失败都是环境和时机问题。角色拆分之后Observer只看页面变化不参与操作它反馈的“页面未变化”可以让我们很快区分是执行器点错了还是页面真的无响应排查效率提升明显。3. 实操落地从提示词到可执行的最小框架3.1 移动端自动化的环境准备与工具选型讲完设计我直接说怎么落地到代码。DroidRun的整体哲学是用最少依赖搞定链路移动端部分我们假设已经有现成的UI自动化基础能力比如能启动App、能获取页面结构、能执行点击和手势。这里我们有两种基础工具路线根据自己的规模选方案适用场景优点缺点原生UIAutomator/XCUITest单平台、内部工具链成熟稳定、延迟低跨平台需要各自适配跨端设备农场方案需要统一管理Android/iOS真机一套接口管多端需要维护设备代理层我们自己更多是Android真机加设备代理层因为观察能力和截图获取在Android生态上更容易统一。但角色拆分这套组织和iOS自动化一样可以跑核心不依赖具体工具只依赖三个基础接口获取当前页面结构返回简化后的控件树或关键元素列表。查找元素通过文本、resource-id等属性定位。执行动作模拟点击、滑动、输入。这一步不复杂但要注意别把原始XML直接交给大模型。后面第4章会讲为什么要预处理页面结构。3.2 Planner的实现把测试目标变成任务图Planner的提示词不需要特别复杂但需要输出约束非常明确。我给Planner定义的输出格式是JSON数组每个节点包含step_id、action目标、期望页面三部分。实际提示词核心结构大致如下你是移动端自动化测试的规划器。你只需要根据测试目标生成有序的操作计划不要生成具体屏幕坐标。 约束 1. 计划节点控制在10个以内。 2. 每个节点必须包含预期的页面或业务状态。 3. 如果目标信息不足输出{need_more_info: 具体缺失内容}。 请基于以下测试目标输出JSON计划。 测试目标{user_goal} 已知页面清单{available_screens}跑下来效果很稳关键是第三点约束我以前没加这条Planner会硬编出一个假页面流程直接导致后面的执行全废。加上这一条之后至少它能诚实地反馈信息不足。3.3 Executor的实现动作生成的“有限能力”约束Executor的核心提示词要短。它只需要当前页面结构、Planner给的一个子任务以及动作历史的前3步。我要求它输出的格式是{ actions: [ {type: tap, target: text加入购物车, description: 点击加入购物车按钮} ] }可选动作类型只有tap、swipe、input_text、back、wait。我把动作类型限制得很窄因为动作类型越多模型输出解析出错概率越高。实际跑下来这五个动作足够覆盖绝大多数回归场景。这一层最关键的是“一次只做一步”。不要给Executor一串任务让它连续执行。连续执行时模型经常自作主张跳过一些页面状态检查一步错后面全错。我现在强制它在完成一个动作后立即把控制权交还给编排层由编排层拉取新的页面结构再交给Observer判断然后再决定下一步。3.4 Observer的实现只做事实核查Observer的提示词也不难设计。给它执行前页面结构、执行后页面结构以及执行的动作类型让它输出{ page_changed: true, new_screen: product_detail, key_elements: [ {name: title, value: 某商品标题, exists: true} ] }有一个关键细节Observer不需要知道原始测试目标。如果让Observer知道目标它容易在页面没变化时脑补出“这个页面已经符合预期”的结论这在大模型上特别明显。让它只看前后差异它会老实很多。Observer比较页面结构时我还会把截图一并给到模型。图片对界面改版的感知比XML强得多尤其在页面有大量图片元素时靠控件树很容易漏判。多模态模型做Observer这个角色非常合适。3.5 Reporter的实现与回放循环Reporter是最轻量的角色读取所有结构化中间结果之后输出一段简洁的中文总结。它的输出我不做严格JSON约束反而是格式越自然越好因为主要给人看。不过Reporter还有一项职责容易被忽略它要把失败用例的上下文摘要反馈给Planner。比如“第2步进入详情页失败失败原因页面未发生变化”Planner收到这个摘要后可以调整计划例如尝试从搜索入口进入而不是直接点卡片。这个反馈机制实现了简单的自动重试和多路径切换也是DroidRun能够提升通过率的关键所在。整个回放循环用伪代码表达是这样def run_test_case(goal, device_proxy, llm_clients): plan planner.plan(goal) for step in plan.steps: page_before device_proxy.get_page_structure() actions executor.execute(step, page_before) device_proxy.perform(actions) page_after device_proxy.get_page_structure() observation observer.observe(page_before, page_after, actions) if observation.page_changed is false and step.expects_change: feedback reporter.request_feedback(step, observation) alternative_plan planner.replan(goal, feedback) # 进入备选路径继续执行 ... result reporter.summarize(device_proxy.test_session) return result这套循环跑得通之后后面维护基本就是维护四个提示词和结构化协议比维护两千条脚本轻松太多。4. 常见问题与排查实录4.1 页面控件树太大大模型上下文爆炸移动端页面结构比Web端臃肿得多一个复杂的商品详情页控件树经常有几百个节点原始XML长度能到几万tokens。直接把原始XML丢给Executor轻则响应变慢重则超出窗口长度。我的处理是把页面结构先整理成精简列表找所有可点击、可输入的叶子节点。过滤掉坐标类布局容器、装饰性View。保留控件文本、resource-id、bounds但bounds只保留中心点坐标。按面积排序筛掉屏幕外区域。经过这一步一个复杂页面通常能压到几十个节点上下文压力骤降。实测下来Executor的决策准确率并没有下降因为移动端UI真正可交互的节点本来就少剩下的都是布局噪声。4.2 输出格式不稳定Not JSON问题让人崩溃大模型输出JSON时偶尔会在前面加解释文字偶尔会把双引号转义成反斜杠。我们在角色协议上做了两层防护第一层是提示词约束输出开头必须是某个固定标记。第二层是解析容错去掉开头非JSON内容再对常见错误做修复比如单引号转双引号、去掉尾逗号。下面这串代码是我们实际在用的解析壳import json import re def extract_json(raw_output: str) - dict: text raw_output.strip() # 这里容忍常见的自然语言前缀 start text.find({) end text.rfind(}) if start -1 or end -1: raise ValueError(fno json object found: {raw_output[:200]}) json_str text[start:end 1] # 修复尾逗号常见问题 json_str re.sub(r,\s*}, }, json_str) return json.loads(json_str)执行器除了JSON输出之外最好让模型同时产出一次带格式的动作摘要文本给定位问题用。很多时候解析失败不一定是格式问题而是模型把动作和断言混在同一个步骤里了摘要文本能很快看出来。4.3 设备和App渲染时序导致的“假失败”移动端自动化最经典的坑就是等待。页面跳转有动画列表刷新有延迟Executor执行完点击后立刻获取页面结构很可能拿到的还是旧页面Observer就会输出“页面未变化”整个流程被误判为失败。我针对这个场景加了三个策略测试稳定性提升非常明显等待策略执行动作后强制等待页面静止也就是连续多次获取页面结构都一样才继续。目标校验在点击之前Executor输出目标元素之后编排层先用控件树核验这个元素存在且可见再真正执行点击。二次观察Observer拿到首次结果后如果发现“页面临界状态”会再发起一次主动刷新隔两秒重新观察。这三个策略本质上是在LLM能力外面套了一层工程防护效果立竿见影。4.4 角色越权互相抢活角色级模型拆分之后我还是遇到过一次模型“抢活”现象。Executor在输出动作时偶尔会顺手输出“我已经验证了这个元素的文本”Observer也在输出里写“建议下一步点击返回”Reporter在总结里写“该用例失败建议开发排查”。这些越权输出本身不会破坏流程但会给链路数据造成污染尤其当Reporter开始带个人观点报告就不可信了。解决方法是在每个角色的系统提示词里加一条铁律——禁止输出当前角色职责以外的内容。我原以为大模型能天然理解角色边界实际操作下来不行必须把禁止项写进提示词。比如Executor的提示词末尾一定会写“你只输出动作指令JSON禁止输出观察结论或断言结果。”这条指令加上后越权问题基本绝迹。4.5 失败自动重试和无休止循环有了Planner的重规划能力后另一个问题是模型可能在一个错误路径上反复重试。比如某个页面因为网络加载慢一直打不开Planner会换一个入口但如果所有入口都依赖网络它可能连续换七八条路径浪费时间。我的处理是设置重试次数上限和“失败指纹”。当同一页面连续出现超过两次相同的失败事实时直接终止用例并标记为阻塞不再浪费token。这个规则放在编排层代码里不靠LLM自觉否则不可控。5. 用了一个季度后的一些实测体会5.1 什么时候值得拆什么时候不值得拆必须要说角色级模型拆分不是所有移动端项目都需要。我按我们的经验给了一个参照只需要做冒烟测试、核心路径就三四条、用例量低于五十条用一个大模型Agent就够了拆了反而增加协议设计成本。用例量大、页面频繁改版、需要长期维护的回归套件比如超过一百条核心用例角色拆分收益十分明显。团队里有专门做测试平台开发的人能把四个角色的编排工程化而不是停留在“调提示词”才建议拆。如果只是临时验证可行性一个Agent先跑通更重要。我们的DroidRun项目大概在用例数过三百条后做的拆分拆分前后最直观的变化是失败用例的定位时间从按小时算降到按分钟算。5.2 一些实测后的效率对比我把同一个业务模块的三十条核心回归用例跑了三个版本结果大致是方案通过率平均单用例耗时排查失败所需时间纯录制脚本100%约40秒查找定位失败20分钟以上单一LLM Agent78%约60秒需要看整段日志时间不定DroidRun角色拆分91%约90秒直接看Observer/Raporter结构化结果5分钟内单用例耗时有增加因为多角色串行调用会产生额外消耗但换来的是通过率和可维护性的巨大提升。在持续集成流水线里我更在意全量回归能不能自动跑完、失败能不能快速归类而不是单条用例快几秒。这个取舍大家看自己场景。5.3 后续可以怎么扩展这个角色的协议框架稳定下来以后下一步我能看到的方向有用向量库积累历史失败案例给Observer和Reporter做参考提高断言准确性。把Planner的“任务图”作为数据资产沉淀下来做跨版本的变更影响分析。对Observer的结果做小样本统计自动发现页面变化的波动规律提前规避动画时序相关的假失败。另外我个人特别建议在报告层接入业务历史数据。Reporter如果有历史通过率、历史失败类型的参考它能给出的结论会更有价值而不只是单纯说“这次跑了多少条”。最后再分享一个小技巧也是在DroidRun里一直在用的四个角色的模型版本尽量解耦不要绑定同一个模型供应商。Planner用推理能力强的Executor用响应速度快的便宜模型Observer用视觉能力好的多模态模型Reporter用文本总结能力稳定的。这样拆分以后成本方差更小某个模型升级或抽风的时候其他角色不会跟着遭殃。角色级模型拆分这件事的价值说到底不是把任务分给多个LLM那么简单而是把每一个角色的稳定性做成可单独优化、单独验收的工程模块。它让LLM驱动的移动端自动化终于从“看运气”变成了“看工程”。