这个系列前面聊了好几期的LLM自动化测试Web端、PC端都踩过不少坑到了移动端这一篇我想换个角度来聊——不是再写一篇“怎么调Prompt让Agent点按钮”而是聊一件更扎心的事当通用Agent框架真的下沉到测试场景时到底发生了什么。先说一个我自己的反面案例。某一个Demo购物App我带了一个在Web端跑得还算顺的通用Agent框架想着移动端也就那么回事截图换成UI树点击换成driver断言套个壳完事。结果接入当天两条最简单的用例就开始随机失败一会儿是“元素没找到”一会儿是“点击报错”更气人的是Agent自己报告“任务完成”可断言全部红了。那天晚上我盯着日志翻了两个小时才意识到问题根本不是哪个环节写错了而是整个框架的“世界观”和测试场景的“世界观”是冲突的。通用Agent框架的设计目标是“把事搞定”而测试场景的目标是“证明系统没坏”。一个是求完成一个是求判定。这两者在环境假设、输出形式、容错策略上完全不是一回事。这篇文章就把“下沉”这件事拆开讲清楚哪些组件能直接复用哪些必须推翻重做下沉过程中我踩过的具体坑以及最终沉淀下来的框架结构。适合正在做LLM自动化测试、尤其是想把Agent能力用到移动端回归里的团队参考。1. 通用 Agent 框架的产品定位和测试场景的隐藏冲突1.1 通用框架的三个默认假设通用Agent框架能跑起来是因为它做了三个大前提假设。第一环境是不需要追溯的——它只需要知道“当前状态”不需要知道“这个状态是怎么来的”更不需要回答“上一个动作和这个状态之间有什么因果关系”。第二动作正确性由环境反馈来隐式校验——点错了就再点一次页面崩了就换个路径反正最终任务完成了就算成功。第三输出物是动作序列本身任务完成了过程记录只作为附件存在。这三个假设在Web自动化、桌面自动化里问题不大因为环境的可控性足够高。但移动端一旦接进来问题就暴露了。移动端的状态空间比Web大得多系统弹窗、前台切换、键盘状态、网络延迟、跨进程UI随便哪个变化都会导致环境反馈失真。Agent以为自己点的是“确定”实际上弹窗层级已经变了它点中的是一块透明遮罩。我见过不少团队直接在通用框架上叠加测试断言做法就是让Agent在任务结束之后自己生成一句“验证报告”。这是典型的偷懒式下沉结果就是Agent的自我评价出现系统性偏好——它倾向于认为自己的操作是正确的。有个测试同学统计过Agent自评“完全通过”的用例里实际通过率只有六成多。1.2 测试场景真正需要的三样东西测试和通用任务最大的区别在于它要求可判定、可重复、可归因。可判定是说任何一个步骤执行完系统必须能回答“这一步符合预期吗”而不是“这一步有没有执行”。后者是通用框架的逻辑前者才是测试的逻辑。可重复是说同样的用例跑十次结果应该一致——Agent的随机性恰恰是最大的敌人同一个动作在不同次运行里可能产生不同的路径选择这在测试里是致命的。可归因是说用例失败时必须能定位到“哪个状态、哪个动作、哪个断言”出了问题而不是像通用框架那样给一句“任务未完成”。这三样东西通用Agent框架一件都没有默认提供。这不是功能缺失是设计哲学的错位。通用框架追求的是“完成率”测试框架追求的是“证据链”。1.3 下沉边界的判断哪些能留下哪些必须拆我在做下沉改造的时候给组件分过四个象限组件通用框架中的角色下沉后的处理LLM对话引擎核心决策器保留但限制动作空间环境感知UI树/截图决策输入保留但要做结构化清洗状态管理记忆/上下文任务进展跟踪部分保留改为步骤级上下文动作执行器完成任务主体必须重构改为原子动作服务结果反馈机制任务完成判断必须重写替换为独立裁决层这个表格看起来简单但实际操作中最容易犯的错就是把“保留”当成“不改”。比如UI树感知通用框架拿到的是一棵完整的控件树但在测试场景里这棵树里90%的节点都是无用的框架布局节点直接喂给LLM只会消耗token、产生噪音。我后面会详细说感知层具体怎么清洗。2. 下沉前要想清楚的三件事状态、成本、断言2.1 移动端状态空间比桌面/Web大得多通用Agent框架在Web端跑得好一个很重要的原因是Web页面的状态相对收敛DOM树是稳定的交互约束是清晰的。移动端完全不是这样。除了业务页面本身的UI状态还有一堆“系统级状态”横插进来权限弹窗、系统更新提示、输入法面板、前台应用切换、跨进程Toast、焦点变化。这些状态有一个共同点——它们都不是业务代码控制的但都能让用例失败。Web端你不需要处理浏览器弹窗因为那是自动化框架的事移动端不行弹窗是移动操作系统的重要组成部分你不处理它它就会处理你。我在改造框架时遇到过一个典型案例测试的是登录页输入框Agent先输入账号再输入密码然后点击登录。看起来没毛病但在某一次运行时输入密码的时候系统刚好弹出了“输入法联想词”把登录按钮挡住了。Agent截图看到按钮还在实际是联想词遮挡了按钮的真实位置点击坐标落在联想词上登录没有触发。通用框架的逻辑是“点不中再点一次”于是它连续点了三次三次都打在联想词上最后自评“登录按钮被阻塞”。这种问题靠Prompt是解决不了的。必须在感知层把屏幕上的“干扰层”识别出来在执行层做“遮挡检查”在决策层做“状态重试判断”。三层联动才算把移动端状态这个坑填上。2.2 每次Agent决策都是API调用预算控制策略下沉之前我在Web端跑Agent的成本感知还比较模糊因为一个用例通常只需要十几个动作。但移动端一个典型回归用例步骤数几乎是Web的两到三倍因为移动端交互本身就更碎进入页面要等、软键盘要收、卡片要滑。我做过一次粗略统计一条带20个动作步骤的移动端用例如果每一步都走LLM决策感知输入压缩到800token、决策输出200token一条用例大概要消耗20000token左右。这不是大问题但如果有1000条这样的用例做回归单轮回归就是2000万token这个量级就会让成本变得很具体。所以下沉设计里必须考虑三个层次的成本控制而不是盲目省。第一层是感知压缩不要整棵UI树直接进Prompt先做一层本地规则裁剪只保留当前屏幕里“可见、可操作、有业务语义”的节点。第二层是决策缓存对相同状态的UI快照做哈希命中缓存直接复用上次的决策结果App页面上常见的静态界面效果非常明显。第三层是运行预算给每个用例设置步骤上限、API调用上限达到上限强制失败而不是让Agent无限重试。2.3 断言不是生成结果而是对系统状态的精确判定通用Agent框架里“任务完成”是由Agent自己判断的。这在测试场景里行不通——测试断言要求的是对系统状态的精确判定而不是LLM的置信度。我一开始也想让LLM直接生成断言比如让它在用例结尾写一段“我认为页面应该显示什么”结果效果很差。不是参数写得不对是根本性的逻辑问题LLM在决策链路里既是运动员又是裁判员它很难对自己刚刚做出的操作产生“怀疑”。它倾向于维持上下文一致性哪怕页面状态不符合预期它也会倾向于把偏差解释为“预期内的中间态”。正确的做法是让LLM负责生成动作让代码负责验证结果。LLM只管“接下来做什么”以及“做完之后预期出现什么状态”这个预期状态被结构化地传递给一个独立裁决模块由裁决模块去UI树、截图、网络状态里去验证。LLM输出的自然语言断言只能作为辅助解释不能作为通过依据。3. 架构改造从“任务完成型”到“验证执行型”的五层结构3.1 感知层UI树预处理与元素语义化感知层的第一件事就是处理移动端UI树。Android的UI树拿到手顶层结构几乎都是DecorView、FrameLayout、LinearLayout这种布局节点业务信息全在深层。所以感知层做的第一件事是“展平过滤”去掉不可见的节点、去掉纯布局节点、合并同一父容器下的文本子节点把“一个按钮、一段文案、一个输入框”这样的语义单元输出给下一层。第二件事是给元素做属性补充。原生UI树里的控件往往只有resource-id、class、text、bounds这些原始属性但测试决策需要的是更接近人理解的语义信息比如“这是一个登录按钮”“这是一个密码输入框”“这是一个错误提示”。我用两类手段来补一类是本地规则映射基于resource-id和text的关键字命中另一类是轻量级视觉模型标注给截图上的关键区域打上语义标签并把语义标签关联到UI树节点。这里有一个容易被忽略的性能点感知层不能每步都做完整的截图全树处理太慢。我的做法是把感知拆成两级轻量级感知事件触发后500ms内只拿增量UI树和重量级感知失败重试、断言校验时才做完整截图语义标注这样能显著降低单步延迟。3.2 决策层动作规划与状态校验分离决策层的核心改动是把“规划动作”和“校验结果”拆成两个独立的LLM调用链或者至少在同一个调用里强制分离输出结构。具体来说我让Agent输出的是一个结构化的“动作计划块”包含三个字段意图描述、目标元素、预期状态。意图描述是给测试工程师看的说明这一步要做什么目标元素是结构化的控件定位信息预期状态是一组可校验的表达式。这三者构成一个可执行、可验证的动作单元。拆开之后的好处是动作规划可以保持LLM的灵活性和自然语言理解能力而预期状态校验则完全走上代码化的确定性路径。LLM只负责“我想让页面出现什么结果”能不能通过不归它管。3.3 执行层混合驱动保证动作落点稳定移动端执行层最怕的不是“动作不触发”而是“动作触发了但落点在错的元素上”。通用框架里点错一个元素可以重试测试场景里点错就是一条脏数据、一个不可恢复的状态。我采用的方案是混合驱动。优先使用元素定位方式通过感知层返回的控件节点信息来调用原生点击/输入能力。只有在元素定位不可用时比如游戏界面、自定义渲染引擎才降级到坐标点击。并且所有坐标点击在每次执行前都做一次“实时遮挡检查”——获取当前屏幕前台的控件栈确认坐标点对应的最上层控件确实是目标控件再执行点击。移动端还有一类特殊问题应用未启动、页面还在冷启动、网络未就绪。通用的做法是盲目等待但正确的姿势是把“等待”也变成感知条件——等待的是“某个控件出现”而不是“过了几秒钟”。我把通用框架里的隐性等待全部改成显式条件等待Agent不需要自己估算等待时间执行层会在条件满足后自动推进。3.4 裁决层从“Agent自判”到“测试代判”裁决层是我在整个下沉改造里投入最多的部分。它要解决的问题很单一每一步动作结束后系统状态是否符合预期。裁决层接收的输入是决策层给出的预期状态表达式 感知层返回的最新UI状态 可选的屏幕截图。输出是一个三元组的判定PASS、FAIL、UNKNOWN。其中UNKNOWN是专门为LLM场景加的——当感知数据不足以确定性判断时不允许LLM猜直接标记UNKNOWN并触发重试或请求二次决策。预期状态的校验写法我参考了自动化测试中断言的设计思路但做成移动端语义化的版本。例如“元素存在且文本为xx”“元素可见且在屏幕上半区”“网络请求已完成且响应码为2xx”。这些表达能力完全够用又比自然语言断言可靠得多。裁决层最关键的一个设计决定是断言的可追溯性。每一项断言执行完裁决层都会生成一条结构化记录断言类型、目标元素、执行时截图裁剪、比对结果、原始置信度。这些记录最终拼接成完整的“证据链”在用例失败时可以一步步回溯。3.5 记录与回放让每一步都有据可查通用Agent框架是不重视过程记录的它只在乎结果。但测试场景里失败分析和过程复现是核心需求。所以我把记录系统独立出来记录的不只是“动作步骤”而是“状态快照”。每完成一个动作记录系统会保存四样东西执行前的状态摘要、执行的动作原语和参数、执行后的状态摘要、裁决层的判定结果。这四样合起来就是一次完整的“决策-执行-验证”闭环快照。用例结束后这些快照被汇总成一份带时间线的结构化报告。这份报告的调试价值非常大。有一次一条用例失败Agent的报告只说“断言失败”但我打开时间线一看发现第三步执行后页面状态已经是异常分支了后面所有动作都是在错误页面上进行的无效操作——这就是典型的“错位状态延续”问题。没有记录系统这种问题几乎没法定位。4. 核心模块下沉实录动作原语、状态提取与状态机设计4.1 动作原语白名单把“自由发挥”关进笼子下沉的第一刀我先砍掉了Agent的“自由发挥”。通用Agent框架允许LLM发明动作——比如“尝试双击”“试试长按两秒”——这在测试场景里是灾难。测试用例需要的是确定的、可重放的行为序列不是即兴表演。我定义了一套移动端动作原语白名单Agent只能从这套白名单里选择并填充参数。白名单包含tap_element(element_id, timeout)input_text(element_id, content)swipe_element(element_id, direction, distance)press_back()wait_for(condition, timeout)assert_visible(element_id)assert_text(element_id, expected)open_deep_link(url)这套原语不是随便拍的它的设计原则是每个动作都可以对应到系统层可观察的副作用。tap_element之后框架能查到这次点击是否真正改变了焦点或触发了事件input_text之后能查到输入框的文本内容是否变更。只有动作可观测裁决层才有判定依据。把动作空间关进笼子里LLM的自然语言能力并没有退化它依然需要根据页面状态来选择动作、提取控件、决定参数。自由的“操作想象力”被转移到“动作组合策略”上这才是测试场景需要的能力。4.2 状态提取从自然语言到结构化校验表达式Agent在规划动作时我强制要求它输出“预期状态”为此设计了Prompt模板。核心引导逻辑是先让Agent描述意图再从预置的期望模板列表中选择对应的校验类型最后一步填写目标元素。这样LLM不需要自己发明校验语法只需要做选择题和填空题。举个例子Agent想验证“登录失败时提示错误信息”它的输出可能是{ intent: 验证密码错误时的登录失败提示, action: tap_element, target: login_button, expected_states: [ { type: element_visible, element: error_message_banner, timeout: 3000 } ] }这个结构化的expected_states会被裁决层解析为可执行的校验指令。LLM就算描述了“错误提示应该出现”最终执行校验的也是代码而不是语言模型。这是整个下沉改造中确定性最强的一环。我还做了一个容易被低估的功能预期状态里的“隐藏检查”。移动端测试经常要验证“某元素不应该出现”或者“某控件可见但不可点击”这些在通用Agent框架里几乎无法表达因为通用框架只关心“能做什么”不关心“不应该出现什么”。在测试体系里这类负向验证往往比正向验证更容易暴露回归问题。4.3 从自由循环到有限状态机给Agent加上运行边界通用Agent框架的基础运行循环是感知状态→决策→执行→反馈→再感知。这个循环本质上是一个“自由loop”没有边界。测试场景里没有边界的循环是不可接受的——要么提前失败要么长期挂起唯独不能“永远跑下去”。我把运行循环改造成了有限状态机。状态集合是预定义的IDLE→INIT初始化环境→OBSERVE感知状态→PLAN决策动作→EXECUTE执行动作→VERIFY裁决校验→NEXT_OR_END判断继续或结束。每一个状态都有超时、重试次数、退出条件。以VERIFY状态为例它的退出条件有三条校验PASS进入NEXT_OR_END校验FAIL进入归属分析流程校验UNKNOWN触发一次最多三次的再感知重试。这个设计加上前面说的运行预算保证Agent永远不会无限循环。还有一类循环问题更隐蔽状态级循环。Agent可能在一组状态之间来回跳动比如“输入密码→报错→清空→再输入同一个密码”表面上看每一步都有进展实际上原地踏步。我在状态机里加了一个“循环检测器”如果感知层发现最近五步的UI状态指纹由关键元素哈希生成中有重复出现两个以上同一指纹就强制中断并提示Agent改变策略。4.4 设备管理并发跑机的冷启动与状态恢复移动端自动化一旦涉及多设备并发设备管理就变成一个基础设施问题。Agent框架不会替你考虑这些它默认以为环境是干净的。我的做法是把设备生命周期管理放到测试框架层而不是Agent层。每个Agent循环开始前先做设备预检应用是否安装、版本是否匹配、登录态是否有效、权限弹窗是否清除、系统设置是否处于基线状态。如果预检失败不进入Agent流程直接在框架层重置设备。一个容易踩的细节是移动设备在长时间跑并发用例后会累积大量“脏状态”应用缓存膨胀、后台进程残留、系统弹窗堆积。这些状态会让Agent的感知输入变得越来越“脏”决策质量随执行轮数递减。我在框架里加入了每N步骤一次的“环境体检”触发条件包括感知层的元素去重率下降、裁决层的UNKNOWN比例升高一旦触发就做一次软重置清理后台、重置弹窗拦截状态。5. 踩坑实录下沉过程中的三个典型事故与排查链路5.1 坐标点击“看起来成功但断言全红”我第一天下沉测试的时候最难受的一个现象是Agent执行tap操作执行日志显示“坐标点击成功”但紧接着的断言却全部失败。看起来是“点了个寂寞”但到底发生了什么一开始完全无从查起。排查链路是这样的。第一步我在执行层加了坐标落点记录发现Agent点击的坐标是(540, 960)而UI树上目标按钮的bounds却显示着(540, 900)到(540, 1020)。坐标在按钮范围内但为什么不触发第二步打开页面截图逐帧对比发现截图里按钮位置和UI树bounds不一致偏移了大约60个像素。这个偏移刚好是状态栏导航栏的高度。第三步检查坐标系定义发现UI树返回的bounds用的是“内容区坐标”而截图渲染用的是“全屏坐标”两者本来就有系统偏差Agent把截图上的视觉坐标直接当成UI树坐标来用了。根因找到了但这个坑其实有两层。第一层是坐标系混用我修复了坐标转换逻辑统一走内容区坐标系。第二层更隐蔽即使坐标正确移动端点击还有一个“命中判定”问题——点击点落在控件边缘时不同版本的App对touch事件的处理逻辑可能不同有的会冒泡到上层遮罩。我的解决办法是坐标点击一律以元素中心点为基准并把中心点做一次执行前的“命中预见”检查该点在当前控件栈中对应的顶层控件是否是目标元素。5.2 Agent把“等待”当动作时间语义缺失引起的误判第二个坑发生在一条页面跳转用例上。Agent点击了“进入购物车”按钮然后立刻断言“购物车页面标题存在”断言失败。Agent的数据显示“操作已执行状态不符合预期尝试重新点击”于是又点了两次三次点击之后页面跳过去了但系统弹出了一个“重复提交风险”的提示框用例被误判为异常。这个问题的本质是通用Agent框架里没有“等待”这个概念它的时间感受是离散的点了就是点了没点就是没点。但移动端页面跳转是异步的“点击完成”和“页面状态就绪”之间隔着不可忽略的时间差。我的解决方案是在设计决策Prompt时明确告诉Agent“移动端操作之后不代表页面立即就绪必须使用wait_for原语等待目标条件成立再继续下一步”。同时执行层做双保险——每个动作执行后默认插入一个短超时窗口通常200到500ms除非动作原语本身定义了更长的等待条件。这样即使Agent忘了写wait_for框架也能兜底一部分。另外我把裁决层的“断言失败”和“断言时机未到”做了区分后者的重试策略是完全不同的。5.3 token成本失控感知输入成了吞金兽第三个坑不太起眼但影响最大。下沉跑了两周之后我拉了一下整体API账单发现单条用例的平均token消耗比我预估高了三倍多。排查时看了一下统计表消耗大头不是决策输出而是感知输入。我把一份原始UI树打印出来看了下一棵结构复杂的页面节点数量可以到三四百个。直接把整棵树丢给LLM光是输入序列就要3000到5000个token。二十个步骤的用例光感知输入就烧掉六到八万token这还是没算重试的情况。这个问题的根因有两层一是UI树本身太脏大量无意义节点占用输入空间二是Agent每一步都需要“重新看整个页面”没有利用多步间的增量变化。我的优化分为三步第一步感知层过滤只保留可见、可交互、带文本信息的三类节点再按业务语义合并一次典型页面能压到二三十个节点。第二步增量感知如果连续步骤之间页面结构没有大变化只向LLM提交“变更部分”的摘要。第三步缓存决策对相同UI指纹的步骤直接复用缓存结果我实测过一条稳定流程里大约有30%到40%的步骤可以命中缓存。优化之后同一条用例的token消耗降到了原来的三分之一左右。这个数字很直观地说明了移动端Agent的成本大头不在模型能力在于感知输入的组织方式。5.4 从失败类型到判定逻辑一条排查思路的沉淀踩了这些坑之后我把移动端Agent用例的失败来源整理成一个分类框架失败类型典型现象排查方向感知层失败元素在截图中可见但UI树缺失检查控件是否跨进程/原生绘制决策层失败Agent选了错误控件或错误动作检查Prompt里动作空间约束是否清晰执行层失败点击无响应、输入丢失检查坐标系、遮挡层、焦点状态裁决层失败断言表达错误或时机不当检查expected_states的校验类型和超时环境层失败弹窗、网络、应用状态异常检查设备基线状态和预检条件这个分类框架本身也是可执行的——在框架内部每失败一次就会按这五类做归因并把归因结果写进报告。这样Agent用例的失败不再是一锅粥而是有明确调查线索的事件。第一批用例跑了三周之后我统计下来将近一半的失败属于环境层这也进一步印证了那句话下沉动作最重的改造不在LLM而在LLM之外的系统约束。6. 实测效果一个跨平台Demo应用上的横向对比6.1 基准测试的维度设计为了验证下沉改造的实际收益我用一个自己维护的跨平台Demo应用搭了一套基准测试。这个Demo包含典型的移动端业务形态登录、列表加载、详情跳转、表单交互、消息通知。测试场景选了六条有代表性的用例常规登录成功、登录失败提示、列表下拉刷新、加购数量更新、流程中断后恢复、负向校验无关元素不出现。对比维度设了五个用例编写耗时、单用例执行时间、稳定通过率、失败可归因性、异常场景自适应能力。对照组我做了三个纯规则脚本套模板的自动化用例、纯视觉脚本依赖图像识别的自动化用例、通用Agent直出方案不经过下沉改造直接用通用Agent跑测试。6.2 结果对比下沉之后到底强在哪维度纯规则脚本纯视觉脚本通用Agent直出下沉改造方案用例编写耗时条/分钟152025单用例执行时间均值35秒40秒90秒60秒稳定通过率20轮均值92%88%74%95%失败可归因性强中弱强异常场景自适应无弱强强别看通用Agent直出的用例编写耗时最短那只是因为“写出来”和“能稳定跑”是两件事。它的稳定通过率只有七成多而这还是测试集中的常规场景真实环境里的长尾问题会更明显。下沉改造方案的用例编写耗时虽然比通用直出多了一倍多但这部分时间主要花在结构化预期状态、明确动作原语上是做“可验证性”的投资。6.3 适用边界什么样的用例才值得走Agent我跑了这批基准之后对Agent下沉的适用边界也有了一个比较清晰的感受。不是所有用例都该走Agent走Agent反而会有三笔额外成本token消耗、决策延迟、偶尔的不确定性。如果一个用例的步骤高度固定、页面结构长期稳定用纯规则脚本写稳定性和速度都远优于Agent方案。真正适合走Agent的用例是那些“规则脚本写不动、视觉脚本看不清”的场景跨流程的异常恢复、需要根据运行中状态动态调整路径的长流程、涉及自然语言理解的校验比如页面文案是否合理以及页面结构频繁变化导致脚本维护成本高的回归模块。一个具体的例子是消息通知流程的测试。规则脚本要写死“下拉多少次”“点击哪个条目”一旦通知条目的顺序变化脚本就废了。Agent方案里我把期望状态表达成“存在一条内容包含‘订单发货’的通知”至于它在哪个位置、是否需要下拉才能看到完全由Agent在运行中自行感知和决策。这就是下沉改造之后真正让人舒服的地方——写的是意图跑的是执行验的是证据。下沉这个事我个人的体会是与其说是改造Agent框架不如说是给Agent框架补齐了测试场景要求的确定性边界。动作空间被约束了状态校验交给了代码运行过程有了边界和预算这些看起来都是在“限制”模型的能力但恰恰是这些限制让模型真正能在测试体系里站住脚。把这个思路理顺之后下一个可以继续深挖的方向是把裁决层从“单步校验”升级到“序列级校验”让Agent不只验证每一步还验证一段步骤组合后的最终业务状态这个做好了会让移动端Agent测试的稳定性再上一个台阶。