先说结论:通用Agent框架直接拿去跑移动端测试,十有八九会翻车。但把它拆开、给它套上约束、把任务边界重新定义之后,一套真正能落地的"LLM+移动端测试Agent"是完全可以搭出来的。这篇是这个系列里我最想聊的一篇,因为它涉及的问题不是"能不能自动化",而是"敢不敢交给LLM去决策"。我在这个方向上深耕了大半年,经历过模型把测试环境玩崩、跑一晚上崩八次的至暗时刻,也沉淀出了一些真正能抄的方案。
我会先把"通用Agent下沉到测试场景"这件事为什么值得做讲清楚,再给出一套我实际采用的架构拆解,然后详细复盘操作流程、Prompt设计、参数控制、问题排查。涉及的技术栈我都会用虚构代号和通用描述处理,但细节保证真实可复现,适合已经在做接口或Web自动化、正准备把LLM往移动端测试场景延伸的团队参考。
1. 通用Agent框架为什么值得下沉到测试场景
1.1 所谓"通用"的Agent,到底强在哪、缺什么
过去一年里,"通用Agent"这个方向被讲得非常火热。它本质上是一种"大模型决策循环":给定一个目标,模型自己拆解任务、选择工具、执行动作、观察结果,再决定下一步。开源社区里各种基于ReAct模式或规划-执行-反思模式写的Agent框架遍地都是,能力边界被宣传得特别宽。
但"通用"恰恰是最难落地的词。我做过一个类比:通用Agent像一位全科医生,你问他哪里不舒服,他全能给你开一轮基础检查建议,但真要到了专科手术台前,他必须变成外科医生才能动手。移动端自动化测试就是典型的"专科手术台"——它对动作边界、状态判定、结果可断言性有极其苛刻的要求,而这些约束是通用Agent框架默认不关心的。
通用框架通常默认Agent可以访问文件系统、执行任意命令、调用任意API,这对一个探索性任务没问题。但在测试场景里,一个模型如果自作主张去访问不该访问的路径、执行一个不该执行的删除命令,后果就是环境被污染、测试数据被清空、设备状态错乱。所以我们必须做的第一件事,不是把通用Agent能力拉满,而是把它"管住"。
1.2 测试场景给Agent设下了四道隐性门槛
通用Agent框架里跑得好好的任务,下沉到移动测试场景之后,至少会遇到四道它根本没想过的门槛。
第一是动作空间有限。移动端自动化能做的动作其实就那几类:点击、滑动、输入、等待、截图、获取页面层级、切换状态。通用框架里那种琳琅满目的工具列表,在这个场景里会被压缩成一个白名单。模型必须在受限的选项里做决策,这反而对Prompt约束能力提出了更高要求。
第二是状态必须可复现。测试的核心属性是可回归性,同一套用例在同一版本上跑十次,结果应该一致。通用Agent是概率性的,它可能这次选了这个元素、下次选了那个元素,这就需要我们在框架层面加"确定性闸门",把决策多样性控制在业务允许的范围内。
第三是结果必须可断言。通用Agent完成一个任务,返回一段自然语言描述"看起来完成了"就交差了。但测试Agent不行,它必须有明确的通过/失败信号,要有断言逻辑,要能把结果沉淀成结构化报告。
第四是集成成本。真实测试场景不是孤立的,要接入CI流水线、要出报告、要留日志、要在失败时抓到最关键的上下文。通用Agent默认不提供这套东西,下沉时必须全部自己补齐。
所以我的核心观点是:下沉不是简单地在移动自动化工具外面包一层LLM调用,而是要重新设计一个"受约束的Agent运行时",让模型在规则边界内发挥判断力。这个认知一旦建立,后面所有技术选型就都顺了。
2. 移动端自动化测试到底难在哪,Agent能帮上什么忙
2.1 传统移动端自动化绕不开的三个老大难问题
做移动端自动化测试的人,对下面三个痛点肯定都有切身体会。
元素定位脆弱是最磨人的。移动端的UI控件在层级变迁时,ID、xpath、资源名经常跟着变。今天能跑通的脚本,明天因为一个布局文件改了层级结构,整条用例就废了。为了应对这个问题,传统方案要么强制开发提供稳定的测试ID,要么用图像识别做兜底,但这两者都有维护成本。
脚本维护成本高是第二个老大难。一个中型App的用例可能上千条,界面改版一次,元素选择器、操作顺序、断言逻辑可能都要跟着改。传统脚本固化了"怎么做",但没记录"为什么这么做",所以一旦页面结构调整,维护者只能凭猜。
动态弹窗和权限请求是第三个麻烦。安卓和iOS上,权限弹窗、升级提示、运营活动弹窗出现的时机不可预测,传统脚本要专门写等待分支,写少了漏处理,写多了脚本臃肿。测试执行到一半被一个陌生弹窗打断,然后断言失败,这种事情做过移动端测试的都懂。
2.2 LLM和Agent框架真正能改变的关键环节
把这几个老大难逐一拆开,会发现它们的共同根因是"语义鸿沟":测试用例描述的是业务意图(我想进入支付页并验证余额显示),而传统脚本落地的是精确坐标和控件ID。LLM恰好能把这两层连接起来。
意图驱动是我认为最核心的转变。用自然语言描述"我要在这个页面上找到最后一个订单并进入详情",Agent自己去判断"最后一个订单"在页面层级里对应哪个节点,找不到时还能通过截图视觉信息辅助判断。这个能力直接把用例编写成本降了一个数量级。
智能探索让回归测试更贴近真实用户。传统脚本一定要走预设路径,Agent则可以在业务目标约束下自由选择路径,比如被弹窗挡住时,先关掉弹窗再继续。这样不仅把动态弹窗问题化解了,还能发现一些预设路径之外的异常情况。
语义断言也比传统断言更接近业务验证。传统断言写"TextView的文本等于'支付成功'"是不稳固的,碰到加粗字体、前后空格、国际化文案就挂了。Agent可以结合页面上下文做语义级判断:"当前页面是否呈现了支付成功的核心信息",只要页面语义成立就判定通过。
2.3 必须诚实面对Agent做不到的事情
聊Agent能做什么很容易上头,但作为实际落地的人,我得先泼一盆冷水。Agent不能保证100%确定性,这是概率模型的天性,我们能做的是把不确定性压缩到可控范围内。Agent也不是万能测试设计器,好的测试策略、边界值设计、业务风险判断仍然需要人来定。
更关键的是,Agent不能替代测试基建。它依赖稳定的设备管理、可靠的页面感知、清晰的返回信息。设备连接不稳定、页面层级解析失败、截图质量太差,这些底层问题都不是LLM能解决的。
所以我的定位是:Agent是放大器,不是替代者。它把人的精力从繁琐的脚本细节中释放出来,但测试策略和验收标准仍然掌握在测试工程师手里。抱着这个心态去搭方案,才不会在模型表现不稳定的时候心态崩溃。
3. 下沉方案:我把Agent框架拆成了三层
3.1 架构总览:调度层、执行层、感知层
我最终采用的是一套三层架构,每一层的职责边界非常清晰。
调度层是整个Agent的大脑,负责接收测试目标、拆解为子任务、维护当前状态历史、决定下一步动作、判定任务是否完成。这里只做决策,不碰具体设备操作。执行层是Agent的手,封装所有移动端操作能力,比如点击、输入、滑动、等待、截图、获取页面层级。执行层只接受白名单内的动作指令,并把执行结果转成结构化反馈。感知层是Agent的眼睛,负责把屏幕状态变成LLM能理解的信息,比如把UI层级压缩成精简文本描述,把截图处理成合适尺寸,把页面状态抽象成上下文摘要。
这三层对应到生活里,就像一个施工项目里的项目经理、工人和测量员。项目经理只管下达"下一步做什么"的指令,工人只负责按指令干活,测量员负责把现场情况汇报上去。职责不混淆,问题排查时才不会互相甩锅。
3.2 技术选型:为什么我没有直接套用现成的通用Agent框架
在选型阶段,我认真对比过两条路线:直接用现成的通用Agent框架,还是基于轻量ReAct循环自己封装。我把决策因素列成了一张表:
| 对比维度 | 通用Agent框架直用 | 自研轻量Agent |
|---|---|---|
| 功能丰富度 | 高,工具生态多 | 低,但够用 |
| 场景约束能力 | 弱,默认动作空间太大 | 强,可控边界清晰 |
| 工程集成成本 | 高,需要大量改造 | 低,能贴合现有测试体系 |
| 稳定性表现 | 一般,行为随机性大 | 可控,可通过闸门收敛 |
| 团队学习成本 | 中,要理解框架内部机制 | 低,逻辑直白 |
| 长期维护性 | 依赖上游框架更新 | 自持,可随时调整 |
数据上看,通用框架直用似乎赢在功能丰富,但我的核心诉求不是"功能多",而是"行为可控"。我最终选择了一个折中方案:读取通用框架的ReAct循环思想,但没有直接依赖框架本身,而是用主流的LLM接口和结构化输出能力,自己实现了一个不到800行的调度器。做完这个决定之后,整个项目的稳定性提升了一个台阶。
为什么会这样?因为通用框架的定位是开阔地作战,而测试场景需要的是巷战。巷战需要的不是武器多,而是对每条巷子的熟悉程度。自己封装,我才能精确控制模型每次决策看到的上下文、可选动作、输出格式和约束条件。
3.3 让Agent"守规矩"的四条硬约束
下沉方案能不能成,关键不在于模型多聪明,而在于约束有没有设计到位。我在框架里落地了四条硬约束,每一条都是踩坑踩出来的。
动作白名单是第一道防线。所有可执行指令必须提前注册,比如tap、input_text、swipe、wait、screenshot、get_hierarchy、assert_semantic。模型输出指令时,调度器先做合法性校验,不在名单里的一律拒绝,然后要求模型重新生成。这能挡住绝大多数"模型突发奇想"的乱操作。
步骤上限是第二道防线。每个任务最多执行N步,超过上限直接判定失败并输出当前状态摘要。这一步主要是防止模型陷入死循环,比如一直在某个弹窗面前反复点击却无法关闭,没有步数上限的话,一次任务的Token消耗能把你预算烧穿。
确定性开关是第三道防线。对于回归测试场景,我会把模型温度调到接近0,同时在一些标准分支里让模型输出"选项编号"而不是自由文本,减少随机性。
可撤销机制是第四道防线。Agent做关键操作之前,会先记录当前页面指纹。如果操作后状态异常,比如页面消失了、关键元素找不到了,Agent会自动回到上一个稳定状态或执行恢复动作。没有这套机制,模型的一次误判可能会让后续所有用例失去意义。
这四条约束装完之后,Agent的行为才从"自由发挥"变成"戴着镣铐跳舞",而恰恰是这副镣铐,让测试场景真正敢用LLM的决策能力。
4. 实操复盘:一套移动端测试Agent从0到1
4.1 环境准备与工具链
先说准备阶段需要的东西。设备侧我使用了Android模拟器和一台真机做交叉验证,Android调试桥(ADB)负责设备连接和应用安装管理。UI感知层面使用了一个开源移动端UI自动化框架,它基于标准WebDriver协议,能拿到页面层级和截图,也能执行点击、滑动、输入等基础动作。
LLM侧,我在两个方案中切换:一个是商用大模型API,质量高但按Token计费;一个是开源本地模型,免费但决策质量低一些。实际使用中,我让调度层可以配置模型来源,重逻辑任务用商用API,轻量判断比如"当前页面有没有权限弹窗"这类二分类问题,就用本地小模型批量处理,省钱且不影响整体效果。
语言层面用Python编写调度器,这是最顺手的选择,生态里有大量的自动化测试库可以无缝衔接。整个项目代码量不大,核心调度循环加工具注册和状态管理大约两千行。这个量级意味着团队内任何人都有能力维护和扩展。
4.2 核心流程:任务解析、页面感知、决策循环、断言验证
核心流程分四个阶段。我先把主循环以伪代码形式贴出来,再逐段解释:
def run_test_agent(task: str, device: Device, max_steps: int = 30): # 1. 任务解析 steps = planner.parse(task) state = TaskState(task, steps, device) while not state.is_finished(): if state.step_count() > max_steps: return TestResult.FAIL, "step limit exceeded" # 2. 页面感知 screen = Perception.capture(device) # hierarchy + screenshot # 3. 决策:LLM根据当前状态选择动作 action = scheduler.decide(state.history(), screen) # 4. 动作校验与执行 if not ActionGuard.valid(action): state.record_error("invalid action") continue execution_result = ActionExecutor.run(device, action) # 5. 观察与断言 state.update(action, execution_result, screen) if AssertionEngine.check(state, task): return TestResult.PASS, state.summary() sleep(ACTION_INTERVAL) return TestResult.FAIL, state.summary()任务解析阶段,模型把自然语言目标拆成若干验证点,比如"进入设置页并验证飞行模式开关存在"会被拆成"打开设置页、定位飞行模式开关、验证其可见性"。这个拆解动作不是一次性完成的,每步执行之后会结合真实页面上下文进行修正。
页面感知阶段是最需要抠细节的。我最初直接把原始UI层级XML丢给模型,结果Token消耗巨大,响应也慢。后来做了一次重要改造:先解析层级树,过滤掉不可见节点和冗余节点,再把每个控件按"位置、类型、文本、关键属性"转成精简文本,最后配合一张缩放后的截图一起交给模型。这个组合方式让感知质量提升非常明显。
决策循环阶段,调度器把"任务目标、历史动作、当前页面描述、可选动作列表"组装成Prompt,模型输出一个结构化JSON动作,经过校验后执行。这里有一个重要的工程细节:历史记录不能无限膨胀,我只保留最近五步动作和最近两步页面摘要,早期不重要的信息逐渐滑出上下文窗口,既省Token又避免模型注意力分散。
断言验证阶段,执行完一个动作后,Agent会主动判断当前页面是否已经达成某个验证点。这个判断用语义方式完成,比如验证"注册成功"不再是检查某个控件文本精确相等,而是让模型判断页面上是否出现了注册成功的关键语义信息,并且该信息处于可见状态。断言结果会沉淀为结构化数据。
4.3 Prompt与参数设计的核心细节
Prompt设计是这个项目成败的关键,我总结出三个层级的写法。
系统提示词里固定放三块内容:角色定义、动作空间声明、行为红线。角色定义是"你是一名移动端测试自动化Agent,你的任务是验证业务功能是否符合预期"。动作空间声明要列出所有可用的动作和每个动作的参数含义。行为红线要写明几个禁止事项,比如不得访问无关系统资源、不得执行未在动作列表中的操作、遇到权限弹窗时必须先处理弹窗再继续。
少样本示例我放了四组:权限弹窗处理、登录页面输入、常见加载等待、页面切换。每组示例都展示了"页面描述+模型决策JSON+观察反馈"的完整链路,让模型学会的是决策模式,而不只是动作本身。实测下来,加不加这四组示例,弹窗场景的成功率能差出30个百分点。
输出格式我用严格的JSON约束,要求模型每次只输出一个actions列表,每个动作包含action、target、parameters、reason四个字段。reason字段是让模型解释为什么选择这个动作,这个设计很关键,它不仅能提高决策质量,还能在失败时给排查者提供决策依据。
模型参数方面,temperature设为0到0.2之间,测试场景需要可复现性,过高的温度会让模型偶尔"灵光一闪",但在回归测试里这种灵光一闪只会带来不确定性。max_tokens设得够用即可,因为动作输出本来就是短JSON;但反映页面页码时不受此限制,我会分开设置。请求超时设定在三秒左右,超过就重试一次,重试失败就切换备用模型,避免因单次接口抖动卡死整个任务。
4.4 成本与性能数据:一次典型流程消耗多少资源
行动派最关心的还是成本。我把一个中型流程做了拆解:一个完整用例包含8个步骤左右,平均每步需要两三次LLM调用(页面感知辅助判断一次、决策一次、断言验证一次),再加上任务解析和最后汇总,单条用例大约消耗25到35次LLM请求。
Token消耗上,商用API情况下,单条用例大约烧掉几千Token,折算下来一次回归跑几百条用例的成本依然可控,属于绝大多数团队能接受的量级。本地小模型的引入则进一步降低了成本,特别适合处理"页面是否出现某元素"这类高频低难度判断。
性能上最大的瓶颈其实不在模型推理速度,而在页面感知阶段。一次完整的页面层级抓取和解析可能耗时几百毫秒到一两秒不等,这比LLM调用还慢。我做的优化是让感知结果带缓存:同一页面在短时间内重复出现时,直接复用上一次的层级描述。这个缓存机制大约省掉了三成感知开销。
我还把"页面指纹"也纳入了缓存逻辑。所谓页面指纹,就是页面结构加关键文本的一个哈希值。每次执行动作后先算指纹,如果指纹和上一次相同,说明页面没有实质变化,这个信息会直接反馈给模型,"你的操作没有引起页面变化,请尝试其它动作",省去了大量无效的重复探索。
5. 踩坑实录与稳定性优化
5.1 高频问题速查表:我从项目里整理出的真实故障
这不是理论列举,每一个问题都是我实际遇到过、排查过、修完再也没复发的血泪经验。
| 问题现象 | 根本原因 | 解决方案 | 经验备注 |
|---|---|---|---|
| Agent反复点击同一个无效区域 | 页面感知层级被过滤得过狠,模型看不到可交互目标 | 保留可见且可点击的节点,过滤规则增加交互属性判断 | 过滤规则要经常回归验证 |
| 动作执行了但页面没变化,随后陷入死循环 | 操作生效存在延迟,模型观察窗口太短 | 动作后强制等待稳定,再计算页面指纹对比 | 等待时间要做成可配置项 |
| 权限弹窗有时能处理有时处理不了 | Prompt里示例不足,弹窗文案变体太多 | 把弹窗处理抽成独立子任务,优先识别再决策 | 弹窗可以单独做一个小模型二分类 |
| 模型输出JSON格式偶尔出错,导致解析失败 | 输出约束不够严格 | 增加一次语法校验,失败时把错误信息回灌给模型重新生成 | 最多重试两次,防止恶性循环 |
| 长流程中段上下文被撑爆,模型"失忆" | 历史记录无限堆积 | 只保留最近5步动作和最近2帧页面摘要 | 必要时用早期断言结果代替完整页面描述 |
| 商用API偶发超时,整个任务卡死 | 缺少超时和重试机制 | 请求设置超时,超时自动切换备用模型 | 备用模型质量稍降但保证流程不中断 |
| 设备状态跳变导致截图失败 | 设备休眠或锁屏 | 执行前唤醒、关闭自动锁屏,失败后重试一次 | 这一步要写进Agent的初始化流程里 |
除了表格里的问题,还有一个高频坑是页面层级解析失败。移动端的层级树在某些动态渲染页面里会膨胀到几千个节点,直接序列化整个树既慢又费Token。我的做法是只抓取可视区域的树,过滤掉display为none和不可见节点,并把坐标归一化到百分比。经过这些处理后,层级文本体积能缩小十分之一以上,模型理解准确率反而更高。
5.2 稳定性的三道防线:操作前校验、操作后验证、失败自动回退
稳定性是测试Agent的生命线。我在实践中慢慢总结出三道防线,缺任何一道,线上跑起来都会出幺蛾子。
第一道防线是操作前校验。动作不能只做白名单检查,还要做人机共判:这个动作在当前页面状态下是否合理。比如模型决策"点击设置按钮",但当前页面层级里根本没有设置按钮,这个动作就应该在进执行器之前就被拦下来。我把这个校验做成独立逻辑,不依赖模型自身判断,因为模型有时候会基于历史记忆操作一个已经不存在的界面。
第二道防线是操作后验证。每个动作执行完后,调度器不会盲目相信执行器返回的"成功",而是重新感知页面状态,对比页面指纹是否发生了变化。如果模型说点击了某个元素,但页面指纹完全没变,系统就会给模型返回一个"页面未变化"的反馈,引导模型修正策略。
第三道防线是失败自动回退。一旦连续两次操作都失败或触发异常预警,Agent会执行一个预置的恢复例程:回到首页、清空后台任务、重新进入目标页面。如果恢复例程也失败,才会把用例标记为失败并保留完整现场日志。
这三道防线让整体稳定性从最初的不到五成提升到八成以上,其中关键业务路径的稳定率已经接近九成。剩下的失败大多来自设备自身问题,比如网络切换、系统级弹窗,这类问题我也接受了它们的存在,因为真实手工测试也避免不了这些干扰。
5.3 关于效率和预算的几条实战建议
如果你准备在自己团队落地这套方案,我给你几条基于实战的预算建议。
第一,把用例按复杂度分档。简单用例只做"到达某个页面并断言关键元素存在",中等用例做"完整走一条业务路径",复杂用例才涉及数据构造和状态依赖。分档出准确的价格预期,也方便规划模型调用策略。
第二,大量引入本地小模型做前置筛选。很多页面感知类的判断其实不需要大模型出手,用一个表现稳定的本地小模型先做一遍粗筛,只有小模型判断"情况异常"时才升级到大模型重新审视。这个机制能让整体成本再降三成左右。
第三,一定做一个"调用审计日志"。每一次LLM请求的用户Prompt、模型输出、耗时、Token数都记录下来。不要小看这个日志,它不仅能帮你定位成本黑洞,还能在模型表现异常时回放当时的决策过程。有一次模型反复走错页面,我翻审计日志才发现是页面描述字段里混进了上一个页面的缓存数据。
几点个人体会
这套移动端测试Agent下沉方案,从最开始被所有人质疑"不就是调个LLM吗",到现在能稳定承担一部分回归工作,中间隔着的不是模型能力,而是工程细节。我对想跟进这个方向的团队的忠告是:不要一上来就想做全自动大而全的平台,先挑一个最痛、最重复、失败成本最低的用例类型,把它跑稳,再逐步扩展。
最后分享一个小技巧:每次用例执行结束时,把"结束时的页面指纹 + 断言结果 + 模型决策摘要"存成一条快照记录。后续版本回归时,如果同一条用例失败,先对比快照,往往能立刻判定是"业务改动导致"还是"Agent决策异常",这个不起眼的习惯让排查效率翻倍。
移动端自动化的大趋势一定会从脚本时代走向意图时代,但中间这段路需要大量工程化补全。希望这篇分享能帮你少踩一些我踩过的坑,稳稳走完下沉的最后一公里。