移动端自动化做了这么多年,我越来越觉得当前测试体系真正短缺的不是执行框架,而是"决策层"。Appium、XCUITest 这类工具把"手"练得很强壮,但每一次点击、滑动、断言,还是要人肉提前写进脚本。页面上一个按钮挪了位置,脚本就废了;弹窗出现时机稍有变化,assert 就红了。而所谓 "LLM 驱动的自动化测试",我和团队同事最近一段时间集中尝试的方向,不是拿大模型去生成一堆一次性测试代码,而是把一套通用的 Agent 框架直接下沉到移动端测试场景里,让 Agent 自己看屏幕、自己做决策、自己调用工具执行并验证结果。
这篇文章是这个系列的第 9 篇,也是移动端自动化子专题的第 5 篇。前面几篇聊过元素定位、设备调度和脚本稳定性,这篇重点换个视角:当一个通用 Agent(能推理、能规划、能调用工具的那套东西)落到移动测试任务中,它和传统 UI 自动化碰撞之后,会冒出哪些新问题,又该怎么设计整套调用关系。如果你正在纠结"LLM 到底能在测试里干什么",或者已经试过用大模型生成测试脚本但效果一般,这篇文章应该能给你一些不一样的思路。
1. 先搞清楚:通用 Agent 框架到底"通用"在哪
1.1 Agent 不是聊天机器人,而是一套"观察-决策-执行"的闭环
很多人一听到 Agent 就想到对话框,其实那是最浅层的用法。真正能下沉到测试领域的 Agent,是一个可以持续运转的决策循环:接收任务描述,读取当前环境状态,由大模型推理下一步动作,调用对应工具执行,再观察执行结果,调整计划,直到任务完成或触发终止条件。
这个循环对应到测试工作上,其实就是测试执行的核心逻辑:打开页面、看清界面、决定下一步操作、执行操作、验证结果。传统自动化测试把这套逻辑用代码写死,而 Agent 把这套逻辑交给模型在运行时动态生成。它不需要事先定义从 A 到 B 的每一条路径,只需要给它一个目标和一组工具,它自己会绕开弹窗、处理异常、寻找替代入口。
用个生活化的类比:传统脚本像一个严格按照剧本演戏的演员,台词动作一个不能差,舞台布景一变动就演不下去;Agent 更像一个任务导向的临时工,你跟他说"去把那个文件找出来",他自己会看柜子标签、拉开抽屉、翻翻桌面,找不到还会换个思路。移动端测试里页面状态千变万化,恰恰需要这种灵活性。
1.2 移动端测试为什么是 Agent 下沉的好场景
移动端 App 的界面层级复杂,远不是 Web 页面能比的。一个页面可能同时存在原生控件、H5 容器、小程序页面,还有各种动态弹出的授权框、更新提示、运营弹窗。控件树动不动几千个节点,而且不同机型、不同系统版本渲染结果还不一样。
传统自动化在这一层非常吃力,每个弹窗都要写专门的兜底逻辑,每条业务路径都要覆盖大量分支。而 Agent 框架天然擅长处理这种"环境不确定"的任务:它理解"当前页面上有弹窗,应该先关掉再继续"这种常识,不需要你把每个弹窗规则都写死。
另外,移动端测试断言也比 Web 复杂。Web 里经常断言一个元素可见、一段文本存在,移动端却经常要验证"登录状态是否真的写入本地""这个页面的某个组件到底加载完没有"。Agent 可以通过组合工具完成多维度验证,比如截图找关键元素、读取控件树、调本地存储接口确认数据状态,这些在传统脚本里往往要写不少胶水代码。
这里要澄清一个容易误会的地方:Agent 下沉到移动测试,不是要完全取代现有自动化框架。它更合适的定位,是接管那些"路径不固定、页面易变化、需要探索"的测试任务,比如冒烟测试、兼容性探索测试、异常路径回归。固定主路径上的高频回归,用确定性脚本仍然更稳、更快、更便宜。
2. 下沉的第一步:把 Driver API 翻译成"任务友好"的工具集
2.1 直接暴露 Element 操作接口,Agent 根本用不好
Agent 要操作手机界面,绕不开底层驱动,但直接把 Appium 的find_element、click、send_keys这些接口暴露给大模型,效果会非常差。原因很简单:这些接口要求的入参(比如定位器、元素属性)是"机器视角"的东西,而大模型擅长的是"意图表达"。你跟它说"点击登录按钮",它很容易找出文案是"登 录"的那个控件;但你让它填一个xpath=//android.widget.TextView[@text='登录'],它反而容易在长长的层级结构里迷路。
所以第一步要做的是工具面的重新封装,把底层 Driver 能力抽成一组"语义化工具"。我在实际项目中常用的工具集大致是这样:
| 工具名 | 输入参数 | 作用 |
|---|---|---|
ui_launch_app | app_name, 可选参数 | 启动或恢复应用 |
ui_observe_page | 无 | 获取当前页面控件树摘要和截图 |
ui_click | 目标描述(如"登录按钮""个人中心入口") | 点击某个界面元素 |
ui_input | 输入框描述, 文本内容 | 在指定输入框中输入内容 |
ui_swipe | 方向, 距离 | 滑动页面 |
ui_assert | 条件描述(如"页面出现登录成功提示") | 验证界面状态 |
ui_wait_for | 描述, 超时时间 | 等待某个条件满足 |
get_local_state | 键名 | 读取 App 本地存储或进程内状态 |
screen_record | 时长 | 录屏留证 |
每个工具的参数都尽量是自然语言描述,Agent 只需要说清楚"想做什么",工具内部再把这些描述翻译成真正的定位策略和执行动作。这就是"语义化工具层"。
另外一个关键设计是,工具内部必须有护栏。比如ui_click收到的目标描述如果无法唯一对应控件,不能硬点,要返回一个明确的状态码,告诉 Agent"有多个候选"或者"找不到目标",并附上候选列表。否则 Agent 会在错误的方向上越走越远,连续误操作。
2.2 状态观察:把整棵控件树压缩成 Agent 能消化的页面摘要
Agent 做决策依赖对当前页面的理解,因此每次循环都要把页面状态喂给模型。但直接丢原始控件树是不行的,一个深层次页面可能有一千多个节点,全部转成文本会轻松打爆上下文窗口,还会让模型抓不住重点。
我的做法是分层处理,先做节点压缩。过滤掉大量纯布局容器、无文本无 id 的 ViewGroup,只保留可交互节点和带关键文本、content-desc、resource-id 的节点。然后做层级拍平,把多级嵌套转成一串扁平描述,每个节点给一个短编号,方便后续工具引用。压缩之后,一般一个页面从几千节点降到几十个关键节点。
截图同样重要,但不必每次都传给模型。有些控件树描述不了的场景(比如纯图标按钮、Canvas 绘制区域),必须靠视觉。我采用的策略是"按需截图":常规页面只传控件树摘要;Agent 发出特定请求,或控件树里找不到可匹配项时,才附带截图让多模态模型辅助识别。这样既控制成本,又保留视觉兜底能力。
页面摘要给出来后,Agent 的"观察"就变成了一个高密度的环境描述,它能看到页面有什么、可操作什么、当前状态是什么。观察质量直接决定决策质量,这部分值得花大力气优化。
2.3 会话与记忆管理:Agent 上下文需要做"分段保存"
移动端测试任务往往不是单步操作,而是一个连续流程。Agent 需要记住自己已经做过什么、当前进行到哪一步、页面上有哪些历史状态。但大模型的上下文窗口不是无限的,而且塞太多历史信息也会干扰判断。
我建议把记忆拆成三层:短期工作记忆只保留最近几轮操作轨迹,用于理解当前进度;中期任务记忆保存本任务的关键中间结果,比如已经验证过的接口、已经出现过的异常;长期知识库则以提示词或检索库形式保存项目特有的业务规则和页面特征。这三层分别消费不同的上下文预算,既保证模型决策所需的最小信息量,又避免上下文爆炸。
实际操作中,我会把每轮 Agent 的执行轨迹按固定格式写入结构化日志,这样即使模型自身短期记忆被压缩,外部记录也能在任务中断后帮助恢复现场。后面排查问题,这份日志就是第一手证据。
3. 核心实操:把通用 Agent 跑在一个真实移动测试任务上
3.1 一个典型场景:登录流程自动探索回归
空谈抽象设计不够,我们来看一个具体任务。假设要对一个模拟 App 做主路径回归,目标是验证"未登录 -> 登录 -> 登录成功 -> 退出登录"这条链路是否正常。传统脚本大概会写四五十行定位代码,遇到弹窗再加条件分支。用 Agent 做,任务描述可能就一句话:
任务:验证登录流程 步骤要求:如果当前未登录,使用测试账号完成登录,登录后确认首页加载成功,最后退出登录,确认回到未登录状态。所有关键节点截图留证。接下来 Agent 会自主执行,过程中的推理轨迹大致长这样:
[Action] ui_launch_app(app_name="mock_app") [Observation] 应用启动完成,当前处于首页,界面右上角显示"我的"入口 [Action] ui_click(target="我的入口按钮") [Observation] 页面跳转到个人中心,检测到"未登录"提示卡片,并出现"立即登录"按钮 [Action] ui_click(target="立即登录按钮") [Observation] 进入登录页,存在手机号输入框、密码输入框和"登录"提交按钮 [Action] ui_input(target="手机号输入框", text="13800138000") [Action] ui_input(target="密码输入框", text="test123456") [Action] ui_click(target="登录按钮") [Observation] 请求已提交,当前页面出现 loading 状态 [Action] ui_wait_for(condition="首页加载完成", timeout=10) [Observation] 条件满足,已回到首页,右上角显示用户头像 [Action] ui_assert(condition="本地登录状态为已登录") [Observation] 断言通过,本地存储包含有效会话字段 [Action] ui_click(target="用户头像") [Action] ui_click(target="退出登录") [Action] ui_assert(condition="回到未登录状态") [Observation] 断言通过,登录流程回归完成,共耗时 42 秒这段轨迹里,Agent 没有预先给它任何 xpath 或资源 id,它完全通过页面摘要理解当前状态,自己决定下一步。中间如果弹出更新提示,它会判断这不是当前任务要素,自动找关闭按钮绕过;如果登录按钮文案变成了"登 录",它也能通过模糊匹配和上下文理解找到正确控件。
3.2 定位兜底:意图定位与确定性定位的混合策略
有人会问:让 Agent 完全自由定位,会不会不可控?我的答案是:不要让它完全自由,而是要做"混合定位策略",分三层兜底:
第一层是确定性优先。工程里维护一份常见关键控件的稳定标识映射表(例如资源 id、全局唯一 testID),Agent 在拿到目标描述后,优先查映射表。如果映射表命中,直接走传统定位,速度和稳定性都有保障。
第二层是意图推理。映射表没有命中,就交给模型结合压缩后的控件树和页面截图做判断。模型把"登录按钮"这个意图和当前页面里的候选节点做匹配,选出最可能的一个。这一层能覆盖大量页面结构变化。
第三层是执行后验证。点击或输入动作完成后,Agent 必须验证页面状态是否真的变了。如果动作执行了但状态没变化,不能当作成功,要触发重试或换一种方式实现目标。例如点登录没反应,可能弹出键盘遮挡了按钮,这时就先收起键盘再点。
这套策略的好处是兼顾稳定性与灵活性。确定性映射保证了高频路径的可靠执行,意图推理覆盖了页面频繁变动的场景,执行后验证则防止 Agent"自欺欺人"地把操作完成当作任务完成。
3.3 断言设计:界面验证之外还要验证真实状态
移动端测试最容易被忽略的是断言深度。从上面的轨迹可以看到,Agent 不只验证"首页加载完成"这种界面文本,还通过get_local_state去读取本地存储,确认登录态真的写入成功。这是移动端场景下非常重要的设计。
很多 App 的界面渲染和实际数据状态并不同步。登录页面跳转了,但这可能是因为前端缓存导致的假象;退出按钮不见了,不一定是真的退出了,可能是某个网络请求还在途。如果 Agent 只依赖视觉文本,很容易得出错误结论。
所以我在工具集里专门设计了"状态验证"类工具,用来读取应用本地数据库、SharedPreferences、文件系统中的关键字段,或者通过测试后门接口查询服务端会话状态。Agent 在做关键断言时,必须同时做界面级和数据级验证,两者都通过才算断言成功。这个习惯能挡住不少"看着绿了,实际是假绿"的测试结果。
4. 踩坑实录:Agent 下沉测试场景的五个经典问题
4.1 模型误触:点错按钮、乱操作
大模型不是测试专家,它在理解页面时会犯错。最典型的是"意图正确但目标选错",比如界面上既有"登录"又有"登录遇到问题",模型可能选错。还有更让人头疼的:某些页面布局里按钮层次较深,模型把"返回箭头"当成"关闭按钮"。
我的处理办法是在工具内部加操作校验规则,比如点击前先检查目标控件是否真正可见、是否在可点击区域内、是否被其他弹窗遮挡。另外,在页面摘要中给关键操作区加标注,把"主导航区域""弹窗区域""底部 Tab 栏"区分开来,引导模型选对范围。如果模型连续两次选择同一目标但页面状态没有变化,我会让工具返回一个警告,提示"可能选错目标",强制它换策略。
4.2 上下文爆炸与 token 成本失控
Agent 跑的时间越长,观测日志越多,上下文就越大。有一次我跑一个 20 分钟的长链路任务,最后模型开始把以前的操作记录当成当前页面状态来推理,明显是上下文被历史信息污染了。
解决思路是前面提到的分层记忆。同时,对页面摘要本身也要做缓存:相同页面重复出现时,不需要重新把完整控件树喂给模型,只传页面标识和变化部分。这一步优化能把 token 消耗降到原来的三分之一左右。启动任务前,还会先做"上下文预算"检查,如果预判会超过窗口上限,就提前把历史轨迹固化到外部存储,只保留几个关键摘要。
4.3 断言只看"看起来正常",漏掉真实故障
Agent 执行操作后,我最早踩的一个坑是:它点完"登录",看到页面跳转就报告成功,但后来人工复测发现登录请求其实失败了,跳转只是因为前端路由提前切换。这提醒我,Agent 的"观察"不一定等于"事实"。
现在我对关键断言有一个强制性约定:凡是涉及登录态、支付状态、数据提交这类核心动作,Agent 必须调用状态验证工具去后端或本地存储确认,不能只看界面。遇到网络超时这类常见异常,还必须区分是环境问题还是产品 bug,环境问题要自动重试,产品 bug 才标记失败。
4.4 重试死循环:同一个错误动作反复执行
模型有时会陷入死循环,尤其在遇到它不理解的弹窗时,可能尝试关闭弹窗 -> 失败 -> 再关闭 -> 再失败,同一个动作重复十几遍。这种循环不仅浪费时间,还会让测试报告失去参考价值。
两个手段配合使用:第一,在 Agent 循环里加"动作去重"机制,记录最近 N 步执行过的动作和对应页面状态,如果页面状态未变化且动作重复,就判定为无效操作,强制 Agent 换一种方式;第二,设置总步数上限(一般是 30 到 50 步),超出后自动终止任务并标记为"未完成+疑似环境阻塞"。宁可让任务失败,也不能让它无意义地空转。
4.5 多设备并行时的会话隔离问题
移动端测试很少只跑一台设备,Agent 用在多设备上,最大的问题是会话串线。如果多个 Agent 共用一个模型上下文,A 设备的页面状态可能被 B 设备的操作污染,导致定位错乱。
我的方案是一个设备绑定一个独立的 Agent 实例,上下文完全隔离。模型服务本身可以共享,但要做并发控制和独立 session 标记。设备资源调度用队列管理,每台设备上的任务复用一个常驻 Agent 进程,避免频繁创建销毁带来的额外开销。
下面整理成一份排查速查表,方便大家对照:
| 问题 | 典型现象 | 核心对策 |
|---|---|---|
| 模型误触 | 点了错误按钮、关闭了不该关的弹窗 | 工具层做可见性/遮挡校验,标注主要操作区域 |
| 上下文爆炸 | 模型把历史日志当页面状态 | 分层记忆,缓存页面摘要,控制 token 预算 |
| 断言假绿 | 界面看起来正常,实际数据未写入 | 关键断言必须调用状态验证工具,双重确认 |
| 重试死循环 | 同一动作反复执行,页面无变化 | 动作去重 + 最大步数上限 + 强制换策略 |
| 会话串线 | 多设备状态相互污染 | 设备绑定独立 Agent 实例,上下文隔离 |
5. 效果衡量与适用范围:别把 Agent 测试捧上天
5.1 怎么衡量 Agent 测试到底有没有用
团队里引入 Agent 测试后,最常被问的是:它到底比传统自动化强在哪里?我需要一组能说服自己的指标。我目前主要看四个数字:一是任务完成率,在一批固定测试任务中,Agent 自主完成且断言正确的比例;二是平均执行时长,尤其关注长链路任务是否能控制在可接受延迟内;三是脚本维护人天,也就是页面改版后需要人工修改脚本的工作量;四是单任务平均工具调用次数,这个数字直接对应 token 成本和执行稳定性。
从我们自己体感看,任务完成率从最初的不到五成,经过工具集优化和页面摘要改进,能稳定到八成以上。单独看这个数字可能还比不上精心维护的传统脚本,但脚本维护人天下降得非常明显,页面结构小幅变动时,Agent 几乎不需要干预。
真正好用的不是"全 Agent 化"或"全脚本化",而是两者混用:主路径、高频回归用确定性脚本保证速度和稳定;探索性测试、异常路径、快速冒烟交给 Agent 处理。把 Agent 当成团队的探索型测试工程师,而不是替代全部自动化,它的价值才最容易呈现。
5.2 三个不适合用 Agent 的场景
Agent 不是万能的,我实际用下来有三类场景明确不会交给它。一是毫秒级性能断言,Agent 的决策和执行链路本身有延迟,无法精确测量页面渲染耗时;二是像素级视觉回归,这类任务需要精确到像素的对比,大模型的"大概像"判断达不到要求;三是强依赖外部服务且服务状态不可控的流程,Agent 遇到外部超时会反复试探,浪费大量时间。
另外还要提醒一点:Agent 并不是越聪明越好,它只是个决策大脑,真正干活还是靠底层工具链。如果底层驱动不稳定、页面摘要质量差,换再强的模型也补不回来。所以要对整套链路分层做稳定性建设,决策层、工具层、设备层都有各自的性能和失败处理机制,整条链路才扛得住真实业务压力。
我自己的经验是,Agent 下沉到测试场景,最大的价值不是省掉了写脚本的人,而是把测试思维从"代码实现"提升到了"目标管理"层面。测试人员不再需要关心每个控件怎么定位,而是把精力放在设计更合理的测试任务、更严谨的验证标准、更完整的业务覆盖上。至于 Agent 具体怎么点、怎么滑,那是工具层的事,不该占用测试设计者的注意力。
如果你们团队正准备上手这个方向,我给的建议是:别一上来就想着端到端全部跑通。先选一条最关键的业务路径,把工具集和页面摘要打磨顺,跑出第一批可信的结果,再逐步扩大范围。过程中一定要保留完整的执行轨迹日志,那是后续定位所有问题的基础。踩过几次坑之后你会和我有同样的感受:通用 Agent 框架下沉到移动测试,拼的不是模型有多聪明,而是你有没有想清楚怎么让它和现有的测试工程平稳共处。