☰
移动端自动化测试架构拐点:LLM角色级模型拆分实战
2026/10/20 8:21:30 网站建设 项目流程

1. 从“一个模型打天下”到角色级拆分:移动端自动化测试的架构拐点

移动端自动化测试这件事,做过的人都懂,最磨人的不是写用例,而是维护用例。UI 一改,定位器全废;弹窗一冒,流程全断;不同机型分辨率一换,坐标全飘。前几年大家靠 Appium、UiAutomator、XCUITest 这些框架硬扛,后来发现扛不住,因为移动端的“不确定性”实在太多——系统权限弹窗、网络抖动、广告插屏、输入法遮挡,每一个都能让一条稳定的用例变成随机失败的“薛定谔测试”。

大语言模型进来之后,情况开始变了。LLM 能理解界面语义,能根据自然语言描述去推断该点哪里、该输入什么,甚至能在定位器失效时“猜”出一个合理的替代路径。但很快,新的问题出现了:如果你把“理解界面”“规划步骤”“执行操作”“校验结果”全部塞给同一个模型,它会开始精神分裂。规划的时候它在想“我要完成登录”,执行的时候它又在想“这个按钮的坐标是多少”,校验的时候它还要判断“登录成功没成功”——上下文互相污染,token 消耗爆炸,准确率断崖式下跌。

这就是 DroidRun / Mobilerun 这类项目要解决的核心问题:角色级模型拆分。不是用一个万能模型干所有事,而是把移动端自动化测试的完整链路拆成若干个明确的角色,每个角色配一个专门优化过的模型或提示词策略,各司其职、各管一段。听起来像是微服务架构的思路搬到了 LLM 应用层,但实际做下来,里面的门道比想象中多得多。

这篇文章面向的是已经对 LLM 驱动测试有基本概念、正在或准备落地移动端自动化方案的测试开发、AI 应用工程师和技术负责人。我会把 DroidRun / Mobilerun 这套角色拆分的设计逻辑、每个角色的职责边界、模型选型依据、提示词工程细节、以及实际跑起来之后踩过的坑,全部摊开讲清楚。不是概念科普,是能直接拿去对照自己项目改的那种。

2. 为什么移动端自动化必须做角色拆分:三个绕不过去的现实约束

2.1 上下文窗口不是无限大,移动端界面信息却极其冗余

移动端和 Web 端最大的区别在于:Web 端你可以拿到相对干净的 DOM 树,节点数量可控,属性结构化程度高。移动端呢?一个 Android 的 accessibility tree 动辄几百上千个节点,每个节点带着 bounds、class、resource-id、text、content-desc 一堆属性。你把这些全塞给一个模型,光界面描述就能吃掉几千 token。再加上历史操作记录、当前任务目标、校验规则,上下文窗口瞬间见底。

我实测过一个中等复杂度的电商 App 首页,accessibility tree 完整序列化之后大约 12000 个字符,换算成 token 差不多 4000 到 6000。如果模型还要同时负责规划、执行、校验,它需要在每一轮都重新理解这 6000 token 的界面,再加上之前的操作历史,很容易就冲到 16K 甚至 32K 的上下文上限。一旦触发截断,模型就开始“失忆”,前面做过什么全忘了,重复点击、死循环就来了。

角色拆分之后,每个角色只看自己需要的那部分信息。规划角色只看任务目标和当前页面摘要,执行角色只看具体要操作的那个区域,校验角色只看操作前后的关键差异。上下文压力直接降一个数量级。

2.2 不同角色的“思维模式”天然冲突

这一点是我觉得最关键的。规划和执行,本质上是两种完全不同的认知任务。

规划是慢思考:需要全局视野,需要权衡多条路径,需要预判几步之后的情况。它适合用推理能力强的模型,temperature 可以稍微高一点,允许它探索不同方案。

执行是快思考:给定一个明确目标(比如“点击登录按钮”),它只需要在当前界面里找到最匹配的元素并执行。它需要的是精准、快速、低延迟,temperature 必须压到接近 0,不能有任何创造性。

校验又是另一种模式:它需要对比预期和实际,判断是否一致,有点像单元测试里的 assert。它需要的是严谨的布尔判断能力,而不是生成能力。

你把这三种模式塞进一个模型,它就会在“该探索的时候保守、该精准的时候发散”之间反复横跳。表现出来就是:规划时给出模糊指令,执行时点错元素,校验时把失败判成成功。角色拆分本质上是让每个模型只做自己最擅长的那一种思考。

2.3 成本与延迟的硬账:一个模型扛所有事,钱包扛不住

假设你用 GPT-4 级别的模型做全流程,每步操作平均消耗 8000 token 输入、500 token 输出。一条 20 步的测试用例,光 token 成本就是 20 × 8500 = 170000 token。按输入 $10 / 1M token、输出 $30 / 1M token 算,一条用例大约 $1.7。如果你有 500 条回归用例,跑一轮就是 $850。每天跑一次,一个月 $25500。这个账没人算得过来。

角色拆分之后,规划角色可以用强模型但调用次数少(一条用例可能只调 3 到 5 次),执行角色用轻量模型但调用次数多(每步都调),校验角色用中等模型。综合下来,成本能压到原来的 1/5 到 1/10。延迟方面,执行角色用轻量模型后,单步响应能从 3 到 5 秒降到 1 秒以内,整体用例执行时间大幅缩短。

3. DroidRun / Mobilerun 的角色划分逻辑与职责边界

3.1 四个核心角色:Planner、Locator、Actor、Validator

DroidRun / Mobilerun 这套体系里,我把它归纳为四个核心角色。不同项目的命名可能略有差异,但职责划分基本一致。

Planner(规划者):输入是测试用例的自然语言描述和当前整体进度,输出是下一步要做什么的高层指令。比如“在当前页面找到登录入口并点击”。它不关心具体点哪个坐标,只关心任务分解和顺序。

Locator(定位者):输入是 Planner 给出的高层指令和当前界面的结构化描述,输出是具体的目标元素标识。比如“目标元素是 resource-id 为 com.example.app:id/login_btn 的按钮”。它负责把自然语言映射到界面元素。

Actor(执行者):输入是 Locator 给出的元素标识和操作类型,输出是具体的操作指令(点击、输入、滑动等)。它负责调用底层自动化框架完成实际操作。

Validator(校验者):输入是操作前后的界面状态和预期结果,输出是成功或失败的判断,以及失败原因。它负责确认每一步是否达到了预期效果。

这四个角色串起来,就是一条完整的“思考-定位-执行-验证”链路。每个角色只做一件事,只关注自己需要的信息。

3.2 角色之间的信息传递:摘要而非全量

角色拆分最容易踩的坑,就是角色之间传递了过多信息。比如 Planner 把整个界面树传给 Locator,Locator 又把整个界面树传给 Actor,那拆分就白做了,token 该爆还是爆。

正确的做法是:每个角色输出的是摘要,不是全量。

Planner 输出的是自然语言指令,可能就一句话:“点击页面底部的购物车图标”。Locator 拿到这句话,再去当前界面里找匹配的元素,输出的是一个精简的元素描述:“元素类型 ImageButton,content-desc 包含‘购物车’,bounds [540, 2100, 660, 2220]”。Actor 拿到这个描述,直接执行点击。Validator 只需要对比点击前后的页面变化,比如“页面标题从‘首页’变为‘购物车’”,不需要知道中间发生了什么。

这样每个角色处理的 token 量都控制在几百到一千以内,整体链路跑下来非常轻。

3.3 角色拆分的粒度:什么时候该继续拆,什么时候该合并

不是拆得越细越好。我见过有人把 Locator 再拆成“文本定位者”和“图像定位者”,结果两个角色互相打架,文本定位说找不到,图像定位说找到了但置信度低,最后还要一个仲裁者。这就是过度拆分。

我的经验是:当一个角色内部出现明显的“模式切换”时,才考虑拆。比如 Locator 如果既要处理文本匹配又要处理图像匹配,而且两种匹配的提示词策略完全不同,那就拆。如果只是同一套逻辑处理不同元素类型,没必要拆。

另一个判断标准是调用频率。高频调用的角色(比如 Actor)应该尽量轻量,低频调用的角色(比如 Planner)可以重一些。如果某个角色调用频率很高但逻辑很重,就考虑把它拆成“轻量执行 + 重量决策”两层。

4. 每个角色的模型选型与提示词设计实战

4.1 Planner:用强推理模型,但要把调用次数压下来

Planner 是整个链路的“大脑”,它需要理解测试用例的意图,分解步骤,处理异常分支。这个角色我建议用推理能力最强的模型,Claude 系列或者 GPT-4 级别都可以。

但关键不是选哪个模型,而是怎么把调用次数压下来。我的做法是:让 Planner 一次性规划出接下来 3 到 5 步的指令,而不是每步都调。这样一条 20 步的用例,Planner 只需要调 4 到 7 次。

提示词设计上,Planner 的 system prompt 要包含几个关键信息:当前 App 的整体功能地图(哪些页面、哪些主要入口)、测试用例的完整描述、以及“你只需要输出下一步指令,不需要关心具体元素定位”的明确约束。

一个实际用的 Planner prompt 片段:

你是一个移动端测试规划助手。当前测试用例是:{case_description}。 已知 App 的主要页面包括:首页、搜索页、商品详情页、购物车、订单确认页、支付页。 你当前处于:{current_page_summary}。 请输出接下来最多 3 步的高层操作指令,每步用一句话描述,不要包含具体元素定位信息。 如果当前页面已经完成用例要求,输出 DONE。

注意这里的current_page_summary不是完整界面树,而是 Planner 上一轮自己维护的页面状态摘要,可能就几十个字。这样 Planner 的上下文始终很轻。

4.2 Locator:中等模型 + 结构化输出约束

Locator 的职责是把自然语言指令映射到具体元素。它需要理解界面结构,但不需要深度推理。我一般用中等规模的模型,比如 GPT-3.5 Turbo 或者 Claude Haiku 级别,响应快、成本低。

Locator 的提示词核心是结构化输出约束。你不能让它自由发挥,必须强制它输出固定格式。我通常要求它输出 JSON:

{ "found": true, "element_type": "Button", "resource_id": "com.example.app:id/login_btn", "text": "登录", "bounds": [100, 800, 300, 880], "confidence": 0.95, "reason": "页面中唯一包含'登录'文本的可点击元素" }

如果找不到,输出{"found": false, "reason": "..."}。这样 Actor 拿到之后可以直接解析,不需要再做自然语言理解。

这里有个实操心得:Locator 的界面输入要做裁剪。不要把整个 accessibility tree 塞进去,而是先做一轮规则过滤——只保留可点击、可输入、有文本或 content-desc 的节点,去掉纯容器节点。这样节点数量能从上千降到几十,Locator 的准确率和速度都会大幅提升。

4.3 Actor:最轻量模型 + 确定性执行

Actor 其实不太需要“智能”。它拿到 Locator 给的元素标识和操作类型,直接调用底层框架执行就行。这个角色我甚至建议用规则引擎而不是模型,但如果要用模型,选最便宜的、延迟最低的。

Actor 的提示词极其简单:

根据以下元素信息和操作类型,生成底层自动化框架的调用指令。 元素信息:{element_json} 操作类型:{action_type} 输出格式:{"command": "click", "target": "..."}

实际上,如果 Locator 输出的 JSON 已经足够结构化,Actor 可以直接用代码解析,不需要再过模型。这样又省一层成本和延迟。

4.4 Validator:中等模型 + 对比式提示词

Validator 需要判断操作是否成功。它的输入是操作前的页面状态摘要和操作后的页面状态摘要,输出是成功/失败判断。

提示词设计上,我采用对比式结构:

操作前页面状态:{before_summary} 操作后页面状态:{after_summary} 预期结果:{expected_result} 请判断操作是否成功。如果成功输出 {"success": true}。 如果失败,输出 {"success": false, "reason": "..."}。

这里的关键是before_summary和after_summary都要做摘要化处理,不能是完整界面树。摘要可以包括:页面标题、主要区域的关键文本、弹窗是否存在等。这样 Validator 的输入很短,判断也更快。

5. 角色间通信协议与状态管理:让拆分真正跑起来

5.1 统一的消息格式:JSON Schema 是刚需

四个角色之间如果各说各话,整个链路就散了。我强烈建议定义一套统一的 JSON Schema,所有角色之间的消息都按这个格式来。

一个简化版的消息格式:

{ "role": "planner|locator|actor|validator", "step_id": 3, "timestamp": 1700000000, "payload": { ... }, "status": "success|failed|pending", "error": null }

payload的内容根据角色不同而不同,但外层结构统一。这样日志、追踪、回放都好做。

5.2 状态机管理:谁负责记住“现在到哪了”

角色拆分之后,一个自然的问题是:谁负责维护整体状态?Planner 记不住所有细节,Actor 只关心当前操作。

我的做法是引入一个轻量级状态管理器,它不参与推理,只负责记录:当前执行到第几步、当前页面摘要是什么、上一步操作结果如何。每次调用角色之前,状态管理器把相关状态注入到提示词里;角色返回之后,状态管理器更新状态。

这个状态管理器可以用简单的键值存储实现,不需要模型参与。它就像一个“黑板”,所有角色都往上写、从上面读,但彼此不直接通信。

5.3 异常处理:当某个角色“卡住”了怎么办

实际跑起来,最常见的异常是 Locator 找不到元素。这时候不能直接失败,要有降级策略。

我的降级链路是:Locator 第一次找不到,让它重新分析界面,放宽匹配条件(比如从精确文本匹配改为模糊匹配);第二次还找不到,让 Planner 介入,重新规划这一步(可能换一条路径);第三次还不行,才判定失败并截图记录。

这个降级链路需要状态管理器配合,记录每个步骤的重试次数。超过阈值就升级处理。

6. 实测中的坑与调优:角色拆分不是银弹

6.1 角色之间的“理解偏差”:Planner 说的和 Locator 找的不是一回事

这是最隐蔽的坑。Planner 输出“点击登录按钮”,它心里的“登录按钮”可能是首页右上角的那个入口。但 Locator 在当前页面找到的是一个弹窗里的“登录”文字。两者都没错,但合起来就是错的。

解决办法是在 Planner 的指令里加入上下文约束。比如 Planner 输出时,要带上“在当前页面的主内容区域”这样的限定。Locator 拿到之后,也会优先在主内容区域搜索,而不是全页面乱找。

另一个办法是让 Planner 和 Locator 共享一份“页面区域定义”,比如把页面分为 header、main、footer、dialog 四个区域,Planner 指定区域,Locator 在指定区域内查找。

6.2 模型幻觉导致的“假成功”:Validator 说成功了,实际没有

Validator 有时候会被界面上的相似文本骗到。比如操作前页面有“登录成功”的 toast,操作后 toast 消失了,但 Validator 看到页面标题变成了“首页”,就判断成功了。实际上可能只是页面刷新了,并没有真正登录。

我的应对策略是:Validator 必须对比多个维度的变化,不能只看一个信号。比如同时检查页面标题、关键元素是否存在、URL 或 Activity 名称是否变化。多个信号一致才判定成功。

另外,Validator 的提示词里要明确列出“哪些变化不算成功”,比如“仅 toast 消失不算成功”“仅页面滚动不算成功”。把这些负向条件写清楚,能大幅降低假成功率。

6.3 延迟叠加:四个角色串行,一步操作要等四倍时间

角色拆分带来的一个副作用是延迟叠加。原来一个模型一次调用完成的事,现在要四次调用。如果每个角色平均 1 秒,一步操作就是 4 秒,20 步就是 80 秒。这还没算网络波动。

优化方向有几个:一是并行化,Locator 和 Validator 在某些场景下可以并行(比如 Validator 可以在操作执行的同时就开始准备对比数据);二是缓存,相同页面的 Locator 结果可以缓存,不用每次都调模型;三是模型分级,Actor 用最轻量的模型甚至规则引擎,把延迟压到最低。

我实测下来,经过优化后,单步操作的平均延迟能从 4 秒降到 1.5 秒左右,基本可接受。

6.4 提示词漂移:跑着跑着,角色开始“越权”

LLM 有个毛病,你让它只做一件事,它总想多做一点。Locator 有时候会自作主张地输出“我找到了元素,并且已经帮你点击了”。这就是越权。

解决办法是在提示词里加硬约束,并且用 few-shot 示例强化。比如在 Locator 的 prompt 里放几个正确示例和错误示例,明确告诉它“只输出定位结果,不要执行操作”。同时在代码层面做校验,如果 Locator 的输出里包含执行指令,直接丢弃并重试。

7. 从单机到规模化:角色拆分方案的扩展思路

7.1 多设备并行时的角色复用

当你要同时在多台设备上跑测试时,角色拆分方案有个天然优势:Planner 和 Validator 可以复用,Locator 和 Actor 需要按设备隔离。因为不同设备的界面可能略有差异,但任务规划和结果校验的逻辑是一样的。

我通常的做法是:Planner 和 Validator 各起一个实例,Locator 和 Actor 按设备数量起多个实例。状态管理器按设备 ID 分区。这样资源利用率最高。

7.2 角色模型的持续优化:从日志中学习

每次测试跑完,所有角色的输入输出都有日志。这些日志是宝贵的优化素材。我会定期分析:哪些 Locator 调用失败了、哪些 Validator 判断错了、Planner 的哪些指令导致了后续问题。然后针对性地调整提示词或补充 few-shot 示例。

这个迭代过程不需要重新训练模型,只需要改提示词和示例,成本很低,效果却很直接。

7.3 什么时候该考虑微调而不是提示词工程

如果某个角色的提示词已经优化到极限,准确率还是上不去,那可以考虑微调。但微调的前提是你有足够的高质量标注数据。我的经验是:至少 500 到 1000 条该角色的输入输出对,才值得做微调。而且微调之后要保留提示词工程的版本作为 fallback,因为微调模型可能在某些边缘 case 上反而不如提示词版本。

对于大多数团队来说,提示词工程 + few-shot 已经能解决 90% 的问题,微调是最后的手段。

8. 一些个人体会:角色拆分教会我的事

做这套角色拆分方案的过程中,我最大的体会是:LLM 应用架构和传统软件架构的边界正在模糊。以前我们讲微服务、讲单一职责,那是代码层面的事。现在做 LLM 应用,同样的原则在提示词和模型编排层面又适用了一遍。Planner 就是“服务编排层”,Locator 是“路由层”,Actor 是“执行层”,Validator 是“监控层”。架构思想是通的,只是实现手段从代码变成了提示词和模型调用。

另一个体会是:不要追求一步到位。我一开始想把四个角色全部用模型实现,结果又慢又贵又不准。后来把 Actor 换成规则引擎,把 Locator 的界面输入做了规则过滤,整体效果反而好了很多。该用模型的地方用模型,该用规则的地方用规则,混合架构才是现实解。

最后一点:日志和可观测性比什么都重要。角色拆分之后,链路变长了,出问题的时候如果不知道是哪个角色出的错,排查起来就是灾难。所以从第一天起就要把每个角色的输入输出、耗时、token 消耗全部记录下来。这些数据不仅能帮你排查问题,还能帮你做成本核算和优化决策。

这套方案我目前在一个中等规模的移动端测试项目里跑着,覆盖了大约 200 条核心回归用例,日均执行 3 轮。稳定运行两个月之后,用例通过率从最初的 72% 提升到了 91%,单轮执行成本从原来的 $40 降到了 $6 左右。当然还有优化空间,比如 Validator 的假成功率还有 3% 左右,Locator 在复杂弹窗场景下偶尔会找错元素。但整体来说,角色拆分这条路是走通了的,比单模型硬扛要可持续得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询