本系列走到第五篇前四篇把接口、单测、Web端UI的路数都捋过一遍这一篇终于轮到移动端。先摊牌讲把Web端那套Prompt玩法直接搬到App上大概率要翻车。原因很直接——移动端没有一个稳定得像DOM一样的结构可以依赖同一个按钮今天是“登录”明天版本一换就变成一个不带文字的Icon脚本里写死的路径往往比业务代码还脆弱。ARTEMIS是我在某次专项测试里搭出来的一套LLM移动端自动化测试框架核心是它的双模式架构一条链路让LLM先把自然语言意图离线转换成完整的执行计划再由设备端按计划执行另一条链路让LLM在测试过程中实时读取屏幕状态、边看边做决策。两条链路对应的是两类截然不同的目标——回归稳定性和探索覆盖率。这篇文章就把双模式为什么拆、怎么拆、踩了什么坑一次说清正在做移动端自动化智能化改造的同学应该能从中找到几条可以照搬的实践。1. 移动端自动化为什么要换思路1.1 传统脚本的“稳定性悖论”越走越窄移动端的UI自动化过去几年主流方案其实没有本质变化要么靠Accessibility ID/text定位要么靠层级XPath硬写。这套思路在页面结构稳定的前提下没问题但只要App进入快速迭代节奏问题立刻暴露。最常见的一幕是测试脚本里某个XPath写的是//android.widget.Button[contains(text,登)]结果产品把“登录”文案改成了“登 录”或者干脆换成一个纯英文Sign In按钮脚本直接红一片。问题不在于定位手段不够多而在于传统脚本把“人的意图”和“页面的呈现”死死绑在了一起。脚本作者脑子里想的是“输账号、输密码、点登录”落到代码里却全是坐标和属性中间隔着一层极容易碎裂的映射。每次UI改动碎的先是这层映射然后才是测试脚本。1.2 LLM真正改变的不是“写脚本”而是“拆耦合”LLM出现以后很多人第一反应是让它直接生成脚本省掉维护成本。实测下来这条路能走通但不是最值钱的部分。LLM真正带来的变化是把“意图—页面—动作”之间的强耦合拆松了。人对一个测试任务的描述永远是语义化的我要验证登录成功后能看到首页。而设备端能执行的永远是具体动作在哪个坐标框输入什么文本、点哪个控件。语义和动作之间正好需要一个中间层来承接。这个中间层如果交给LLM去做就等于把“UI变化导致的碎映射”问题从代码层上移到了语义层。语义层的容错能力比代码层强得多。页面文案从“登录”改成“Sign In”对LLM来说只是同一个概念的两种表达它能基于上下文判断出来。而传统脚本看到文字变了就真看不懂了。1.3 双模式的本质把“理解”和“执行”拆到不同时机移动端自动化测试的需求从来不是单一的至少可以分成两类一类是回归测试、冒烟测试。这类场景要求稳定、可复现、快最好在版本发布前批量跑完。它希望你提前把所有动作都规划好执行时不要有任何意外。另一类是探索性测试、新功能尝鲜。这类场景没有固定预期反而希望AI能像人一样边看边点发现脚本写不出来的异常路径。它要求实时理解当前屏幕实时做决策。这两类需求对“LLM介入时机”的要求完全相反一个希望理解发生在执行前一个希望理解穿插在执行中。如果只做其中一种都会有很大的局限。只做离线生成遇到动态变化的新页面就抓瞎只做实时决策回归场景的成本和稳定性根本扛不住。所以ARTEMIS一开始就按双模式来设计两条链路各管一段。2. ARTEMIS架构全景四层结构一张图说清2.1 描述层、规划层、执行层、验证层整个ARTEMIS可以抽象成四个层级层与层之间靠结构化JSON传递数据。描述层负责接收测试意图比如“验证错误密码时能提示失败信息且不会进入首页”。这一层的输入可以是测试人员手写的自然语言也可以是从需求文档里抽取出来的验收标准。规划层的职责是把描述层的自然语言转成结构化的执行计划。执行计划不是代码而是一张动作图。LLM在这层做的事本质上是一个“翻译编排”的过程它要知道用户意图里有哪几个关键操作也知道这些操作在设备上大概对应什么类型的行为。执行层是真正和设备打交道的部分。它读入动作图逐条执行遇到异常时回调自愈服务执行完一条就记录一条轨迹。这一层不关心“用户到底要测什么”只关心“动作原语是否被执行成功”。验证层负责回答“这一步到底算不算通过”。它会把预期状态和当前实际状态做一个语义比对输出结构化判断结果。2.2 动作原语先定义“能干什么”再谈“怎么理解”在架构落地时第一步要做的不是写Prompt而是先定一套动作原语Action Primitive。这是整个系统能运转的前提。API说明关键参数wait_for等待某个条件成立target、timeoutinput_text在目标控件输入文本target、texttap点击目标控件targetswipe滑动操作start_target、end_target、directionassert_state断言当前页面状态符合预期expect_statecapture_state主动抓取当前状态快照scene_typeswitch_app切换前后台或唤起特定页面app_id这里有一个设计上容易忽视的点所有原语的参数必须语义化不能直接给坐标。比如tap的参数是login_button不是(240, 560)。具体坐标由执行层根据当前页面状态去解析。这样一来动作图里承载的是“操作意图”而不是“某一次执行时的页面布局”。页面布局变了动作图不需要变。2.3 运行时状态快照给LLM一个“可读的世界”不管是离线规划还是实时决策LLM都需要知道当前设备上发生了什么。所以ARTEMIS定义了一个标准的状态快照结构页面树把原生控件和WebView控件统一转换成结构化文本清单截图可选动态模式下必须静态模式下用于验证层系统状态当前App包名、Activity/ViewController、网络状态、前后台标记页面树的Token化是成本控制的关键。一个中等复杂度的页面原始View层级可能有上百个节点直接全量丢给LLM会瞬间撑爆上下文。实际做法是过滤掉不可见的、无id无文本的、纯布局容器节点再按“可见性交互性”排序截断。压缩后一个页面大概能控制在两千到四千个Token以内够用且不会太贵。3. 模式A与模式B的内部链路拆解3.1 模式A计划先行适合回归与冒烟模式A的核心思路是“先想清楚再做”。LLM在测试执行之前拿到测试意图和一份页面结构预估离线生成一张完整的动作图然后设备端照着图执行。流程分成五步测试人员输入意图规划服务调用LLM输出动作图JSON动作图经过校验存入测试计划库测试执行时Runner读取计划库里的动作图逐条执行执行结果和验证结果一并写回报告模式A在Prompt设计上有一条实测有效的经验要求LLM严格按Json Schema输出不要让它自己发挥格式。Schema里明确每个节点的action必须取自原语枚举、params.target必须语义化、edges必须能构成完整路径。温度参数在这个模式下要压到极低我用的值是0.2。原因是动作图的生成属于强约束任务不需要创造性越低越稳。把few-shot示例放两到三组进去一组购物下单的、一组登录注册的、一组列表滚动的模型对输出格式的理解立刻上一个台阶。3.2 模式B实时决策适合探索与新功能模式B的起点没有提前生成好的动作图只有一个任务描述和一条执行上限。LLM在每一步都观察当前状态快照然后决定下一个动作原语执行完再次观察形成“感知—决策—执行”的循环。这个循环里最核心的控制参数是步数上限。探索性测试如果没有边界LLM会在一个页面里反复横跳或者陷入某种无意义循环。ARTEMIS的做法是默认给15步上限超过后强制停止并输出已探索路径。实测下来15步已经足够覆盖绝大多数“登录—浏览—操作—退出”类路径。模式B必须使用多模态模型因为它要处理截图。温度可比模式A略高我设的是0.4稍微给一点探索多样性但超过0.6后动作质量会明显下降会出现大量swipe乱滑。还有一点很关键模式B的每一步决策都要带上wait_for原语作为“观察接口”。LLM需要被明确告知执行完一个动作后不能立刻继续必须等待页面响应稳定后重新抓取状态快照。否则很多决策都基于半加载完成的状态后续断言全乱。3.3 两种模式最终落进同一个Runner很多双模式设计最后会做成两套互不相干的系统这是大忌。ARTEMIS的做法是两种模式共享同一个执行引擎Runner。Runner对外只接收一张动作图。模式A的动作图由LLM先生成再传送给Runner模式B同样也是生成动作图只不过这张图不是一次性生成完而是每次决策后追加一个节点逐步拼出来的。这样的好处在于下游所有能力只需要实现一遍元素定位、异常上报、轨迹记录、截图归档两套模式天然复用。以后想改定位策略只改Runner一端就够不需要动任何一层的LLM逻辑。4. 关键实现元素定位、断言验证与自愈4.1 从View层级到语义索引的四级定位元素定位是整个移动端自动化里最敏感的地带也是AREMIS踩坑最深的地方。最终沉淀下来的策略是四级定位按优先级依次尝试成功就停语义ID定位优先查找开发埋的testID和Accessibility ID文本定位根据可见文本和占位符匹配目标控件结构路径定位从页面树里寻找“底部Tab栏第2项”“列表第3行”这类语义路径视觉定位交给多模态模型识别坐标在原语执行前做边界校验前三级在页面树Token化之后执行速度快、成本低。第四级只在动态模式下作为兜底。实际运行中前三级的成功率大概在85%左右剩下15%要靠视觉兜底。一个重要的工程细节执行层在拿到LLM给出的目标描述之后必须先做“锁定”锁定的意思是把当前页面上匹配到的控件实例缓存起来。否则从动作图生成到真正执行之间有几秒延迟期间的页面可能已经变化了。4.2 视觉定位的坐标校正与上下文裁剪多模态模型在截图里点位置看起来灵光真到落地全是细节。第一个问题是坐标误差。模型给的是一个大致区域不是精确落点。所以ARTEMIS拿到坐标后要做两步处理先取目标最小外接矩形的中心点再检查中心点是否落在可点击控件上。不满足就按步长搜索最近的可点击元素搜索半径一般控制在二三十个像素内。第二个问题是截图成本。原始截图直接送进多模态模型又贵又慢。实测下来把截图长边缩到768、JPEG质量压到70%对定位准确率的影响几乎可以忽略但Token和延迟能降一半以上。同时不要一次性把整条截图序列都发给模型每条请求只带当前最新帧和历史最近两帧足够维持上下文延续性。4.3 断言验证让LLM比较“语义状态”而不是数像素断言是双模式里最容易设计失败的一环。一开始的直觉是让LLM看一眼截图判断“登录是否成功”看起来很AI实际上不可靠。截图里偶尔弹出个活动弹窗LLM就可能犹豫。后来我把断言改成了结构化流程预期状态转成可核验的语义条件比如“首页导航栏出现、用户头像控件可见、登录入口消失”把当前页面树文本和截图同时交给LLM要求LLM输出一个严格的结构化结果{result: pass/fail, evidence: [navbar_visible, avatar_visible], reason: ...}关键是evidence必须引用页面树里的具体控件ID或文本而不是模糊地说“看起来没问题”。这能有效逼着模型去对照真实状态而不是凭常识脑补。实测中启用这个约束之后断言误判率下降了约一半。4.4 自愈机制必须“带锁前提”自愈机制是运行时最危险的地方。最开始我做得比较暴力定位失败就无限重试换定位方式再试。结果某次测试中发现一个诡异现象——点击“登录按钮”失败后自愈逻辑换了一个坐标去点结果点到了旁边的“忘记密码”测试继续往下走竟然把“找回密码流程”当成“登录成功”后续路径跑下去了。这实际上是LLM在自愈时产生了“错误的创新”它不是恢复了原定意图而是为了继续执行而扭曲了步骤。后来给自愈加了三条硬约束降级定位只允许在同目标语义下进行不允许更换目标每次降级前必须重新抓取状态快照确认目标控件确实改变了位置而不是误判重试超过2次就放弃本次节点标记异常不允许绕过执行后续动作自愈可以灵活但不能无边界地灵活。灵活的前提是整套动作图仍然成立一旦动作图被扭曲整条测试路径就失去了意义。5. 部署落地时的参数与控制经验5.1 模型选型与几个容易被忽略的参数ARTEMIS在模型选型上走的是“分层混用”路线静态模式的动作图生成、断言判断这类偏语义推理的任务用中小型模型的强逻辑版本就能胜任动态模式的视觉定位和实时决策必须上多模态大模型。选型之外有三个参数直接决定成败第一个是max_tokens。动作图动辄输出几百行JSONtoken给不够就会截断成非法JSON。我一般至少给到2000断言输出短一些800到1000足够。第二个是response_format。只要模型支持强制JSON输出就一定开启。宁可让模型说“我无法判断”也不要让它输出一段带散文性质的文本。第三个是stop序列。在动作图生成场景里把“json”加进停止序列可以防止模型输出包裹代码块的Markdown格式省一道解析逻辑。5.2 Token预算从哪里省状态快照优化成本控制的核心不在模型价格而在状态快照。快照吃掉的Token远比你想象的多。一个中等复杂度的App页面完整View层级打印出来可能有两三百行。ARTEMIS的过滤策略按三步走去掉visibilitygone/invisible的节点去掉宽高为0的纯容器去掉没有id、没有文本、没有描述的纯布局View做过这三步一个页面通常能压掉六成节点。再配合截图降采样单次状态快照的成本大约是纯量输入的3到5倍但已经完全可接受。实测数据跑一轮包含40个case的回归平均每个case触发6次状态快照模型输入Token消耗大约在15万到20万之间。这个量级在预算上是可以接受的。5.3 并发、超时与重试的合理边界LLM服务化和设备集群之间有一个天然的瓶颈设备能并行跑但LLM接口的并发是有限制的。ARTEMIS的做法是给LLM调用加了一层队列保证设备侧请求不会直接打爆模型服务。单步决策在动态模式下设置8秒超时超过就返回“决策失败”。不要设置无限等待因为模型偶尔会陷入“思考僵局”等多久都不会有结果。重试预算要区分场景。模式A每个case最多重试2次重试前必须重新生成动作图模式B单步最多重试1次重试不改动作目标只换定位路径。这个边界定下来以后自动化的整体稳定性反而上升了——因为很多“重试”本质上是在重复一个本身就有问题的决策重试越多坏轨迹越长。6. 我踩过的四个坑6.1 页面还没加载完LLM就开始断言这是最常见、也最隐蔽的坑。App页面切换时有一小段过渡动画此时页面树是一个半空状态。模式B的LLM看到这个半空状态直接下了个“当前页面为空”的结论然后顺着这个结论做了一堆无意义操作。解决办法是在动作图里强制要求执行层“稳定等待”。我现在会在每次页面切换后自动调用一次wait_for条件是“页面树非空且最近0.5秒内无新增节点”然后才允许继续。判断“页面是否稳定”不能靠固定sleep要靠观察节点变化速率。6.2 坐标定位在原生页和WebView之间反复横跳App里混合了原生页面和WebView时同一个“登录”控件一会儿出现在原生层一会儿以Web元素的身份出现。四层定位里前三种都识别不到WebView内部元素只能走视觉兜底。后来发现最有效的办法是先识别当前页面的“类型标记”页面树里是否存在WebView节点。如果存在就把定位策略切换成WebView专用的元素提取方案统一走一次内容注入。这个前置判断比在定位结果里反复尝试高效得多。6.3 自愈机制产生了“伪创新路径”这节前面提过。最典型的例子是LLM为了把一个失败步骤“救回来”故意把目标控件改成另一个功能相似的控件。表面上看测试继续跑通了实际上根本测错了东西。应对方法是给自愈日志加标记。每次走到自愈分支在执行轨迹里写入recovered: true并记录原始目标和新目标。后续分析报告里凡是recovered: true的步骤都要人工过一遍。同时上面提到的“不允许更换目标”这条规则是真正解决这个问题的根。6.4 回归测试的断言开始“撒谎”用LLM做断言最怕的不是它判断失败而是它“判断通过但理由错误”。某次测试中登录失败时弹了一个toast页面实际还留在登录页但LLM看到页面上有一张品牌宣传图就认为“页面内容丰富可能进入首页成功”给出了pass。这之后我强制要求断言输出必须列出evidence而且要引用页面树里的实际节点。凡是没有evidence支撑的pass一律视为fail。同时定期从断言日志里抽一批“通过样本”反向验证看看LLM到底是怎么“骗过自己”的。这个方法很笨但确实管用。我现在每次跑完批量回归第一件事不是看通过率而是先翻一遍自愈标记和evidence清单。凡是标记数量异常增多的区块基本就是页面改动或者用例设计出了问题比盯着红绿报表有效得多。
阅读完成 · 觉得有帮助?