资讯动态

移动端自动化测试:LLM多角色拆分架构与实操指南

发布时间:2026/10/20 14:28:13 来源:尧图企业网站定制
1. 移动端自动化测试的模型拆分困局做过移动端自动化的人都有一个共同感受脚本写起来不难难的是让它稳定跑下去。UI 元素定位漂移、页面加载时序不确定、不同机型渲染差异这些问题叠加在一起传统基于固定选择器的脚本维护成本高得离谱。大语言模型介入之后情况有了转机——模型能理解页面语义能根据自然语言指令推断操作意图不再完全依赖硬编码的元素 ID。但新的问题随之而来一个模型既要理解测试意图又要解析页面结构还要生成具体操作指令任务耦合太重效果反而不好。DroidRun 和 Mobilerun 这两个项目在架构设计上做了一个关键决策把单一模型拆成多个角色每个角色只负责一件事。这个思路借鉴了软件工程里的单一职责原则也参考了多智能体协作的范式。具体来说系统里至少存在三类角色规划者负责把测试用例拆解成步骤序列执行者负责把每一步映射到具体的 UI 操作观察者负责判断当前页面状态是否满足预期。三者通过结构化消息传递协作而不是让一个模型从头到尾包办。这套拆分方案解决的核心问题是让每个模型只处理它最擅长的信息粒度。规划者看到的是测试用例文本和历史执行记录不需要关心页面上有哪些按钮执行者看到的是当前页面的元素树和当前步骤描述不需要理解整个测试的业务背景观察者看到的是操作前后的页面快照对比不需要知道下一步要做什么。信息隔离带来的是提示词更短、推理更聚焦、输出更可控。适合谁来参考这套方案如果你正在做移动端自动化测试并且已经尝试过用单个大模型驱动整个流程但效果不理想那这篇文章就是写给你的。如果你还没接触过 LLM 驱动的测试但手头有大量重复的移动端回归测试任务也可以从这里了解一种新的架构思路。下面的内容会从设计动机、角色定义、通信协议、实操配置、问题排查几个维度展开尽量把每个决策背后的“为什么”讲清楚。2. 角色拆分的核心设计逻辑2.1 为什么单模型方案会失败先说说我踩过的坑。最早尝试用单个大模型驱动移动端测试时提示词大概是这样的你是一个移动端测试助手当前测试用例是“登录后进入个人中心修改昵称”当前页面元素树如下……请输出下一步操作。这个提示词看起来合理但实际跑起来问题很多。第一个问题是上下文长度爆炸。一个中等复杂度的 App 页面元素树序列化之后动辄两三千个 token加上测试用例描述、历史操作记录、系统提示词很容易突破模型的上下文窗口。一旦超限模型就开始丢信息要么忘记测试目标要么忽略页面上的关键元素。第二个问题是注意力分散。模型同时要理解业务目标、解析页面结构、判断当前进度、生成操作指令这四个任务需要的注意力模式完全不同。业务目标需要宏观理解页面解析需要细节捕捉进度判断需要时序推理操作生成需要精确输出。让一个模型同时做这四件事结果就是每件事都做得不够好。第三个问题是错误传播。单模型方案里如果模型在某一步误解了页面元素这个错误会直接影响下一步的决策而且很难定位到底是哪个环节出了问题。是页面解析错了还是业务理解偏了还是操作生成格式不对排查起来非常痛苦。2.2 角色拆分的三个原则DroidRun 和 Mobilerun 的拆分方案遵循了三个原则我觉得值得单独拎出来说。原则一信息隔离。每个角色只接收它完成任务所必需的信息多余的信息一律不传。规划者不需要知道页面上有哪些按钮执行者不需要知道整个测试的业务背景。这样做的好处是提示词短、推理快、输出稳定。我实测下来拆分之后每个角色的提示词长度平均只有单模型方案的 30% 左右推理延迟降低了差不多一半。原则二接口标准化。角色之间的通信必须用结构化格式不能是自由文本。比如规划者输出的是一个步骤列表每个步骤包含步骤编号、操作类型、目标描述、预期结果四个字段。执行者接收这个结构输出的是具体的操作指令包含操作类型、目标元素定位符、输入内容等字段。标准化接口的好处是每个角色的输出可以被程序解析和校验不依赖模型输出的自然语言格式。原则三可替换性。每个角色背后的模型可以独立替换。规划者可以用推理能力强的模型执行者可以用响应速度快的模型观察者可以用多模态能力好的模型。这种灵活性在实际部署时非常重要因为不同角色的性能瓶颈不一样统一用一个模型往往会造成资源浪费。2.3 角色划分的粒度选择角色拆分的粒度是个需要权衡的问题。拆得太粗等于没拆拆得太细通信开销会吃掉大部分收益。DroidRun 和 Mobilerun 选择的是三到四个角色的粒度我觉得这个选择很务实。具体来说规划者负责把测试用例拆成原子步骤。比如“登录后修改昵称”这个用例规划者会拆成打开应用、输入用户名、输入密码、点击登录、等待首页加载、点击个人中心入口、点击昵称字段、清空原昵称、输入新昵称、点击保存、验证昵称已更新。一共十一步每一步都是一个原子操作。执行者负责把原子步骤映射到具体操作。比如“输入用户名”这一步执行者需要根据当前页面元素树找到用户名输入框的定位符然后生成输入操作。如果页面上有多个输入框执行者需要根据步骤描述里的语义信息判断哪个是用户名输入框。观察者负责判断操作结果。比如点击登录之后观察者需要判断页面是否跳转到了首页还是弹出了错误提示。观察者的输入是操作前后的页面截图或元素树对比输出是一个布尔值加置信度。有些方案还会加一个协调者角色负责处理异常情况。比如执行者找不到目标元素时协调者决定是重试、跳过还是终止测试。这个角色在复杂场景下很有用但也会增加系统复杂度。我的建议是先用三角色方案跑通主流程遇到频繁异常再考虑加协调者。3. 各角色的提示词设计与实操要点3.1 规划者角色的提示词模板规划者的核心任务是把自然语言描述的测试用例拆解成结构化步骤列表。提示词设计的关键是约束输出格式和提供足够的业务上下文。我常用的规划者提示词模板是这样的你是一个移动端测试规划助手。你的任务是把测试用例拆解成原子步骤序列。 测试用例{test_case} 应用背景信息 - 应用名称{app_name} - 主要功能模块{modules} - 已知的页面跳转关系{navigation} 拆解要求 1. 每个步骤必须是原子操作不可再分 2. 每个步骤包含步骤编号、操作类型、目标描述、预期结果 3. 操作类型限定为点击、输入、滑动、等待、验证 4. 步骤之间要有明确的时序关系 5. 如果测试用例包含条件分支用“如果...则...”描述 输出格式JSON { steps: [ { step_id: 1, action_type: 点击, target: 登录按钮, expected: 进入登录页面 } ] }这个模板里有几个细节值得注意。应用背景信息这一块很多人会忽略但它对规划质量影响很大。如果规划者不知道应用有哪些模块它可能会拆出一些不存在的步骤。操作类型限定是为了让执行者能提前准备好对应的操作处理器避免出现无法识别的操作类型。条件分支的处理是个难点我的经验是让规划者用自然语言描述分支条件执行者在运行时根据实际页面状态决定走哪个分支。3.2 执行者角色的提示词设计执行者的输入是当前步骤描述和当前页面元素树输出是具体的操作指令。提示词设计的关键是元素定位的准确性和操作参数的完整性。你是一个移动端测试执行助手。你的任务是根据当前步骤描述和页面元素树生成具体的操作指令。 当前步骤 - 步骤编号{step_id} - 操作类型{action_type} - 目标描述{target} - 预期结果{expected} 当前页面元素树 {element_tree} 生成要求 1. 根据目标描述在元素树中找到最匹配的元素 2. 如果找到多个候选元素选择语义最接近的 3. 如果找不到匹配元素输出 {error: element_not_found, reason: ...} 4. 操作指令必须包含完整的定位信息 输出格式JSON { action: click, locator: { type: resource_id, value: com.example.app:id/login_btn }, params: {} }这里有个实操心得元素树的序列化方式直接影响执行者的准确率。我试过三种序列化方式完整 XML、简化后的元素列表、带语义标注的元素列表。实测下来带语义标注的元素列表效果最好。具体做法是只保留可交互元素和文本元素每个元素标注它的类型、文本内容、资源 ID、位置信息。这样既减少了 token 消耗又保留了关键定位信息。另一个心得是候选元素排序。当目标描述比较模糊时执行者可能会找到多个候选元素。我通常会在提示词里要求执行者输出一个候选列表按置信度排序然后由程序根据历史成功率选择最可能正确的那个。这个策略在元素文本相似的情况下特别有用。3.3 观察者角色的判断逻辑观察者的任务是判断操作是否达到了预期结果。它的输入是操作前后的页面状态输出是判断结果和置信度。你是一个移动端测试观察助手。你的任务是根据操作前后的页面状态判断操作是否达到预期结果。 操作信息 - 操作类型{action_type} - 目标描述{target} - 预期结果{expected} 操作前页面状态 {before_state} 操作后页面状态 {after_state} 判断要求 1. 对比操作前后的页面变化 2. 判断变化是否与预期结果一致 3. 如果一致输出 {result: pass, confidence: 0.95} 4. 如果不一致输出 {result: fail, confidence: 0.8, reason: ...} 5. 如果无法判断输出 {result: unknown, confidence: 0.5, reason: ...}观察者的难点在于预期结果的模糊性。比如“进入首页”这个预期怎么判断当前页面就是首页我的做法是在提示词里加入应用页面特征库每个页面用几个关键元素标识。观察者判断时先匹配页面特征再判断是否与预期一致。这个特征库可以手动维护也可以从历史测试记录中自动提取。3.4 角色间的通信协议设计角色之间的通信协议决定了整个系统的稳定性和可调试性。我的经验是全部用 JSON 格式每个消息包含消息类型、发送者、接收者、时间戳、负载五个字段。{ msg_type: step_execution, from: planner, to: executor, timestamp: 1700000000, payload: { step_id: 3, action_type: 输入, target: 用户名输入框, expected: 输入框显示用户名 } }这种结构化协议的好处是每个消息都可以被日志系统记录和回放出问题时可以精确定位是哪个角色在哪个环节出了错。我通常会保留最近十次测试的完整消息日志排查效率比单模型方案高很多。4. 完整实操流程与配置方法4.1 环境准备与依赖安装先说一下基础环境。这套方案对运行环境的要求不算高但有几个关键依赖需要提前装好。Python 3.10 以上推荐 3.11因为异步性能更好移动端自动化框架Android 用 uiautomator2iOS 用 facebook-wda大模型调用 SDK根据你用的模型选择对应的包消息队列轻量级场景用 Python 内置的 asyncio.Queue 就够了安装命令大概是这样pip install uiautomator2 facebook-wda openai pydantic loguru这里有个坑要注意uiautomator2 的初始化需要设备连接稳定。我遇到过设备断连导致元素树获取失败的情况后来加了一个重连机制每次获取元素树之前先检查设备状态断了就自动重连。这个逻辑虽然简单但能省掉很多莫名其妙的失败。4.2 角色调度器的实现角色调度器是整个系统的中枢负责接收测试用例、调用规划者、分发步骤给执行者、调用观察者验证结果。我用 asyncio 实现了一个简化版调度器核心逻辑大概一百行左右。import asyncio from dataclasses import dataclass dataclass class TestContext: test_case: str steps: list current_step: int 0 history: list None class Orchestrator: def __init__(self, planner, executor, observer): self.planner planner self.executor executor self.observer observer async def run(self, test_case: str): ctx TestContext(test_casetest_case, steps[], history[]) ctx.steps await self.planner.plan(test_case) for step in ctx.steps: before_state await self.get_page_state() action await self.executor.execute(step, before_state) if error in action: await self.handle_error(ctx, step, action) continue await self.perform_action(action) after_state await self.get_page_state() result await self.observer.verify(step, before_state, after_state) ctx.history.append({ step: step, action: action, result: result }) if result[result] fail: await self.handle_failure(ctx, step, result) break return ctx.history这个调度器的关键设计是每一步都记录完整上下文。before_state、action、after_state、result 四个信息全部落盘出问题时可以精确回放。我实测下来这套日志机制在排查偶发失败时特别有用因为很多失败是时序相关的不看完整上下文根本定位不到原因。4.3 元素树获取与预处理元素树的获取和预处理是执行者准确率的关键。Android 上用 uiautomator2 的 dump_hierarchy 方法iOS 上用 wda 的 source 方法。原始元素树是 XML 格式直接丢给模型 token 消耗太大需要预处理。我的预处理流程分三步。第一步是过滤只保留可交互元素按钮、输入框、可点击的容器和文本元素。第二步是扁平化把嵌套的 XML 结构转成扁平列表每个元素记录它的层级路径。第三步是语义标注给每个元素加上类型标签和文本描述。def preprocess_element_tree(xml_str): import xml.etree.ElementTree as ET root ET.fromstring(xml_str) elements [] def traverse(node, path): attrib node.attrib if is_interactive(attrib) or has_text(attrib): elements.append({ path: path, type: attrib.get(class, ).split(.)[-1], text: attrib.get(text, ), resource_id: attrib.get(resource-id, ), bounds: attrib.get(bounds, ), clickable: attrib.get(clickable, false) true }) for i, child in enumerate(node): traverse(child, f{path}/{i}) traverse(root) return elements预处理之后一个中等复杂度页面的元素列表大概只有二三十个条目token 消耗降到原来的十分之一左右。这个优化对成本控制非常关键尤其是测试用例多的时候。4.4 操作执行与异常处理操作执行看起来简单但异常处理是重头戏。我总结了移动端自动化最常见的五类异常每类都有对应的处理策略。异常类型典型表现处理策略元素未找到定位符匹配不到元素重试一次仍失败则上报协调者元素不可点击元素存在但被遮挡等待 500ms 后重试最多三次页面未跳转点击后页面无变化等待 2s 后重新获取页面状态输入失败输入框未获得焦点先点击输入框再输入应用崩溃进程退出重启应用从当前步骤重试这些策略不是拍脑袋想的是实际跑了几百个测试用例之后总结出来的。比如元素不可点击这种情况很多时候是因为页面还在渲染动画等 500ms 就好了。页面未跳转往往是网络请求慢等 2s 比立即重试成功率高得多。4.5 结果验证与报告生成观察者验证完之后调度器会把结果汇总成测试报告。报告包含每个步骤的执行状态、耗时、截图、失败原因。我通常会把报告存成 JSON 和 HTML 两种格式JSON 用于程序分析HTML 用于人工查看。def generate_report(ctx): report { test_case: ctx.test_case, total_steps: len(ctx.steps), passed: sum(1 for h in ctx.history if h[result][result] pass), failed: sum(1 for h in ctx.history if h[result][result] fail), steps: ctx.history } return report报告里我特别关注失败步骤的上下文。一个步骤失败往往不是孤立事件可能是前面某一步的副作用。所以报告里会把失败步骤前两步和后一步的信息也带上方便定位根因。5. 常见问题与排查技巧实录5.1 规划者拆解粒度不合理怎么办规划者拆解太粗或太细都是常见问题。太粗的典型表现是“登录”作为一个步骤执行者不知道具体要做什么。太细的典型表现是把“点击登录按钮”拆成“移动手指到按钮位置”和“按下”两步执行者反而懵了。我的调整方法是在提示词里加正反例。给规划者看两个例子一个是好的拆解一个是不好的拆解让它模仿好的那个。这个方法比单纯描述要求有效得多。另外我会在提示词里明确“每个步骤对应一个用户可感知的操作”这个约束能过滤掉大部分过细的拆解。5.2 执行者定位元素频繁出错怎么优化执行者定位出错通常有三个原因元素树信息不足、目标描述模糊、页面有多个相似元素。元素树信息不足的解决办法是增加语义标注比如给每个元素加上它在页面中的相对位置顶部、中部、底部和相邻元素的文本。目标描述模糊的解决办法是在规划者输出时要求更具体的描述比如“用户名输入框”比“输入框”好。多个相似元素的解决办法是让执行者输出候选列表程序根据历史成功率选择。我实测下来这三个优化做完之后执行者的定位准确率能从 70% 左右提升到 90% 以上。5.3 观察者误判怎么减少观察者误判主要有两种把失败判成成功把成功判成失败。前者更危险因为会掩盖真实问题。减少误判的关键是提供更明确的判断依据。我会在提示词里加入页面特征库每个页面用三到五个关键元素标识。观察者判断时先匹配页面特征再判断是否与预期一致。另外对于“验证”类型的步骤我会要求观察者输出具体的验证依据比如“页面标题为‘个人中心’与预期一致”而不是只输出 pass 或 fail。5.4 角色间通信超时怎么处理角色间通信超时通常是因为某个角色的模型调用响应慢。我的处理策略是设置分级超时规划者超时 30s执行者超时 15s观察者超时 10s。超时后先重试一次仍超时则跳过当前步骤并记录异常。这里有个经验超时时间不要设得太短。我一开始把执行者超时设成 5s结果很多正常请求也被判超时因为模型偶尔会有响应慢的时候。后来调到 15s误判率大幅下降。5.5 多设备并行测试的注意事项多设备并行时最大的坑是设备间状态互相干扰。比如两个设备同时操作同一个账号后操作的会覆盖先操作的结果。解决办法是每个设备分配独立的测试账号或者用不同的测试数据。另一个坑是资源竞争。多个设备同时获取元素树时如果共用一个模型服务可能会出现请求排队。我的做法是给每个设备分配独立的模型调用配额避免互相影响。6. 角色拆分方案的扩展思路6.1 加入记忆角色提升长期稳定性三角色方案跑通之后我尝试加了一个记忆角色负责记录历史执行中的成功和失败模式。比如某个元素在特定页面状态下总是定位失败记忆角色会把这个模式记下来下次遇到类似情况时提前预警。记忆角色的实现不复杂本质上是一个向量数据库加检索逻辑。每次执行完一个步骤把步骤描述、页面特征、执行结果存成一条记录。下次执行前先检索相似记录如果有失败模式匹配就提醒执行者注意。这个扩展在长期运行的测试套件里效果很明显尤其是那些偶发失败的用例记忆角色能帮助快速定位规律。6.2 用微调模型替代通用模型通用模型在特定应用上的表现往往不够稳定因为每个应用的 UI 风格和交互模式都不一样。我试过用历史执行数据微调一个小模型专门做执行者角色效果比通用模型好不少。微调数据的构造方法是把历史执行记录里的页面元素树和对应的正确操作指令配对作为训练样本。样本量大概几百条就能看到效果。微调后的模型在元素定位准确率上比通用模型高 10 到 15 个百分点而且响应速度更快。6.3 跨平台适配的注意事项Android 和 iOS 的元素树结构差异很大执行者的提示词需要分别适配。Android 的资源 ID 通常比较规范iOS 的 accessibility identifier 则经常缺失。我的做法是抽象一层元素描述协议把两个平台的元素树都转成统一的中间格式执行者只处理中间格式。这个抽象层的好处是执行者的提示词可以复用坏处是转换过程可能丢失信息。我的经验是转换时保留原始属性作为附加字段执行者需要时可以查看。7. 我在实际项目中的几点体会这套角色拆分方案我在三个不同规模的移动端测试项目里用过最大的感受是前期投入值得。虽然搭建角色调度器和调试通信协议花了不少时间但后期维护成本比单模型方案低很多。单模型方案每次调整提示词都像拆盲盒不知道会影响到哪个环节角色拆分之后每个角色的提示词可以独立调整影响范围可控。另一个体会是日志比什么都重要。角色拆分方案的消息日志天然就是结构化的排查问题时可以按角色、按步骤、按时间多个维度过滤。我现在的习惯是每个测试用例执行完自动生成一份完整日志出问题先看日志再动手调试。最后分享一个小技巧角色提示词版本化管理。每个角色的提示词都当成代码来管理用 Git 记录每次修改和对应的测试通过率。这样当通过率下降时可以快速回滚到上一个稳定版本。这个习惯帮我省了很多次深夜排查的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑