这个系列写到第九篇,前面我们已经把LLM驱动的自动化测试从桌面端一路聊到了移动端:元素定位、用例生成、执行器、结果分析,该有的零件基本都有了。于是这一篇顺理成章地想到一个方向——把通用的Agent框架直接搬过来,让它自己规划、自己执行、自己汇报。移动端自动化这一摊子事,是不是就能彻底交给一个"大脑"了。
我抱着这个想法把一套社区常见的通用Agent框架接进移动端测试场景,跑了一轮之后发现,结论有点反直觉:通用Agent框架越"通用",直接拿来跑测试就越别扭。不是模型能力不行,也不是框架本身不好,而是测试这种任务对可重复、可解释、可断言的要求,和通用Agent默认那种"开放任务自由发挥"的交互模型,天然不在一个频道。这篇就把下沉改造过程中的关键决策、踩过的坑和最后沉淀下来的方案,完整拆给你看。
1. 通用 Agent 下沉的第一个错觉:能规划不代表能测试
1.1 任务形态不同:开放问答 vs 闭合验收
通用Agent的典型任务是"帮我整理一下这份会议纪要""把搜索结果汇总成三条要点"——这类任务有一个共同点:没有唯一正确答案,只要结果看起来合理就算成功。测试不一样。测试的每一个步骤都对应着明确的预期:点击登录按钮之后,要么进入首页,要么出现错误提示,不存在"差不多看起来像进入了首页"这种模糊地带。
我们最开始把通用Agent框架接进模拟项目X的移动端回归时,直接用自然语言描述目标:"验证登录流程正常"。框架确实会自己去点输入框、填账号、点下一步,但问题很快暴露:它会因为界面上的一条偶然提示文案,就自作主张地判断登录失败,甚至偶尔会把"登录成功"和"注册成功"两个页面混淆。后来我们翻日志分析,发现不是模型不认字,而是它在用开放语义空间的方式处理测试任务——把测试当成"理解题"做,但测试本质上是一道"判断题"。
通用任务和测试任务的核心差异,可以这么看:
| 维度 | 通用Agent任务 | 测试任务 |
|---|---|---|
| 答案形态 | 开放、多元、可协商 | 闭合、唯一、可断言 |
| 成功标准 | 结果合理即可 | 实际行为与预期一致 |
| 出错成本 | 重来一次就能补救 | 漏测一个缺陷就是事故 |
| 过程要求 | 关注结果,过程随意 | 过程可复现、可回溯 |
这张表是后续所有改造的总纲。你会发现,凡是Agent在通用场景里体现出来的"灵活""智能",到了测试场景里都可能变成"不稳定""不可控"。这不是要抹掉Agent的智能,而是要给它重新定一套行为规则。
1.2 失败成本不同:答错了可以重来,测错了就是漏测
通用任务里模型答错了,用户顶多骂一句,再问一次就行。测试场景里Agent判断错了,要么把一个真实缺陷放走了,要么把一个正常页面标记成失败,白白消耗整个团队去排查。这个成本差异直接决定了Agent在测试场景下必须多一套"自我约束"机制:不能随便猜测、不能跳过断言、不能靠幻觉补齐缺失信息。
这个约束不是靠prompt写一句"请严格按照测试步骤执行"就能解决的。我们在第一版prompt里加了大量这种强调,结果该乱来还是乱来。后来才意识到,通用Agent的规划层擅长的是把大目标拆成子目标,但不会自动具备测试领域的"预期管理"能力。你不可能指望它天生知道"等待元素出现3秒是合理的、等待4秒就要报异常"这种阈值判断。所以下沉改造的第一步,不是调prompt,而是重构它的动作空间和观测方式,让模型在一个被严格限定的轨道里做决策。
2. 动作空间收敛:把"模型自由发挥"改成"测试原语 + Schema 约束"
2.1 移动端测试原语的设计原则与分类
通用Agent通常让模型直接输出要执行的动作序列。在Web场景这还勉强能用,因为页面结构相对规整;移动端一旦交给模型自由发挥,它可能输出一些"看起来合理但根本不可执行"的动作,比如"点击右上角的设置图标,然后等待5秒"。"右上角"这种描述在视觉上没问题,但在自动化执行器里没法直接转换成坐标或控件句柄。
我们把动作空间收敛成一套可枚举的测试原语,每一条原语都对应移动端一个最小可执行操作,模型只能在这套集合里做选择,不能自己发明动作。最终沉淀下来的原语大致分成四类:
| 类别 | 原语示例 | 使用场景 |
|---|---|---|
| 操作类 | 点击、长按、滑动、输入、清除、键盘操作 | 执行用户动作 |
| 状态类 | 等待元素出现/消失、获取控件树快照、获取页面文本 | 感知页面状态 |
| 断言类 | 断言存在/不存在、断言文本匹配、断言元素可用 | 验证预期 |
| 辅助类 | 截图、OCR识别区域、处理系统弹窗、切换前后台 | 兜底与恢复 |
这里有个容易忽略的点:原语数量不是越多越好。数量一旦超过三十个,模型的选择准确率就会明显下降。我们在模拟项目X上做过对比,原语表从42个收敛到24个之后,单轮动作选择准确率提升了将近10个百分点。原则是让原语之间尽量正交,能用组合表达的不要单独设计一个动作。比如"滑动并点击"这种复合动作就不该存在,拆成"滑动"+"点击"两步,模型每走一步都有机会重新观测页面,这比一个高阶动作更稳。
2.2 用 Schema 约束输出,而不是让模型自由写操作
动作原语定义好之后,下一个关键决策是输出格式。我们一开始让模型直接以自然语言输出操作步骤,再由解析器去理解,结果解析错误比想象中多出不少。自然语言太自由了,同一个意思能写出十几种句式,解析器为了兼容这些句式要写一堆规则,写完还是会漏。后来改成结构化Schema约束,模型每次输出必须是一个符合JSON Schema的动作块,包含action、target、params、expected等字段。示例是这样的:
{ "action": "click", "target": { "by": "xpath", "value": "//android.widget.TextView[@text='登录']" }, "params": {}, "expected": { "type": "element_displayed", "value": "//android.widget.TextView[@text='首页']" } }这样每个动作自带预期结果,执行器执行完动作后自动去校验expected,而不是等模型下一步再判断。这一步做完之后,Agent的每一次操作都变成"可断言的动作",后续做失败归因才有证据可查,否则每个动作都是一个黑盒,出问题只能猜。
2.3 为什么这一步能省掉后面大量排查时间
用自然语言输出时,出了问题很难定位是"模型想错了""解析器理解错了"还是"执行器执行错了"。结构化Schema的价值在于把这三层分开:模型负责决策层,Schema负责接口层,执行器负责动作层。哪一层出问题,看日志格式就能直接定位,不用靠猜。这对Agent测试的维护体验是决定性的,因为Agent执行失败时,团队最痛的不是失败本身,而是不知道这个失败该怎么归属。
这里再提醒一句:Schema校验一定要在运行时做,不要只调模型的时候验证。我们在上线初期遇到过一个隐蔽问题——模型在95%的情况下输出是合法的,但偶尔会在嵌套参数里多出一个未知字段。如果执行器选择忽略未知字段,动作还是能跑,但断言条件可能已经悄悄变了。后来我们在校验层把unknown字段直接判为非法输出,强制模型重新生成。虽然多花了少量token,但执行结果的可信度上来了。这个决策在后面的排查中帮了大忙。
3. 观测层重构:Agent 的"眼睛"不能只靠截图
3.1 三层观测源:控件树、OCR、图像特征
通用Agent在多模态场景下通常以截图为主理解界面。但在移动端直接依赖截图有几个麻烦:深色模式下部分文字对比度不足,动态加载的列表项在截图上看着完整但实际还没渲染完,以及系统字体缩放导致模型对控件位置的判断出现偏差。截图不是不能用,而是必须作为观测的一部分,不能作为唯一依据。
我们给Agent搭了三层观测。第一层是控件树,通过系统辅助能力拿到当前界面的控件层级和属性,这是最可靠的结构化信息。第二层是OCR,当控件树拿不到或者控件没有文本属性时,用OCR识别界面文字,给模型提供文本层面的上下文。第三层是图像特征,主要用于定位验证和视觉断言,比如确认某个toast提示条是否消失。三层里控件树优先级最高,OCR次之,图像特征主要用于辅助确认。这个优先级在后面的冲突仲裁里会发挥作用。
3.2 弹窗、闪屏与"假加载":移动端专属干扰
在PC端自动化里,弹窗通常由被测应用自己控制。移动端最大的变量是系统级弹窗和应用内半透明浮层——它们会出现在任何一步操作之后,而且内容高度相似。例如定位权限弹窗、通知授权弹窗、应用更新提示,OCR识别出来都是"允许""禁止""以后再说"之类的按钮,几乎没有区分度。
如果不做处理,Agent很可能把系统弹窗当成业务弹窗去点。我们专门做了一层弹窗识别器,在每次动作执行前先做一次"全局弹窗扫描":比对当前控件树和OCR文本里是否出现高频权限弹窗关键词,如果命中,先执行预设的弹窗处理策略,再继续原测试流程。这层逻辑不能交给模型临场发挥,否则一个弹窗就能让整轮用例路径跑偏,而且你很难从日志里看出偏到了哪里。
除了弹窗,"假加载"也是移动端专属问题。页面转圈动画可能在内容已经加载完成后仍然显示几秒,Agent如果盯着"加载中"状态就会傻等。我们的状态机里把"加载完成"定义为"目标控件的关键属性出现且转圈动画消失",两个条件同时满足才算空闲。这个定义一开始没做好,结果Agent经常卡在"看到了目标控件但还在等动画消失"的僵局里,后来把条件改成"与或组合"才解决。
3.3 多源信息冲突仲裁:别让模型猜
三层观测源同时存在时,数据会打架。控件树说"登录按钮可用",OCR在同一个位置读到了"登录",但图像特征发现这个区域的按钮处于置灰状态。这种情况在移动端很常见,尤其在表单校验未通过时。
我们定的仲裁规则是:控件树属性优先于OCR文本,但图像特征在某些状态下可以推翻前两者。比如控件树里按钮enabled=true但截图明显置灰,就以图像特征为准。仲裁逻辑必须放在观测层,不要让模型自己拿两路信息去猜。模型一旦被扔进这种冲突里,就会开始"编"理由解释为什么按钮点不了、为什么断言失败,这才是最危险的。你希望模型专注于决策,而不是替观测层打圆场。
4. 环境扰动才是移动端 Agent 测试的真正敌人
4.1 等待策略:从固定 sleep 到状态机轮询
通用Agent框架在等待这个问题上通常很随意:要么让模型自己决定"等多久",要么干脆用固定sleep。移动端性能波动太大,固定sleep只有两个结果——等短了在控件没出现时报错,等长了每次操作都拖慢整体执行。我们最后实现了一个轻量状态机:空闲、加载中、出错三个状态。每次动作执行后,先进入"加载中"状态,轮询目标控件状态,同时设置超时上限。
超时后不是直接失败,而是先触发一次"重新取快照"。原因是很多超时是控件树漏更导致的,重新取一次大概率就好了。这个重试逻辑不能省,实测中它能救回大约20%的伪失败。别小看这20%,在Agent测试里,伪失败是最消耗团队信任感的东西,跑十次挂三次,就算全是误报,大家也会开始怀疑这套方案到底靠不靠谱。
4.2 设备性能、系统动画与点击偏移
移动端设备性能差异大,老设备上动画掉帧会导致控件实际坐标和渲染坐标不一致。我们在模拟项目X的低端机型上出现过一种经典问题:模型点击的坐标没问题,控件树也显示元素存在,但实际点击落在按钮边缘,事件被系统判定为点击空白,断言失败。这个问题排查了很久,最后在日志里发现每次失败的坐标都偏离控件中心几个像素。
解决办法是在点击动作里加一个"命中校验":点击完成后检查目标元素是否获得焦点或发生预期变化,如果没有,执行一次微小偏移重试。另外,部分设备开启了窗口缩放动画,控件树刚返回时元素已经存在,但视觉上还在移动。我们又加了一条规则:点击前如果检测到界面正在进行位移动画,先等动画结束。这个规则的误伤概率很低,但能显著降低误判,尤其是列表项点击场景。
4.3 多设备并行下的会话隔离
Agent框架在单设备上没问题,一旦并行跑多台设备,最容易出问题的反而不是模型,而是会话上下文串了。我们在接入之初就遇到过:设备A的测试路径切换到了设备B上,模型拿着设备A的控件树做决策,导致设备B上一连串误判,而且每一条误判看起来都很有道理。
后来我们在框架的每个步骤都显式携带设备标识,动作记录、观测快照、prompt上下文三个层面全部按设备隔离:
| 隔离层面 | 具体做法 |
|---|---|
| 动作记录 | 每步动作日志绑定设备标识和会话标识 |
| 观测快照 | 控件树、截图、OCR结果按设备分别存储 |
| prompt上下文 | 模型提示词只注入当前设备的历史信息 |
设备切换、断连、重连也必须重新加载该设备的上下文栈,不能直接复用上一个会话。这个听上去很基础,但通用Agent框架默认是为单会话服务的,下沉到并行测试场景时,这一点必须在框架层改掉,而不是靠业务侧规避风险。
4.4 权限与授权:前置处理优于临场处理
移动端权限弹窗如果在测试中途弹出,不管Agent多聪明,让它处理都是一种浪费。它每处理一次弹窗,就要多一次观测、多一次思考、多一次动作,还会打断原本的测试节奏。更合理的做法是在测试前置阶段就把这些问题清掉。
我们在框架里加了一个"环境预处理"阶段,跑测试之前自动完成权限授权、关闭不必要的系统通知、锁定设备屏幕超时,必要时通过启动参数直接跳过首启引导页。这套预处理做完之后,Agent在测试执行中需要面对的环境干扰减少了一半以上。我的建议是,凡是能前置稳定解决的问题,就不要丢给Agent临场发挥,Agent的智能应该花在真正的业务路径探索上,而不是浪费在跟系统弹窗斗智斗勇上。
5. 失败闭环:Agent 测试最值钱的产出是"为什么挂的"
5.1 三段证据链:执行上下文、操作回放、控件树快照
传统自动化脚本挂掉,日志里能看到是哪一行报错,排查相对容易。Agent测试挂掉时,模型可能做了二十步操作才失败,如果没有完整证据链,根本说不清是第几步出了问题。我们给每一次执行都留了三段证据:执行上下文(prompt、模型选择动作的理由摘要、Schema校验结果)、操作回放(每一步动作的关键参数和实际执行结果)、控件树快照(动作执行前后的界面状态对比)。
一旦断言失败,这三样东西自动打包成一份带时间线的报告。排查效率提升非常明显,不用再翻原始日志。尤其是"模型为什么在第五步选择了这个点击目标",从执行上下文里能直接看到它当时的观测输入,这比事后反推要快得多。Agent测试本质上还是一个黑盒,证据链就是打开黑盒的那把钥匙。
5.2 失败归因:三类错误分开对待
我们把失败归成三类。第一类是环境错误,比如设备断连、网络超时、App闪退,这类错误和被测功能无关,重跑即可。第二类是执行错误,比如控件没找到、点击偏移、弹窗遮挡,这类错误意味着Agent在特定环境下的动作执行不够稳,需要调整原语或等待策略。第三类是产品缺陷,这是最想看到的结果,说明测试真的发现了问题。
归因逻辑分两层:第一层用规则引擎粗筛,比如检测到设备断连关键词就直接归为环境错误。第二层把粗筛不了的情况交给LLM辅助判断,但前提是给模型提供完整的证据链,而不是只给一句失败信息。这样"伪装成环境问题的产品缺陷"才能被识别出来。我们实测中确实抓到过几个这类案例,如果只靠规则引擎,大概率会被当成环境问题直接放走,这些案例后来都成了推动Agent测试落地的最佳论据。
5.3 失败恢复:进阶不是重跑,是续跑
通用Agent失败后最常见的处理是重新规划整个任务。这在测试场景里代价太大——一轮移动端回归可能已经执行了十几分钟,从头再来既耗时又容易掩盖问题。我们改造成"分阶段续跑"模式:测试流程按业务链路切成多个阶段,每个阶段结束会保存会话检查点。失败后,Agent可以加载最近一个检查点,只重试当前阶段,前面的阶段结果保持不变。
这里要注意一个边界:并不是所有失败都适合续跑。如果产品缺陷已经确认,应该停止续跑,保留证据走缺陷流程;只有环境或执行类错误才尝试恢复。另外,续跑时建议把恢复操作也记录进操作回放,否则看报告的人会疑惑中间那段操作从哪冒出来的。续跑做得好不好,直接决定了Agent测试在长链路场景下的实用价值,这比单纯提高单次执行成功率更重要。
6. 落地效果与人效账:什么时候该交给 Agent,什么时候别交给它
6.1 一组有参考价值的复盘数据
讲一下模拟项目X的改造效果,不追求严格统计意义,但方向有参考价值。改造完成后大概两周时间,我们把原先70多条移动端核心回归用例做了分层处理。约30条高频冒烟用例仍然跑传统脚本,剩余40多条中长尾用例交给Agent驱动。单轮回归耗时从人工执行的约4小时压缩到Agent执行加人工审阅合计约1小时,路径覆盖率提升约25%,还额外增加了不少原先没覆盖到的跨页面状态组合。
要说明的是,这里面有相当一部分收益来自框架本身的结构化改造,而不是模型能力。把动作空间收敛、观测层重构、失败留痕做好之后,哪怕用不是最强的模型,稳定性也能达到可接受水平。很多团队在评估Agent测试时,把注意力全放在模型选型上,其实框架层的工程改造才是决定成败的胜负手。
6.2 分类决策:脚本、Agent、人工审阅三者的边界
哪些用例适合继续用脚本来跑?答案是那些步骤稳定、断言明确、执行频率极高的用例。脚本在这些场景下最可靠,也最省钱。哪些用例适合交给Agent?答案是跨页面链路长、前置条件多、组合状态多的中长尾用例,以及探索性测试场景。Agent的价值不在于跑得快,而在于它能在遇到小偏差时自动调整路径,而不是像脚本一样直接报错。
人要在哪里介入?不是每一步都介入,而是在Agent执行完一轮后做集中审阅,重点看失败证据链和覆盖率报告。如果Agent发现了一个新路径,但这条路径在业务上不该被走到,那可能反而是Agent自身导航能力太强导致的过度探索,需要在观测层加约束。我的建议是,这套分类边界要在一开始就固定下来,允许微调,但不能让Agent和脚本的边界变得模糊,否则维护成本会失控。
6.3 最后一条经验
下沉改造做下来,我个人的体会是:通用Agent框架不是不能用于测试,但它默认的那种"自由感",恰恰是测试场景最不需要的东西。你要做的不是给模型更多的发挥空间,而是给它一张足够清晰的地图、一套明确的红绿灯规则,然后允许它在地图边缘多走两步,并把它走过的路完整记录下来。这样一来,它既保留了智能体在意外情况下的应变能力,又能满足质量活动最看重的可重复和可追溯。
从执行结果看,Agent最适合的身份不是一个全自主的测试替身,而是一个"被严格约束的探索者"。它负责在长链路和复杂状态组合里帮我们蹚路,然后把每一步行动变成可审计的证据。这套思路迁移到Web端和接口层测试同样成立,如果你所在的团队也在做类似的尝试,强烈建议先从动作空间和观测层入手改造,而不是一上来就追求端到端的全自动。