如果你做过一段时间的移动端自动化测试,大概率对两件事印象深刻:一是UI自动化用例的脆弱,二是LLM在这件事上前所未有的吸引力。前几期我们聊过LLM做测试用例生成、错误分析、元素匹配,也专门讲过移动端的取流、控件定位和稳定性策略。这一期(系列第9篇,移动端第5篇)把话题指向一个更偏架构的方向:当一套面向通用任务的Agent框架,真正下沉到移动端测试场景,会发生什么。所谓下沉,不是把Agent聊天界面换成一个App就完事,而是说把大模型驱动的任务规划、工具调用、状态感知、自我反思这套能力,折叠进测试执行器里,让它在移动端UI自动化的真实约束下干活。开头先下个结论:通用Agent很强,但不会原样适合测试;能在测试场景里站住脚的,是对Agent做了一整套场景化改造后的产物。这篇文章就把这套改造过程拆开讲,适合正在做LLM加移动端自动化的团队和个人参考。
1. 为什么通用 Agent 框架到了测试场景必须重新改造
1.1 场景诉求的反转:从“探索开放目标”到“完成固定任务”
通用Agent的设计初衷,是面对一个开放式目标,例如“帮我规划一次周末旅行”,它自己去拆步骤、选工具、判断中间结果是否合理。它的能力假设里藏着几件事:目标可以是模糊的,允许试错,环境反馈足够丰富,单次会话结束就算交付。
移动端自动化测试把这几条几乎全部反过来了。测试用例的目标不是模糊的,而是明确的:登录要走到首页,下单要出现订单号,设置里改完开关状态要持久化。试错是被强烈禁止的,一套回归用例跑十次,历史问题不该因为Agent某次“尝试”新路径而被漏掉。环境反馈不仅不丰富,而且噪声极大,权限弹窗、广告位、启动动画、网络抖动都在干扰判断。最重要的一点是,测试要讲究可复现和可审计,一个用例跑过之后,你得能回答“它到底点了哪里、为什么认为结果正确”。
这就导致了一个结构性矛盾:通用Agent的强项是自由探索,测试场景最不需要的恰恰就是自由探索。把通用Agent不做改造直接接到移动端设备上,它会很自然地去做许多测试人员不想让它做的事情,比如点掉一个系统弹窗、滑到页面底部看有没有新内容、对着广告位产生兴趣。这些行为对普通助手来说是“智能”,对测试执行来说是灾难。
1.2 为什么“加一句 Prompt”解决不了下沉问题
有人可能会说,下沉无非是把Agent接到设备控制工具上,再加一句“你是一个专业的测试工程师,请严格按照用例执行”。这句话单独看没错,实际用起来却远远不够。原因是Prompt只能影响模型的输出倾向,改变不了它决策的信息来源和执行约束。
一个Agent在移动端设备上做决策时,需要知道“当前屏幕上有什么、哪些是可点的、哪些是文本输入框”,这些东西来自设备侧的UI层级快照。而测试场景要求Agent不能去点系统状态栏、不能误触广告位、不能在没有等到页面稳定时连续点击,这些约束写在系统提示词里当然可以,但模型并不会逐条遵守,尤其在上下文很长、前面已经执行了几十个步骤之后,模型很容易忘记早期的边界设定。
所以下沉的本质工作是两件:第一,在Agent的感知层和执行层之间插入一套测试场景特有的过滤器,让模型只看到它应该看到的;第二,把“允许自由决策”的边界用规则显式写死,让Agent在边界内规划,而不是在每个动作上等待它自觉。这件事靠Prompt做不干净,必须从框架层面动手。
提示:判断一个方案是“真下沉”还是“套壳”,就看它在感知层和执行层有没有引入测试语义。如果只是给模型加了一段测试工程师角色设定,那只能算是聊天Demo接了个鼠标键盘。
2. 把测试用例翻译成 Agent 能执行的任务剧本,这一步别偷懒
2.1 任务描述从“一句目标”升级为“结构化剧本”
通用Agent的输入可以是一句话,比如“帮我把这个App的登录流程跑通”。但移动端测试用例落到执行层面,至少要让Agent知道四个维度的信息:当前设备处于什么状态、操作目标是什么、哪些界面元素可以作为操作对象、最终拿什么标准来判定成败。
我们内部把一个测试用例改写成结构化剧本,字段大致长这样:
| 字段 | 示例值 | 作用 |
|---|---|---|
| 前置状态 | App冷启动,未登录 | 决定Agent第一步从哪里开始 |
| 起始页面 | 登录页 | 帮助Agent建立“当前坐标” |
| 操作目标 | 完成账号密码登录并进入首页 | 驱动规划层拆解原子步骤 |
| 预期断言 | 首页出现用户名文本 | 执行结束后由规则断言器判定 |
| 约束条件 | 跳过隐私协议弹窗,不点击任何广告位 | 限制Agent的行为边界 |
| 超时控制 | 单步10秒,整条用例90秒 | 避免在异常状态里无限循环 |
这个模板看起来像是传统测试用例的字段扩充,但它的关键区别在于:这些字段是给规划器用的,而不只是给人看的文档。前置状态和起始页面决定了Agent第一次感知时应该期望看到什么,预期断言决定了Agent结束执行后框架要检查什么,约束条件实际上会被编译成执行器侧的动作拦截规则。
我们实践下来发现,如果省略“约束条件”这个字段,Agent在场景里被系统弹窗或广告带走几乎是必然的。因为从模型的视角看,弹窗上的“允许”按钮和业务页面里的登录按钮地位相同,都是可点击节点。你不提前告诉它哪些区域不许碰,它就真的会碰。
2.2 规划输出必须结构化,目标必须以语义描述为中心
Agent拿到任务剧本后的第一件事是生成规划。为了不让这个规划变成一段“看起来有道理但无法执行”的自然语言,我们在输出侧做了强约束。规划被定义为JSON数组,每个步骤包含动作类型、目标描述、执行后校验点。
{ "plan": [ { "step": 1, "action": "tap", "target": "登录页账号输入框", "check": "出现键盘输入光标" }, { "step": 2, "action": "input", "target": "登录页账号输入框", "text": "test_user_001", "check": "输入框文本变为test_user_001" }, { "step": 3, "action": "tap", "target": "登录页登录按钮", "check": "页面跳转到首页" } ] }这里有两个细节值得展开。第一,target字段写的是“登录页账号输入框”,而不是具体的“控件ID”或“坐标值”。坐标在UI微调后立刻失效,ID在跨平台和动态渲染场景里经常拿不到,而语义描述可以在执行时通过UI层级摘要做匹配,对细微的布局变化有更强容忍度。定位这件事真正落地时,是用“语义描述”去匹配实时UI快照,而不是由模型直接拍一个坐标出来。
第二,每个步骤自带check字段,相当于把传统自动化里的“执行后断言”提前到了规划阶段。它可以是一个页面跳转判断,也可以是一个文本变化判断。执行器每完成一步,先跑该步的check,再决定是继续下一步还是让Agent反思修复。这个机制把“规划”和“校验”解耦了,即使Agent规划的路径有问题,执行器也能在中间步骤发现问题,而不是一路错到底。
3. 移动端 UI 状态怎么喂给 LLM:场景感知与动作空间设计
3.1 从原生层级到UI摘要:控制信息密度比堆信息更重要
移动端的UI状态感知,最原始的做法就是把设备侧的视图层级整包丢给模型。iOS侧拿到的视图树、Android侧拿到的View节点树,一个复杂的页面动辄上千个节点,其中包含大量不可见、不可交互、纯用于布局的节点。把这些原样塞给LLM,会出现两个问题:一是token消耗会瞬间爆炸,一个页面几万token很常见,跑一条用例几十步,成本完全失控;二是信息噪声会显著拉低模型的判断准确率,模型会盯着哪些隐藏节点或非业务区域的文本,反而忽视真正该操作的目标。
我们的处理流程分三步:先过滤,再摘要,最后叠加截图。过滤规则只保留“可见且可交互”的节点,剔除不可见、透明、宽高为0、纯容器节点。接着把剩余节点按页面区块归纳成结构化文本,形成一页UI摘要。最后对于视觉信息占主导的页面,例如图片轮播、卡片式信息流,再附上一张降采样截图给多模态模型兜底。
一份UI摘要长这样:
当前页面:登录页 可见控件: - 输入框: 账号(placeholder: 请输入手机号) - 输入框: 密码(placeholder: 请输入密码) - 按钮: 登录(支持点击) - 链接: 忘记密码 - 弹窗: 隐私协议(按钮: 同意、拒绝) 坐标信息:账号输入框(540,320,700,100),密码输入框(540,460,700,100),登录按钮(540,620,700,100)注意摘要里保留了坐标,但坐标在这里是给“执行器”用的,不是给“模型规划”用的。模型读到的是控件的语义类型和文本,执行器拿到模型选择的语义描述后,再去摘要里匹配对应坐标完成点击。这种分层各取所长的做法,比让模型直接输出坐标要稳定得多。
3.2 动作空间必须收窄成最小可靠原子集
通用Agent的工具面通常很宽,能打开网页、查数据库、发请求、调用代码解释器,工具之间还能自由组合。但这种宽泛放到移动端测试场景里不是优势,模型在工具选择上多花一次决策,就多一分选错的风险。我们最终把动作空间收敛成9个原子动作,每个动作对应设备侧一个确定的执行函数。
| 动作 | 参数 | 说明 |
|---|---|---|
| tap | target | 点击一个语义目标 |
| long_press | target, duration | 长按触发上下文菜单 |
| input | target, text | 输入文本 |
| swipe | start, end, duration | 按坐标滑动 |
| scroll | direction, distance | 列表滚动 |
| back | 无 | 返回上一页 |
| wait | seconds | 显式等待,用于页面稳定 |
| capture | 无 | 截图并生成UI摘要 |
| assert | expected | 规则断言,不开放给Agent自由评断 |
为什么没有提供更多动作,比如双击、拖拽、摇晃设备?因为这些动作在测试主流程里出现频率极低,多放一个动作进来,模型在下一次决策时就要多一个候选,复杂页面里很容易选错。动作空间越窄,Agent的行为越可预期,排查问题也越容易。工具不是越多越好,是越贴合场景越好。
输入动作值得单独说一句。移动端输入相比Web端的type操作有一个隐蔽的坑:系统输入法和第三方输入法会改变键盘形态,直接type的文本经常因为键盘切换而丢失。我们的做法是优先使用剪贴板方案,把文本写入剪贴板,再通过长按输入框选择“粘贴”。这个方案能绕开大部分输入法兼容问题,但也要注意剪贴板权限弹窗,弹窗出现时仍然会被执行器拦截逻辑接住。
每个动作执行完,设备侧需要返回一个统一格式的结果对象,里面包含执行状态、当前UI摘要、截图路径和需要给到模型的关键信息。这个统一结果结构是后续所有反思、重试、失败分析的基础,没有它,Agent就是一个只能发指令、听不到回音的瞎指挥。
{ "action": "tap", "status": "success", "target": "登录页登录按钮", "evidence": { "screenshot": "/evidence/shot_001.png", "ui_snapshot": "/evidence/ui_001.txt" } }4. 稳定性设计与状态校验:把 Agent 的“自由发挥”关进笼子
4.1 先感知后行动:执行前校验是规则代码,不是LLM自评
Agent的规划输出里,目标描述是“登录页登录按钮”,它并不知道这个按钮在当前屏幕上对应哪个坐标。坐标解析发生在执行器里:执行器拿到UI摘要后,用语义匹配把“登录页登录按钮”映射到具体坐标点,然后才执行点击。
这个环节里最关键的一个设计是:如果当前UI摘要里根本找不到Agent描述的目标,执行器绝不猜测一个近似位置去点,而是直接把“目标不可见”的结果反馈给Agent,并附带当前UI摘要,让Agent重新感知和规划。这个“找不到就不执行”的原则,是我们踩了很多次“模型幻觉导致乱点”之后总结出来的。
LLM有一种倾向,在上下文不完整时会“脑补”出它认为应该存在的元素。比如它认为这一步之后应该进入设置页,就可能在当前页面没有设置入口的情况下仍然输出“点击设置按钮”。如果执行器直接按文本去匹配,匹配不到就报错,那Agent会得到正确的反馈;但如果你为了“提升执行成功率”加了一个模糊匹配,让它去点最接近的元素,那就可能点到完全不相干的功能。测试场景最怕这种静默错位,错误往往到很后面才会暴露。
所以“先感知后行动”是一条强制性原则。ActionDescriptor只作为期望,能否执行完全以实时感知结果为准。所有语义匹配逻辑全是确定性规则代码,不允许模型在“目标是否存在”这个问题上自行判断。模型能做的是在匹配失败后调整策略,例如滚动一下页面,或者回头审视自己的规划是否出了问题。
4.2 断言与证据链:信任规划能力,但绝不信任自我评价
另一个容易踩的坑,是让LLM自己判断执行是否成功。模型偏低估失败、倾向乐观,让它对着截图说“这个步骤已经完成”,经常得到错误的“成功”结论。我们现在的分工是:规划能力信任LLM,校验结论交给规则。
规则断言可以是元素的显隐判断、文本内容匹配、控件属性值校验,也可以是页面截图的像素差异分析。这些断言必须能在毫秒级完成,不能依赖模型推理。每条用例结束时的预期断言,默认全走规则框架。如果规则断言通过了,整条用例判为通过;如果断言不通过,才轮到LLM介入分析失败原因。
证据链是这套设计的另一个支撑。每执行一个动作,设备侧就自动生成一张截图、一份UI摘要、一条动作日志。用例结束时如果失败,框架会把完整证据包自动打包,包括动作序列、每一步的截图压缩包、UI摘要变化记录、设备侧系统日志和崩溃栈信息。我们在实际排查问题时发现,这个证据链的价值甚至高于LLM的失败分析本身。传统自动化失败时要靠人去复现,Agent模式下的执行本身就产出了足够详细的现场记录,人工排查时间能压缩到一个非常小的量级。
evidence/ 001_tap_login_button_ok.png 001_ui_snapshot_ok.txt 002_input_account_ok.png 002_ui_snapshot_ok.txt 003_tap_submit_fail.png 003_ui_snapshot_fail.txt crash.log device_sys.log5. 成本与并发:Agent 进入移动端测试规模化的两道坎
5.1 token成本拆解:一条用例究竟吃掉多少上下文
移动端自动化不像Web端那样可以用一个长文档把所有页面元素塞给模型。每个页面都需要实时感知,每次感知都意味着新的token消耗。我们做过一次粗略估算:一个30步操作的回归场景,假设每步需要UI摘要约300token,带上历史上下文约800token,模型回复约200token,单次操作就要消耗约1300token,整个场景跑完约4万token。再加Agent反思和重试,实际消耗翻一倍并不稀奇。
这个成本模型决定了,任何“每步都调用最强模型”的做法都不能规模化。降本手段有三个方向。第一是感知层降本,UI摘要里只保留模型决策真正需要的节点,能用规则过滤的就不进模型,能用文本OCR解决的就不上多模态。第二是上下文压缩,相同页面的UI摘要加哈希缓存,页面没变就直接复用,不重复生成。第三是模型分层,页面摘要、元素分类这类简单任务用开源小模型处理,只有规划、反思、失败分析这类高难度任务才调用大参数模型。
注意:成本优化目标不一定是最低单次消耗,而是让一整轮完整执行的总成本可控。单纯压低单步成本,如果导致重试率上升,整体成本反而更高。判断标准应该看“每百条用例有效通过所需的总token数”。
5.2 多设备并发的调度:Agent规划一次,设备执行N次
移动端测试的日常场景里,设备往往不是一个而是一批,真机集群、模拟器云平台动辄几十台。如果按“每台设备跑一个Agent实例”的粗放方式做并发,模型调用量会随设备数量线性膨胀,而且各设备执行结果不一致的概率也会放大。
更合理的思路是给Agent分层:Model层只做规划、反思、失败分析,Device层只做确定性执行。一个Agent在Host侧完成规划,把结构化脚本下发给一组设备,设备执行期间不依赖模型,只在失败时把本机证据包传回Host,由Agent解释失败原因并决定是生成修正脚本还是标记缺陷。这样模型调用量从“设备数乘步骤数”降为“规划一次加失败分析若干次”,整体成本下降一个数量级,执行一致性也大幅提升。
这套“Agent规划、设备执行、云端回溯”的分层设计,是通用Agent框架下沉到测试场景时最值得做的架构调整之一。它的本质是把大模型的“生成能力”从执行管线里剥离出来,让它只负责创造性部分,把重复性、确定性部分留给规则引擎。
6. 踩坑实录:通用 Agent 下沉到移动端测试时的五个典型问题
6.1 五类高频问题的现象与解法
| 问题现象 | 根因分析 | 我们的解法 |
|---|---|---|
| Agent 点击了系统弹窗的“允许”按钮 | 系统弹窗节点在UI树中可见,模型无法区分系统级和业务级控件 | 系统弹窗坐标黑名单,UI摘要中直接剔除 |
| Agent 被截图里的广告横幅带偏目标 | 广告区域视觉权重高,多模态模型易误判为业务内容 | 广告区域蒙版,UI摘要中剔除该区块 |
| 输入中文后页面状态迟迟不更新 | 第三方输入法覆盖了原生控件信息,UI树读取不完整 | 输入后强制隐藏键盘并等待层级稳定再取快照 |
| 页面轻微变化导致整页重新感知 | 没有页面指纹,每次操作后全量重新生成UI摘要 | 页面哈希缓存,命中后直接复用摘要 |
| 模型自评“成功”但实际断言失败 | LLM存在乐观偏差,倾向于在信息不足时判定成功 | 规则断言优先,失败证据必须由框架收集 |
这个表格里的坑,前两个都是典型的“模型看到了不应该看到的东西”。下沉改造的核心工作之一,就是在感知层帮模型做减法。系统弹窗、广告位、诱导浮层这些在通用对话场景里无关紧要的内容,在测试场景里就是必须屏蔽的噪声。我们在UI摘要生成阶段就把它们剥掉,模型根本没机会决策要不要点它们。
6.2 当前沉淀下来的参考架构与动手建议
经过这几轮改造,我们最后沉淀下来的参考结构大致呈四层:任务输入层负责把测试用例转成结构化剧本;规划层由LLM负责生成路径、反思异常、给出修复建议;校验层用规则引擎做元素匹配、断言执行、UI摘要缓存;执行层只负责调用设备侧的原子动作并采集证据。
一条用例的形态也随之变化,它从一段挺长的代码脚本变成一份声明式的YAML任务描述,配上规划器生成的执行计划,再配上全套证据链。设备上的用例描述大概是这种样子:
case_id: login_001 precondition: app_state: cold_start login_state: not_logged start_page: 登录页 goal: 完成账号密码登录并进入首页 assertions: - element: 首页用户名 condition: visible - element: 首页用户名文本 condition: text_equals 测试用户001 constraints: skip_system_dialog: true ad_area_blocked: true timeout: step_timeout: 10s case_timeout: 90s如果你正打算把通用Agent框架往移动端测试场景引,我的建议是先别急着上最复杂的场景。挑一条高频却稳定复现的登录用例,手工整理出它的剧本、动作空间、UI摘要格式和断言规则,把这条路走通。然后让Agent连续重复跑这同一条用例二十次,重点观察三个指标:规划成功率、重试率、模型自评与规则断言的一致率。这三个数字基本能反映出你的下沉改造做到什么程度了。
我个人在实际操作中的体会是,通用Agent框架下沉到移动端测试场景,最难的并不是把LLM接进来,而是把自由空间收窄到一个测试工程师愿意签字放行的范围。我们踩过最大的坑,从来不是模型能力不够,而是太相信模型自主判断。现在这套方案里,凡是跟最终结果相关的地方基本都是规则兜底,LLM负责生成路径、解释异常、补全描述。这个分工一旦清晰,落地速度反而快了很多。下一步我打算继续做的是把失败证据包和用例历史数据回流成更好的评估集,用来持续衡量Agent在测试场景下的真实退化,而不是只看它今天跑了多少通过率。