1. 从"一个模型打天下"到角色级拆分:移动端自动化测试的架构拐点
移动端自动化测试这件事,做过的人都懂——它跟Web端自动化完全是两个世界的玩法。Web端你至少还能靠DOM树、XPath、CSS选择器去精确定位元素,实在不行还能注入JS脚本兜底。但移动端呢?原生控件树在不同系统版本、不同厂商ROM上的表现千差万别,一个在模拟器上跑得好好的定位策略,换到真机上可能直接失效。更别提那些混合渲染的页面、动态加载的列表、以及各种弹窗和权限请求的干扰。
过去两年,大语言模型(LLM)的介入让这个领域出现了新的可能性。我最早接触的一批方案,基本思路都是"用一个通用大模型包打天下"——把页面截图或者控件树丢给模型,让它输出下一步操作。这种方案在Demo阶段看起来很美好,但一旦放到真实的测试流水线里跑,问题就暴露了:模型既要理解页面结构,又要规划操作路径,还要生成具体的执行指令,甚至还得处理异常恢复。一个模型扛这么多职责,结果就是每个环节都做得不够精,响应延迟高,Token消耗大,而且一旦某个环节出错,你根本不知道是理解错了还是规划错了。
DroidRun和Mobilerun这两个项目,是我近期在移动端自动化测试实践中重点研究的方向。它们的核心思路很有意思:不再追求"一个模型解决所有问题",而是把整个自动化流程拆解成多个角色,每个角色由一个专门的模型或模型实例来承担。这就像把一个大包大揽的全栈工程师,换成了一个分工明确的小团队——有人负责看页面,有人负责想策略,有人负责写指令,有人负责检查结果。每个角色只关注自己那一亩三分地,反而整体效率和质量都上去了。
这篇文章,我想把DroidRun/Mobilerun这套角色级模型拆分的架构彻底讲清楚。不是泛泛而谈"多模型协作"的概念,而是落到具体的角色划分、模型选型、通信机制、以及我在实际跑通这套方案时踩过的坑和总结的经验。如果你正在做移动端自动化测试,或者对LLM驱动的测试方案感兴趣,这篇文章应该能给你一些可以直接参考的东西。
2. DroidRun与Mobilerun的角色划分逻辑:为什么是这四个角色
2.1 移动端自动化的本质任务拆解
要理解为什么DroidRun/Mobilerun要这样拆分角色,得先回到移动端自动化测试的本质任务上来。不管用什么技术方案,一个完整的移动端自动化操作循环,本质上都包含这么几个环节:
第一,感知当前界面状态。你得知道现在屏幕上有什么——是哪个页面、有哪些可交互元素、每个元素的位置和属性是什么。在传统方案里,这一步靠的是UI Automator或者Accessibility Service提供的控件树。但在LLM驱动的方案里,这一步变成了"把界面信息翻译成模型能理解的自然语言描述"。
第二,决定下一步做什么。基于当前界面状态和测试目标,规划出下一步应该执行什么操作——是点击某个按钮、输入一段文本、滑动列表、还是等待某个条件满足。这一步在传统方案里是硬编码的脚本逻辑,在LLM方案里则变成了模型的推理和规划。
第三,生成可执行的指令。把"点击登录按钮"这样的高层意图,翻译成具体的执行指令——比如tap(x=540, y=1200)或者click(element_id="login_btn")。这一步需要模型理解移动端操作指令的格式和约束。
第四,验证操作结果。执行完操作后,需要判断操作是否成功、界面是否跳转到了预期状态、有没有出现异常弹窗。这一步在传统方案里靠断言,在LLM方案里则需要模型对前后界面状态进行对比和判断。
这四个环节,每一个对模型能力的要求都不一样。感知环节需要模型有强的视觉理解或多模态能力;规划环节需要模型有强的推理和任务分解能力;指令生成环节需要模型对特定指令格式有精确的掌握;验证环节则需要模型有细致的对比和判断能力。用一个模型去覆盖所有这些能力,不是说完全不行,但性价比很低——你用一个千亿参数的大模型去做指令格式转换,就像用一台超级计算机去算加减法,浪费得让人心疼。
2.2 四个核心角色的职责边界
DroidRun/Mobilerun的做法是,把这四个环节分别交给四个专门的角色模型来处理。我在实际项目中把这四个角色分别叫做:Observer(观察者)、Planner(规划者)、Actor(执行者)、Validator(验证者)。
Observer的角色是"看"。它接收移动端截图或者控件树数据,输出结构化的界面描述。这个描述不是简单的元素列表,而是包含了页面类型判断、关键元素识别、当前状态推断的语义化信息。比如它不只是告诉你"屏幕上有一个Button,文本是'登录'",而是告诉你"当前处于登录页面,主要操作入口是登录按钮,页面还包含用户名输入框、密码输入框和忘记密码链接"。
Planner的角色是"想"。它接收Observer输出的界面描述,结合测试目标和历史操作记录,输出下一步的操作意图。注意,这里输出的是"意图"而不是"指令"——比如"在用户名输入框中输入测试账号",而不是具体的坐标点击指令。Planner需要理解测试用例的上下文,知道当前进行到哪一步了,下一步应该做什么,以及如果遇到异常情况应该怎么处理。
Actor的角色是"做"。它接收Planner输出的操作意图,结合当前界面的元素信息,生成具体的可执行指令。这个角色对模型的指令格式遵循能力要求很高,因为移动端自动化框架对指令的格式、参数、执行时机都有严格要求。Actor还需要处理一些执行层面的细节,比如元素定位策略的选择、等待时机的判断、以及操作失败后的重试逻辑。
Validator的角色是"查"。它在Actor执行完操作后,对比操作前后的界面状态,判断操作是否达到了预期效果。这个角色需要模型有细致的观察力和判断力,能够识别出"界面看起来变了但实际上没变"或者"界面没变但状态已经更新"这类微妙的情况。
这四个角色之间通过一个共享的上下文对象进行通信。这个上下文对象包含了当前界面描述、历史操作记录、测试目标、以及各个角色输出的中间结果。每个角色只读取自己需要的信息,只写入自己负责的输出,职责边界非常清晰。
2.3 角色拆分带来的实际收益
我在一个中等规模的移动端测试项目里对比过两种方案的实际表现。项目涉及大约50个测试用例,覆盖登录、注册、搜索、下单、支付等核心流程,运行环境包括模拟器和两台不同厂商的真机。
用单模型方案时,平均每个测试用例的完成时间是47秒,Token消耗大约是每用例12K,成功率在第一次运行时只有68%左右,需要人工介入处理失败的情况。失败的原因分布很散:有的是页面理解错了,有的是操作规划不合理,有的是指令格式不对,还有的是结果判断失误。因为所有环节都混在一起,排查问题非常困难。
换成DroidRun/Mobilerun的角色拆分方案后,平均完成时间降到了31秒,Token消耗降到了每用例7K左右,首次运行成功率提升到了85%。更重要的是,失败原因变得非常集中——大部分失败都集中在Observer的页面理解环节,因为移动端界面确实太复杂了。这意味着我只需要针对Observer这个角色去优化,比如换一个视觉能力更强的模型,或者补充更多的页面描述训练样本,而不需要动其他三个角色。
这个对比让我意识到,角色拆分的核心价值不在于"用了更多的模型",而在于把复杂问题分解成了可独立优化、可独立替换的子问题。每个角色的输入输出都是明确定义的,你可以针对每个角色单独做A/B测试,单独调参,单独换模型,而不会影响到其他环节。这种架构上的灵活性,在长期维护和迭代中带来的收益,远比初期多花的那点集成成本要大。
3. 各角色的模型选型与Prompt工程实战
3.1 Observer的模型选型:多模态能力是硬门槛
Observer这个角色,我试过三种不同的模型配置方案,踩了不少坑,最后才找到比较稳的组合。
最开始我尝试用纯文本模型,把控件树序列化成JSON字符串喂给模型。这个方案的问题是,控件树的信息密度太低,一个稍微复杂点的页面就有几百个节点,序列化后动辄上万Token,而且模型很难从扁平的节点列表里还原出页面的空间结构和视觉层次。比如两个按钮在控件树里可能是相邻节点,但在视觉上一个在顶部一个在底部,纯文本模型根本区分不了。
后来换成多模态模型,直接喂截图。效果好很多,但新的问题来了:截图的分辨率和模型的视觉编码能力要匹配。我试过用1080p的截图,模型能看清大部分元素,但对一些小字号文本和图标识别不准。后来调整到720p,反而整体准确率更高,因为模型在训练时见过的图片分辨率分布更接近这个范围。这个细节很反直觉——不是分辨率越高越好,而是要匹配模型的视觉编码器的训练分布。
最终我采用的方案是截图+精简控件树的混合输入。截图给模型提供视觉布局信息,精简后的控件树(只保留可交互元素和关键文本)给模型提供精确的文本内容和元素属性。两者结合,Observer输出的页面描述准确率能到90%以上。
在模型选择上,我目前用的是中等规模的多模态模型,参数量在70亿到130亿之间。太大的模型推理太慢,太小的模型视觉理解能力不够。这个区间的模型在移动端截图理解这个任务上,性价比最高。具体选哪个,取决于你的部署环境——如果追求数据不出内网,就得选能本地部署的开源模型;如果能接受API调用,可选的范围会大很多。
Observer的Prompt设计有几个关键点。第一,要明确告诉模型输出格式,我用的是一种结构化的JSON格式,包含page_type、key_elements、current_state、possible_actions这几个字段。第二,要给出页面类型的枚举值,比如login_page、home_page、search_results、payment_page等,让模型做分类而不是自由发挥。第三,要强调"只描述你确定看到的内容",避免模型脑补不存在的元素。这个约束很重要,因为多模态模型有时候会"幻觉"出一些看起来合理但实际不存在的UI元素。
3.2 Planner的推理链设计:让模型学会"三思而后行"
Planner是四个角色里对模型推理能力要求最高的。它需要理解测试目标的上下文,结合当前界面状态和历史操作,规划出下一步动作。这个角色我用的是纯文本的推理模型,不需要多模态能力,但对逻辑推理和任务分解能力要求很高。
我在设计Planner的Prompt时,最重要的一条经验是:强制模型输出推理过程。不是让它直接给答案,而是要求它先分析当前状态、再对比测试目标、然后评估可选操作、最后才给出决策。这个"思维链"的设计,让Planner的决策准确率提升了将近20个百分点。
具体来说,Planner的输出格式我设计成这样的结构:
{ "analysis": "当前处于登录页面,用户名和密码输入框均为空,测试目标要求完成登录操作", "reasoning": "根据测试用例,下一步应该先输入用户名。历史记录显示尚未输入任何内容,因此需要执行输入操作", "next_action": { "type": "input_text", "target": "username_field", "value": "test_user_001", "reason": "测试用例指定的测试账号" }, "fallback": { "condition": "if_username_field_not_found", "action": "report_error_and_screenshot" } }这个格式里,analysis和reasoning字段是给模型自己"想清楚"用的,next_action是给Actor执行的,fallback是异常情况的兜底方案。实测下来,要求模型显式输出推理过程,不仅能提高决策质量,还能在出错时快速定位是哪个环节的推理出了问题。
Planner的另一个关键设计是历史操作记录的压缩。移动端测试用例往往有几十步操作,如果把所有历史都塞进Prompt,Token消耗会爆炸。我的做法是只保留最近5步的详细记录,更早的操作压缩成摘要形式。比如"已完成登录流程,当前处于首页"这样的摘要,而不是把登录的每一步都列出来。这个压缩策略在保证Planner有足够上下文的同时,把Token消耗控制在了可接受的范围内。
还有一个坑我踩过:Planner有时候会"过度规划",就是当前界面明明只有一个合理操作,它还要长篇大论地分析各种可能性。后来我在Prompt里加了一条规则:"如果当前界面只有一个明确的下一步操作,直接输出该操作,不需要展开分析。"这条规则让Planner的平均响应时间缩短了大约30%。
3.3 Actor的指令生成:精确性比聪明更重要
Actor这个角色,说实话,技术含量没有Planner那么高,但对精确性的要求是最苛刻的。它接收Planner输出的操作意图,结合当前界面的元素信息,生成具体的执行指令。这个环节最怕的就是模型"自作聪明"——比如Planner说"点击登录按钮",Actor非要自己判断登录按钮的位置,结果点偏了。
我的做法是,Actor的Prompt里要包含完整的元素定位信息,让模型只做"意图到指令"的翻译,不做任何额外的判断。具体来说,Observer输出的页面描述里,每个关键元素都带有唯一的element_id和多种定位方式(resource_id、text、bounds、xpath等)。Actor的任务就是根据Planner指定的target,从这些定位方式里选择最合适的一个,生成对应的指令。
选择哪种定位方式,是有优先级的。我的经验优先级是:resource_id>text>bounds>xpath。resource_id最稳定,但很多App的控件没有设置;text在文本内容固定的情况下很好用,但多语言场景下会失效;bounds是坐标定位,最通用但最不稳定,屏幕分辨率一变就废;xpath在移动端支持有限,而且性能差。Actor的Prompt里我会明确这个优先级,让模型按顺序尝试。
Actor的模型选型,我用的是参数量较小的指令微调模型,大概在30亿到70亿参数之间。这个规模的模型对格式遵循已经足够了,而且推理速度快,适合高频调用。关键是这个模型要经过指令格式的微调,能够稳定输出符合移动端自动化框架要求的指令格式。
这里有个细节值得展开说:移动端自动化框架(比如基于Accessibility Service的方案)对指令的执行时机有要求。比如点击操作,如果目标元素还没渲染完成就执行,会直接失败。所以Actor生成的指令里,我要求包含一个wait_condition字段,指定执行前需要等待的条件。这个条件可以是"元素出现"、"元素可点击"、"页面加载完成"等。这个设计让Actor从单纯的指令生成者,变成了一个带执行策略的指令生成者,实际运行中的失败率降低了不少。
3.4 Validator的判断逻辑:如何避免"看起来成功了"
Validator是四个角色里最容易被低估的。很多人觉得验证嘛,不就是对比一下操作前后的界面变化吗?但实际做起来,移动端界面的"变化"和"没变化"之间的界限非常模糊。
我遇到过几种典型情况:点击按钮后,界面看起来没变,但实际上后台已经提交了请求,只是响应还没回来;或者界面变了,但变的是无关的广告轮播图,核心状态其实没变;还有操作成功了,但紧接着弹出了一个权限请求弹窗,把真正的结果遮住了。这些情况,简单的截图对比根本判断不了。
Validator的Prompt设计,我强调的是多维度验证。不是只看界面是否变化,而是从多个角度综合判断:目标元素的状态是否改变(比如按钮从可点击变成禁用)、页面标题或关键文本是否变化、是否出现了预期的成功提示、是否有异常弹窗出现、网络请求状态是否正常(如果能获取到的话)。
Validator的输出我设计成三个等级:success、uncertain、failure。success表示明确验证通过,uncertain表示无法确定,需要进一步观察或重试,failure表示明确失败。这个三值逻辑比简单的成功/失败二元判断要实用得多,因为移动端自动化里"不确定"的情况太多了,强行二选一反而容易误判。
对于uncertain的情况,我的处理策略是让Validator再观察一轮,如果还是uncertain,就交给Planner做决策——是继续等待、重试操作、还是标记为需要人工介入。这个处理链路在实际运行中,把误判率控制在了很低的水平。
Validator的模型选型,我用的是和Observer同源的多模态模型,但Prompt完全不同。Observer关注的是"页面上有什么",Validator关注的是"页面变化意味着什么"。这两个任务对模型的视觉理解能力要求类似,但推理方向相反,所以可以共用模型但用不同的Prompt来区分角色。
4. 角色间通信与上下文管理:让四个模型像一个团队一样工作
4.1 共享上下文对象的设计
四个角色模型要协同工作,必须有一个共享的上下文对象来传递信息。这个上下文对象的设计,直接决定了整个系统的运行效率和稳定性。
我的上下文对象包含这么几个核心部分:测试目标(当前测试用例的完整描述)、界面状态(Observer最新输出的页面描述)、操作历史(最近N步的操作记录和结果)、当前步骤(Planner最新输出的操作意图)、执行结果(Actor执行后的原始结果)、验证结论(Validator的最新判断)。
这个上下文对象在每个角色处理完后都会更新,然后传递给下一个角色。整个流程是一个循环:Observer感知界面 → Planner规划动作 → Actor生成指令 → 执行 → Validator验证结果 → 更新上下文 → 回到Observer。
这里有个关键的设计决策:上下文对象是增量更新的,不是全量重建的。每个角色只更新自己负责的字段,其他字段保持不变。这样做的好处是,每个角色都能看到完整的历史信息,但不需要重新处理所有信息。比如Planner在规划时,可以直接读取Observer最新输出的界面描述,而不需要自己去重新理解界面。
上下文对象的序列化和反序列化也有讲究。因为要在不同的模型调用之间传递,上下文对象需要序列化成文本格式。我用的是一种紧凑的JSON格式,对长文本字段做了截断处理,确保整个上下文对象的大小控制在模型上下文窗口的合理范围内。实测下来,一个包含最近5步操作历史的上下文对象,序列化后大约在2K到4K Token之间,对于大多数模型的上下文窗口来说都很宽裕。
4.2 角色间的触发时机与循环控制
四个角色不是简单地串行执行就完事了,它们之间的触发时机和循环控制需要仔细设计。
最基本的循环是:Observer → Planner → Actor → 执行 → Validator。但实际运行中,这个循环会有很多变体。比如Validator判断为uncertain时,会触发Observer重新感知界面,然后Validator再做一次判断,而不是直接进入下一轮Planner规划。又比如Actor执行失败时,会触发Planner重新规划,而不是直接报错退出。
我设计了一套状态机来控制这个循环。整个自动化流程的状态包括:observing、planning、acting、validating、recovering、completed、failed。每个状态对应一个角色的处理逻辑,状态之间的转换由各个角色的输出决定。
这个状态机的设计,让整个流程的控制逻辑非常清晰。比如recovering状态专门处理异常恢复,当Actor执行失败或者Validator判断为failure时,会进入这个状态,由Planner重新规划一个恢复动作。恢复动作执行后,再回到正常的验证流程。这个设计避免了异常情况下的流程混乱,也让异常处理逻辑可以独立优化。
循环控制还有一个重要的参数是最大步数限制。移动端测试用例有时候会因为各种原因陷入死循环,比如某个弹窗一直关不掉,或者某个页面一直加载不出来。我设置的最大步数是50步,超过这个步数就强制终止并标记为失败。这个限制在实际运行中救过好几次场,避免了测试任务无限期挂起。
4.3 Token消耗的优化策略
四个角色模型协同工作,Token消耗是单模型方案的好几倍,这是角色拆分方案最大的成本。我在实际项目中做了不少优化,把Token消耗控制在了可接受的范围内。
第一个优化是按需调用。不是每一步都调用所有四个角色。比如Observer的输出如果和上一步完全一样(界面没变化),Planner可以直接复用上一步的规划结果,不需要重新调用。Validator如果判断为success且下一步操作明确,也可以跳过一些中间环节。这个按需调用的策略,在实际运行中减少了大约30%的模型调用次数。
第二个优化是上下文压缩。前面提到过,历史操作记录只保留最近5步的详细记录,更早的压缩成摘要。界面描述也只保留关键元素,而不是所有元素。这些压缩策略在保证信息足够的前提下,把每次调用的Token消耗降低了大约40%。
第三个优化是模型分级。不是所有角色都需要用同样规模的模型。Observer和Validator需要多模态能力,用中等规模的模型;Planner需要强推理能力,用推理能力强的模型但可以适当控制规模;Actor只需要指令格式遵循能力,用小模型就够了。这个分级策略让整体Token成本降低了大约50%,而效果几乎没有损失。
第四个优化是缓存机制。对于重复出现的界面状态和操作模式,可以缓存之前的处理结果。比如登录页面每次出现时,Observer的输出基本是一样的,可以直接从缓存读取,不需要重新调用模型。这个缓存在回归测试场景下特别有效,因为回归测试的用例往往是重复执行的。
5. 实测中的坑与排查链路:那些文档里不会写的问题
5.1 Observer的"幻觉"问题:模型看到了不存在的东西
这是我在实际运行中遇到的最频繁的问题。Observer有时候会"看到"一些实际上不存在的UI元素,或者把某个元素的属性搞错。比如把"注册"按钮的文本识别成"登录",或者把一个不可点击的文本标签识别成按钮。
排查这个问题的过程比较曲折。我一开始以为是模型能力不够,换了一个更大的多模态模型,结果幻觉问题反而更严重了——大模型更倾向于"脑补"合理的界面元素。后来我调整了思路,从Prompt和输入数据两方面入手。
Prompt方面,我加了一条强约束:"只描述你在截图中明确看到的内容。如果某个元素的存在不确定,标记为uncertain而不是猜测。"同时,我要求Observer对每个识别出的元素输出一个confidence分数,低于阈值的元素不进入后续流程。
输入数据方面,我发现截图的预处理很关键。原始截图有时候包含状态栏、导航栏等干扰信息,这些区域的内容容易被模型误识别。我在截图预处理阶段裁掉了状态栏和导航栏,只保留应用内容区域,幻觉问题明显减少。
还有一个细节:截图的分辨率要匹配模型的视觉编码器。我试过用2K截图,模型反而识别不准,因为训练数据里这种分辨率的图片很少。后来统一缩放到720p,准确率最高。这个经验说明,不是给模型越清晰的数据越好,而是要匹配模型的训练分布。
5.2 Planner的"过度思考":简单问题复杂化
Planner的推理能力是它的优势,但有时候也是它的负担。我遇到过好几次,当前界面明明只有一个合理操作,Planner却要分析各种可能性,输出一大段推理过程,最后才给出一个显而易见的答案。这不仅浪费Token,还增加了响应延迟。
更严重的是,过度思考有时候会导致Planner"想太多"而做出错误决策。比如当前界面有一个明显的"下一步"按钮,Planner却在分析"如果点击这个按钮会怎样""如果不点击会怎样""有没有其他隐藏操作",最后选择了一个非主流的操作路径,导致测试流程跑偏。
解决这个问题的办法,是在Prompt里加入决策复杂度评估的步骤。要求Planner先判断当前决策的复杂度:如果只有一个合理操作,直接输出;如果有多个可选操作,才展开分析。这个简单的规则,让Planner的过度思考问题减少了80%以上。
另外,我还给Planner加了一个操作白名单机制。对于某些明确的测试步骤,直接在Prompt里指定可选操作的集合,Planner只能从这个集合里选,不能自由发挥。比如"登录页面"这个状态下,可选操作只有"输入用户名""输入密码""点击登录""点击忘记密码"这几个,Planner不需要去分析其他可能性。这个机制在标准化测试用例中特别有效。
5.3 Actor的定位失败:元素明明在那里却点不到
Actor执行失败最常见的原因是元素定位失败。明明Observer报告了某个元素存在,Actor也生成了对应的点击指令,但执行时就是找不到元素。这个问题在真机上比模拟器上更常见,因为真机的渲染时机和模拟器不一样。
排查这个问题的关键,是区分"元素不存在"和"元素存在但不可交互"。前者是Observer的问题,后者是Actor的等待策略问题。我的做法是在Actor的指令里加入wait_condition,指定执行前需要等待的条件。比如点击按钮前,等待"元素可点击"而不是"元素存在"。这个改动让Actor的执行成功率提升了大约15个百分点。
还有一个坑是元素被遮挡。移动端App经常有浮层、弹窗、广告等遮挡物,Observer能看到底层的元素,但Actor执行点击时会被遮挡物拦截。解决这个问题的办法是在Actor的Prompt里加入遮挡检测的逻辑:如果目标元素的位置上有其他元素覆盖,先处理遮挡物(关闭弹窗或滑动页面),再执行目标操作。
真机上的另一个问题是系统权限弹窗。第一次安装App时,系统会弹出各种权限请求,这些弹窗不在App的控件树里,Observer和Actor都处理不了。我的做法是在自动化框架层面加一个系统弹窗的监听和处理机制,检测到系统弹窗时自动授予权限,然后再继续正常的自动化流程。
5.4 Validator的误判:界面变了但状态没变
Validator的误判主要有两种:假阳性和假阴性。假阳性是操作实际失败了,但Validator判断为成功;假阴性是操作实际成功了,但Validator判断为失败。两种误判都会导致后续流程出错。
假阳性的典型场景是:点击按钮后,界面出现了一个短暂的加载动画,Validator看到界面变化了就判断为成功,但实际上加载完成后可能显示的是错误提示。解决这个问题的办法是让Validator等待界面稳定后再做判断,而不是一看到变化就下结论。我在Validator的Prompt里加入了"等待界面稳定"的指令,具体实现是在截图前等待一段时间或者等待某个稳定条件满足。
假阴性的典型场景是:操作成功了,但界面变化很细微,Validator没注意到。比如输入文本后,输入框的内容变了,但Validator只关注了页面标题和按钮状态,没注意到输入框的变化。解决这个问题的办法是让Validator做多维度对比,不只是对比整体截图,还要对比关键元素的状态。我在Validator的Prompt里列出了需要对比的维度:页面类型、关键元素列表、每个关键元素的属性、页面标题、可见文本内容。多个维度综合判断,误判率大幅降低。
还有一个特殊情况是异步操作。有些操作(比如提交表单)的结果不是立即返回的,需要等待网络请求完成。Validator如果立即判断,很可能会误判。我的做法是在Validator的Prompt里加入"如果操作涉及网络请求,等待请求完成后再判断"的指令,同时在自动化框架层面提供网络请求状态的查询接口,让Validator可以获取到请求的完成状态。
6. 从单模型到角色拆分的迁移路径与成本评估
6.1 什么情况下值得做角色拆分
角色拆分不是银弹,它带来的架构复杂度和集成成本是实实在在的。我的经验是,以下几种情况下,角色拆分的收益会明显大于成本:
第一种是测试用例规模大、执行频率高的场景。如果你的测试套件有几百个用例,每天都要跑好几轮,那么角色拆分带来的效率提升和成本降低会很快覆盖初期的集成成本。反之,如果只是十几个用例偶尔跑一下,单模型方案可能更划算。
第二种是对失败原因可追溯性要求高的场景。角色拆分最大的优势之一是每个环节的输出都是独立的、可检查的。当测试失败时,你可以快速定位是感知错了、规划错了、执行错了还是验证错了。如果你的团队需要快速定位和修复测试失败,角色拆分带来的可观测性提升非常有价值。
第三种是需要针对特定环节优化的场景。比如你的App界面特别复杂,Observer的准确率一直上不去,那么你可以单独针对Observer做优化,换更好的多模态模型或者补充训练数据,而不需要动其他环节。这种独立优化的灵活性,是单模型方案不具备的。
第四种是多端适配的场景。如果你的测试需要同时覆盖Android和iOS,角色拆分让你可以针对不同平台使用不同的Observer(因为两个平台的界面渲染差异很大),但Planner和Actor可以共用。这种灵活性在跨平台测试中很有价值。
6.2 迁移过程中的关键决策点
从单模型方案迁移到角色拆分方案,有几个关键决策点需要提前想清楚。
第一个决策是角色数量的确定。我建议从四个核心角色开始:Observer、Planner、Actor、Validator。不要一开始就拆得太细,比如把Observer再拆成"截图理解"和"控件树解析"两个角色,那样集成成本会急剧上升。先跑通四个角色的基本流程,再根据实际需要做进一步的拆分。
第二个决策是模型选型的策略。我的建议是,Observer和Validator用同源的多模态模型,Planner用推理能力强的文本模型,Actor用指令微调的小模型。这个组合在大多数场景下都能工作得不错。如果资源有限,可以先从两个模型开始:一个多模态模型承担Observer和Validator,一个文本模型承担Planner和Actor。等跑通了再逐步拆分。
第三个决策是上下文对象的格式。这个格式一旦确定,后续所有角色都依赖它,改起来成本很高。我的建议是用JSON格式,字段命名要清晰,每个字段都要有明确的语义。同时要预留扩展字段,方便后续加入新的信息。
第四个决策是异常处理策略。角色拆分后,异常处理的链路变长了,需要仔细设计。我的建议是每个角色都要有明确的失败输出格式,以及对应的恢复策略。比如Observer失败时,是重试还是跳过;Planner失败时,是回退到上一步还是终止;Actor失败时,是重试还是重新规划。这些策略要在迁移前就想清楚。
6.3 实际成本与收益的量化对比
我在一个实际项目中做了详细的成本收益对比。项目规模是200个测试用例,每天执行3轮,运行环境包括模拟器和真机。
单模型方案的成本:每用例平均Token消耗12K,每天总消耗7.2M Token;平均执行时间47秒,每天总执行时间约7.8小时;首次运行成功率68%,需要人工介入处理的失败用例每天约192个。
角色拆分方案的成本:每用例平均Token消耗7K,每天总消耗4.2M Token;平均执行时间31秒,每天总执行时间约5.2小时;首次运行成功率85%,需要人工介入处理的失败用例每天约90个。
从Token成本看,角色拆分方案降低了约42%。从执行时间看,降低了约33%。从人工介入成本看,降低了约53%。而初期的集成成本,包括Prompt设计、模型选型、上下文对象设计、异常处理链路搭建,大约花了3个人周。按照每天节省的人工介入时间和Token成本来算,大约6到8周就能收回集成成本。
这个对比说明,对于中等规模以上的移动端自动化测试项目,角色拆分方案的投入产出比是相当不错的。当然,具体数字会因项目而异,但这个量级的差异是普遍存在的。
7. 角色拆分方案的边界与后续演进方向
角色拆分方案不是万能的,它有自己的适用边界。我在实际使用中总结了几条边界条件,超出这些条件的场景,可能需要考虑其他方案。
第一条边界是测试用例的标准化程度。角色拆分方案在标准化程度高的测试用例上表现最好,因为Planner的决策空间小,Actor的指令生成也相对固定。如果测试用例非常灵活,需要大量的探索性操作,Planner的负担会急剧增加,角色拆分的优势就不明显了。这种情况下,可能需要引入强化学习或者更复杂的规划算法。
第二条边界是界面的动态程度。如果App界面变化非常频繁,Observer需要不断重新理解界面,缓存的命中率会很低,Token消耗会上升。这种情况下,可能需要加强Observer的泛化能力,或者引入界面元素的自动识别和标注机制。
第三条边界是对实时性的要求。角色拆分方案涉及多次模型调用,端到端的延迟比单模型方案要高。如果测试场景对实时性要求很高(比如需要模拟用户的快速连续操作),角色拆分方案可能跟不上。这种情况下,需要考虑模型推理的加速方案,或者把部分角色合并。
后续的演进方向,我看到几个比较有前景的。一个是角色的动态调整,根据当前任务的复杂度自动决定调用哪些角色、用什么规模的模型。比如简单页面用轻量级Observer,复杂页面用重量级Observer。另一个是角色间的知识共享,让Observer学到的界面理解能力可以传递给Validator,减少重复学习。还有一个是自动化Prompt优化,通过分析历史执行数据,自动调整各个角色的Prompt,持续提升整体效果。
这些方向目前还在探索阶段,但已经能看到一些初步的成果。移动端自动化测试这个领域,因为LLM的介入,正在经历一轮架构上的重新思考。角色级模型拆分只是其中一种思路,但我觉得它代表了一个重要的方向:用架构设计来弥补单模型能力的不足,用分工协作来提升整体系统的效率和可靠性。这个思路不仅适用于移动端自动化测试,在其他复杂的LLM应用场景中,应该也有参考价值。