
1. 从通用 Agent 到测试场景为什么下沉这么难移动端自动化测试这个领域过去几年一直有个尴尬的现实工具链很成熟但真正能跑起来的稳定用例少得可怜。Appium、UiAutomator、XCUITest 这些框架本身没问题问题出在写用例这件事上——它太依赖人对界面的理解、对业务逻辑的熟悉、以及对各种异常情况的预判。一个中等复杂度的 App维护一套端到端测试用例的成本往往比开发功能本身还高。大语言模型出来之后很多人第一反应是让模型直接操作手机。这个想法很直觉但真正落地时会撞上一堵墙通用 Agent 框架比如那些做网页操作、做桌面任务的框架的设计假设和移动端测试场景的需求根本不是一回事。通用 Agent 框架的核心目标是完成任务它允许一定的随机性和探索性。你让它去某个 App 里查一个订单状态它可能点错两次、退回来、换个路径再试最后拿到了结果任务就算成功。但测试场景要的不是最终拿到结果而是每一次操作都可复现、可断言、可回归。这两者的评价标准完全不同。我在实际项目里踩过的最大的坑就是一开始直接拿一个开源的通用 Agent 框架套到移动端测试上。跑 demo 的时候感觉很惊艳——模型看着截图自己决定点哪里居然真的能走完一个下单流程。但一旦要把它变成 CI 里每天跑的回归用例问题就全暴露了同样的输入今天跑通明天跑挂模型偶尔会自作聪明地跳过某个步骤断言逻辑无处安放因为 Agent 的输出是自然语言描述不是结构化的通过/失败。所以这个系列走到第九篇我想聊的核心就是当通用 Agent 框架下沉到测试场景需要做哪些改造才能让它从能跑变成能信。这不是简单的 prompt 调优而是要在架构层面重新划分职责——哪些交给模型哪些必须由确定性代码兜底。这篇文章适合两类人看一类是已经在做移动端自动化、想引入 LLM 能力提升用例编写效率的测试工程师另一类是在做 Agent 应用、想理解任务型 Agent和测试型 Agent本质差异的开发者。我会尽量把每一步的设计取舍讲清楚包括我为什么这么选、不这么选会怎样以及那些文档里不会写的实操细节。2. 通用 Agent 框架的三大假设以及它们在测试场景的崩塌要理解改造的必要性得先看清楚通用 Agent 框架是建立在哪些假设之上的。我把它们归纳为三条每一条在测试场景里都会出问题。2.1 假设一环境是宽容的失败可以重试通用 Agent 处理网页或桌面任务时环境相对宽容。点错一个按钮大不了返回上一页重来页面加载慢等几秒再截图。它的容错机制建立在重试成本低这个前提上。移动端测试环境完全不是这样。真机上的状态是有副作用的你点了一次提交订单订单就真的创建了你误触了退出登录整个会话就没了后续步骤全部失效。更麻烦的是很多 App 有防重复提交、有风控、有验证码重试的代价可能是整个用例报废甚至污染测试数据。我遇到过最典型的情况模型在支付页面犹豫了一下先点了返回想确认一下金额结果触发了订单取消逻辑后面所有断言全部对不上。通用框架的探索式重试在这里是灾难。2.2 假设二动作空间是开放的模型可以自由发挥通用 Agent 的动作空间通常是任意坐标点击 任意文本输入 滚动。它相信模型有能力从截图里判断出该点哪里。这个假设在网页上勉强成立因为网页的 DOM 结构相对规整元素边界清晰。移动端的界面要复杂得多。同一个功能不同机型、不同分辨率、不同系统版本下控件的位置和层级可能完全不同。而且移动端有大量看不见但存在的元素——悬浮窗、透明遮罩、WebView 里的混合内容。模型看着一张截图很容易把看起来像按钮的装饰性元素当成可点击目标。实操心得我后来强制把动作空间收窄成基于控件 ID 或无障碍标签的语义化操作而不是坐标点击。这一步改造之后用例的跨机型稳定性直接上了一个台阶。代价是需要提前做一层控件树的解析和映射但这层投入绝对值得。2.3 假设三成功标准是任务完成而非过程正确这是最根本的差异。通用 Agent 的终止条件是任务达成中间走了什么路径不重要。测试场景恰恰相反过程本身就是被测对象。一个下单流程测试要验证的不只是最后订单创建成功还包括库存是否正确扣减优惠券是否按预期抵扣页面跳转是否符合设计。通用框架没有断言这个概念它的输出是一段自然语言总结。而测试需要的是结构化的、机器可判定的结果。这就逼着我们在 Agent 之上再包一层断言引擎把模型的自然语言输出翻译成可断言的中间表示。理解了这三条假设的崩塌后面的架构设计就有了明确的靶子收窄动作空间、隔离副作用、外挂断言层。这三件事构成了整个下沉改造的主干。3. 下沉改造的核心架构把决策和执行彻底分开我在多个项目里反复调整之后稳定下来的架构可以概括成一句话模型只负责看和想做和判全部交给确定性代码。下面拆开讲。3.1 三层职责划分整个系统分成三层层与层之间通过结构化数据通信不传自然语言。层级职责实现方式是否用模型感知层把当前界面转成模型能理解的结构化描述控件树解析 截图 OCR 兜底否纯代码决策层根据目标和当前状态决定下一步动作LLM 约束解码是执行层执行动作、校验结果、记录证据确定性脚本 断言库否这个划分的关键在于决策层的输出必须是结构化的动作指令而不是自然语言。比如模型不能说我觉得应该点击登录按钮而必须输出{action: click, target: login_btn, reason: ...}。reason字段是给人看的action和target是给执行层用的。3.2 为什么不让模型直接输出坐标有人会问既然模型能看懂截图为什么不直接让它输出点击坐标我的答案是坐标是易失的语义是稳定的。同一个按钮在 1080p 屏幕上坐标是 (540, 1200)在 1440p 屏幕上就变成 (720, 1600)。如果模型输出坐标换个机型用例就废了。但如果模型输出的是点击登录按钮这个语义执行层再去控件树里找对应的元素跨机型就自然兼容了。实现上我在感知层做了一件事把控件树里的每个可交互元素提取出来生成一个带编号的清单连同截图一起喂给模型。模型只需要输出编号执行层根据编号反查真实控件。这样既保留了模型的视觉理解能力又避免了坐标漂移。3.3 约束解码让模型只能输出合法动作光靠 prompt 约束模型输出格式是不够的实测下来模型有 5% 到 10% 的概率会跑偏输出一些格式不对或者动作类型不存在的内容。在测试场景里这 5% 就是每天几十条失败用例。我的做法是在推理层加约束解码constrained decoding。具体来说用支持 JSON Schema 约束的推理接口把动作的 schema 固定下来动作类型只能是枚举里的几个值目标编号必须在当前感知层提供的清单范围内。这样模型在生成时就被框住了物理上不可能输出非法动作。注意约束解码会增加一点推理延迟但在测试场景里这点延迟完全可以接受。稳定性比速度重要得多。3.4 状态机兜底模型不是唯一决策者即便有了约束解码我也不建议把决策权 100% 交给模型。原因很简单测试用例里有很多步骤是死的比如输入用户名输入密码点击登录这些步骤根本不需要模型判断写死就行。所以我在决策层之上又加了一个轻量状态机。每个测试用例被拆成若干步骤每个步骤标注是确定性步骤还是智能步骤。确定性步骤直接执行智能步骤才交给模型。这样既降低了推理成本又减少了模型出错的暴露面。实测下来一个典型的下单流程用例大概 70% 的步骤是确定性的只有 30% 需要模型介入——通常是那些界面变化频繁、元素定位不稳定的环节。这个比例让整个系统的稳定性有了质的提升。4. 感知层实操把界面翻译成模型能吃的格式感知层是整个系统的地基它决定了模型能看到什么。这一层做不好后面全是空中楼阁。4.1 控件树解析的取舍移动端的控件树Android 的 AccessibilityNodeInfo、iOS 的 XCUIElement信息量很大但直接全量喂给模型是不现实的——一个复杂页面可能有上千个节点token 直接爆炸。我的做法是做三层过滤可见性过滤只保留visibletrue且displayedtrue的节点。可交互性过滤只保留clickable、editable、scrollable为真的节点以及它们的父容器。去重合并把嵌套的、语义重复的节点合并成一个。过滤之后一个页面的可交互元素通常能压到 20 到 50 个这个量级模型完全能处理。4.2 截图和控件树的融合光有控件树不够因为有些界面元素比如游戏画面、自定义绘制的内容在控件树里是空的。这时候需要截图来补。我的融合策略是控件树为主截图为辅。具体做法是把控件树里的元素在截图上标注出边界框生成一张带标注的截图连同元素清单一起给模型。这样模型既能看到视觉布局又能对应到具体的元素编号。# 感知层核心逻辑示意伪代码 def build_perception(driver): tree parse_accessibility_tree(driver) interactables filter_interactables(tree) screenshot driver.get_screenshot() annotated draw_boxes(screenshot, interactables) element_list [ {id: i, type: e.type, text: e.text, bounds: e.bounds} for i, e in enumerate(interactables) ] return { screenshot: annotated, elements: element_list, page_signature: compute_page_hash(interactables) }4.3 页面指纹判断我还在不在同一个页面这是很多人会忽略的一点。移动端测试里判断当前处于哪个页面非常关键因为同一个动作在不同页面上的含义完全不同。我用一个简单的页面指纹来做这件事把当前页面的可交互元素集合做哈希得到一个签名。如果连续两次感知的签名一致说明页面没变如果变了说明发生了跳转。这个指纹不需要精确匹配允许一定程度的容差比如元素顺序变化不影响。有了页面指纹就能实现一个很实用的能力动作执行后等待页面稳定。很多测试失败是因为动作执行太快页面还没渲染完就去截图了。用指纹轮询等到连续两次签名一致再继续稳定性提升非常明显。4.4 OCR 兜底处理控件树缺失的情况有些 App 用了大量自绘控件控件树里啥都没有。这时候 OCR 就是最后的救命稻草。我用的是轻量级的 OCR 模型把截图里的文字识别出来生成一个伪控件树再走上面的流程。OCR 的准确率肯定不如原生控件树所以我的策略是能用控件树就用控件树OCR 只作为兜底。在感知层的输出里会标注每个元素的来源native 还是 ocr模型在做决策时可以参考这个信息——如果某个关键元素是 OCR 识别的模型会更谨慎。5. 决策层实操让模型想得对比想得多更重要决策层是模型真正发挥作用的地方但也是最容易失控的地方。我的经验是给模型的自由度越小系统越稳。5.1 目标分解把大任务拆成小步骤不要让模型一次性决定整个流程怎么走。我的做法是把测试用例拆成原子步骤每一步只让模型决定当前这一步该做什么。比如完成登录这个任务拆成步骤 1定位用户名输入框并输入步骤 2定位密码输入框并输入步骤 3定位登录按钮并点击步骤 4验证是否进入首页每个步骤单独调用模型上下文只包含当前页面状态和当前步骤目标。这样模型不需要记住整个流程出错的影响面也被限制在单步之内。5.2 Prompt 设计少即是多我试过很多版本的 prompt最后稳定下来的版本非常简洁。核心就三块角色定义你是一个移动端测试执行助手你的任务是根据当前界面状态选择最合适的下一步动作。动作空间说明列出所有合法动作类型和参数格式。当前状态元素清单 当前步骤目标。我特意去掉了那些请仔细思考请一步步分析之类的引导语。实测下来这些引导语在测试场景里反而会让模型过度思考输出一些不必要的中间步骤。测试场景要的是快准狠不是深思熟虑。实操心得如果你用的是支持思考模式的模型在测试场景里建议关掉。思考模式会让延迟翻倍而且对动作选择的准确率提升非常有限。测试场景的动作选择本质上是一个从有限选项里挑一个的问题不需要复杂的推理链。5.3 少样本示例给模型抄作业的机会纯 zero-shot 的效果在测试场景里不够稳。我的做法是给每个动作类型配 2 到 3 个少样本示例示例里包含什么样的界面状态对应什么样的动作。关键是示例要覆盖边界情况。比如当页面上有多个相似按钮时如何根据上下文选择正确的那个。这类示例能显著降低模型的误选率。5.4 决策缓存相同状态直接复用测试用例有个特点很多步骤在不同用例里是重复的。比如登录步骤几乎每个用例都要走一遍。我加了一层决策缓存把页面指纹 步骤目标作为 key把模型输出的动作作为 value 缓存起来。下次遇到相同的状态直接复用缓存结果不再调用模型。这个优化带来的收益是双重的一是推理成本大幅下降二是稳定性提升——因为缓存的结果是确定性的不会出现同一个状态模型这次选 A 下次选 B的情况。当然缓存需要失效机制。我的做法是给缓存加一个版本号当 App 版本更新或者页面结构变化时手动清空缓存重新生成。6. 执行层实操断言、证据、重试的工程化执行层是测试场景区别于通用 Agent 的核心。这一层做得好不好直接决定了用例能不能进 CI。6.1 断言引擎把看起来对变成确实对断言是测试的灵魂。我的断言引擎支持三种断言类型断言类型适用场景实现方式元素断言验证某个元素存在/不存在/文本内容控件树查询视觉断言验证页面视觉状态如截图对比图像相似度数据断言验证业务数据如订单状态接口查询或数据库查询元素断言是最常用的也最稳定。视觉断言作为补充用于那些控件树无法覆盖的场景。数据断言是最可靠的因为它绕过了 UI 层直接验证业务结果。我的建议是能用数据断言就用数据断言。UI 层的变化太频繁了而业务数据相对稳定。一个下单流程与其断言页面上显示了订单号不如直接查数据库确认订单已创建。6.2 证据留存失败时能复盘测试失败时最怕的是不知道为什么失败。所以执行层必须留存足够的证据。我留存三类证据截图每个关键步骤执行前后各一张。控件树快照失败时的完整控件树用于事后分析。动作日志模型输出的动作、执行结果、耗时。这些证据打包成一个报告失败时自动附在 CI 结果里。有了这些排查问题的时间能从小时级降到分钟级。6.3 重试策略区分可重试和不可重试前面说过移动端测试的重试代价很高。所以重试必须分类处理。我把失败分成三类环境类失败如网络超时、设备卡顿可重试最多 2 次。定位类失败如元素找不到可重试但重试前要刷新感知层。业务类失败如断言不通过不可重试直接标记失败。这个分类逻辑写死在执行层里不交给模型判断。模型可以建议要不要重试但最终决定权在执行层。6.4 副作用隔离每个用例独立环境这是保证测试可信度的关键。每个用例执行前必须把 App 恢复到干净状态——清数据、重新登录、重置配置。我的做法是用容器化或者快照技术给每个用例分配一个独立的设备状态。用例执行完直接销毁环境下一个用例用全新的。这样用例之间不会互相污染失败也能精确定位到具体用例。代价是执行时间变长但换来的是可信度。在测试这件事上可信度永远优先于速度。7. 常见问题与排查技巧实录这一节是我在实际项目里踩过的坑整理成速查表希望能帮你少走弯路。7.1 模型幻觉出界面上不存在的元素现象模型输出的动作目标编号在感知层提供的清单里根本不存在。原因通常是 prompt 里的元素清单和模型看到的截图不一致或者模型在长上下文里记混了。解决约束解码是第一道防线物理上禁止输出越界编号。第二道防线是在执行层做校验如果编号越界直接判定为决策失败触发重新感知和重新决策。7.2 页面跳转后模型还在操作旧页面现象动作执行后页面已经跳转但模型基于旧截图做决策导致操作错位。原因感知和决策之间有时间差页面在这期间发生了变化。解决用页面指纹做同步。每次决策前先确认当前页面指纹和感知时的指纹一致。如果不一致重新感知。7.3 相似元素导致误选现象页面上有多个确认按钮模型选错了那个。原因模型只看文本不看上下文。解决在感知层给每个元素补充上下文信息比如所属容器相邻文本在页面中的位置。这些信息能帮模型区分相似元素。7.4 推理延迟导致用例超时现象单个用例执行时间过长CI 超时。原因每个步骤都调用模型累积延迟很高。解决决策缓存 确定性步骤直连。把不需要模型判断的步骤全部走确定性路径只把真正需要智能判断的步骤交给模型。7.5 跨机型兼容性差现象在一个机型上跑通的用例换个机型就挂。原因用了坐标点击或者依赖了特定机型的控件树结构。解决强制语义化操作禁止坐标点击。控件树解析时做归一化处理屏蔽机型差异。问题根因首选解法兜底方案元素幻觉上下文不一致约束解码执行层校验页面错位感知决策不同步页面指纹同步重新感知相似元素误选上下文缺失补充元素上下文人工标注推理延迟全步骤调模型决策缓存确定性直连跨机型失败坐标依赖语义化操作控件树归一化7.6 一个容易被忽略的坑模型对空状态的处理很多页面在数据加载完成前是空白的或者显示 loading。模型看到这种页面往往会做出奇怪的动作比如点击空白区域。我的解法是在感知层加一个页面就绪判断如果页面上没有足够的可交互元素或者检测到 loading 标识就等待而不是决策。这个判断用简单的规则就能实现不需要模型参与。8. 这套架构适合什么场景不适合什么场景聊了这么多技术细节最后说说适用边界。不是所有移动端测试都适合引入 LLM Agent。适合的场景界面变化频繁、传统定位方式维护成本高的用例。需要理解语义才能操作的流程比如找到价格最低的商品。探索性测试需要 Agent 自主发现问题的场景。不适合的场景纯接口测试根本不需要 UI 交互。界面极其稳定、定位方式长期不变的用例用传统方式更划算。对执行速度要求极高的场景LLM 推理延迟是硬伤。我的建议是混合使用稳定的核心流程用传统方式易变的边缘流程用 Agent。不要为了用 LLM 而用 LLM工具是拿来解决问题的不是拿来炫技的。这套架构我在几个项目里跑了大半年最大的体会是LLM 在测试场景里的价值不在于替代人写用例而在于降低用例的维护成本。传统用例最怕的是界面改版一改就要重新定位元素。而语义化的 Agent 用例界面小改版基本不用动大改版也只需要调整感知层的映射。这个维护成本的下降才是真正值得投入的地方。如果你正准备在自己的项目里尝试这套方案我的建议是从一个小流程开始比如登录或者搜索先把感知层和执行层跑通再逐步引入决策层。不要一上来就搞全流程那样调试成本会让你怀疑人生。