news 2026/10/20 3:30:44

通用Agent框架下沉移动端测试:从脚本驱动到目标驱动的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
通用Agent框架下沉移动端测试:从脚本驱动到目标驱动的工程实践

1. 从通用 Agent 到移动端测试:这次下沉到底意味着什么

移动端自动化测试这个领域,过去几年一直有个很尴尬的局面:工具链很成熟,但写用例的人很痛苦。Appium、Espresso、XCUITest 这些框架本身没问题,问题在于维护成本。一个页面改个 ID,几十条用例跟着挂;一个弹窗逻辑调整,定位表达式全部重写。团队里但凡做过两年移动端自动化的,都经历过那种“改一个按钮,修三天脚本”的崩溃。

大语言模型出来之后,很多人第一反应是“能不能让模型直接写用例”。这个思路对,但不够深。真正有价值的变化不是让模型帮你生成几段 Appium 代码,而是把通用 Agent 框架整体下沉到测试场景里——让 Agent 自己去观察界面、规划操作路径、执行动作、验证结果、失败后自我修正。这跟传统的“录制回放”或者“脚本驱动”是两种完全不同的范式。

我最近几个月一直在折腾这件事,把几个主流的通用 Agent 框架往移动端测试场景里搬。踩了不少坑,也摸出了一些门道。这篇文章就把整个思路、技术选型、实操细节和避坑经验完整拆一遍。适合两类人看:一是做移动端测试开发、想了解 Agent 怎么落地的工程师;二是已经在用 LLM 做测试、但发现“生成脚本”这条路走不通、想往更深层走的人。

核心要解决的问题就一个:让 Agent 像人一样操作手机,而不是像脚本一样执行坐标。听起来简单,做起来涉及视觉理解、动作空间设计、状态管理、失败恢复等一整套工程问题。下面逐层拆。

2. 通用 Agent 框架下沉测试场景的整体设计思路

2.1 为什么不能直接用“LLM 生成脚本”这条路

先说清楚为什么“让模型写 Appium 脚本”这个方案在真实项目里很难跑通。我试过,用 GPT 系列模型根据页面截图和 DOM 树生成 Python + Appium 代码,初期看着很美好,生成速度确实快。但问题在三个地方集中爆发。

第一,定位表达式极其脆弱。模型生成的 XPath 或 accessibility id 往往基于当前页面快照,一旦页面结构微调,比如多了一层容器 View,表达式就失效。而移动端页面结构变动频率远高于 Web,尤其是敏捷迭代的团队,一周改三次布局很正常。

第二,模型对时序和等待的理解是缺失的。移动端操作有大量隐式等待、动画过渡、网络加载。模型生成的代码经常是“点击后立刻断言”,实际跑起来十次有八次因为元素还没渲染出来而失败。你得手动补一堆显式等待,补到最后发现还不如自己写。

第三,异常分支覆盖不了。真实测试场景里,弹窗、权限请求、网络异常、版本更新提示这些“意外”才是测试的重点。模型生成的线性脚本对这些分支基本没有处理能力,一遇到就卡死。

所以“生成脚本”本质上还是把 LLM 当代码补全工具用,没有发挥 Agent 的核心能力——感知、决策、执行、反馈的闭环。

2.2 Agent 下沉的核心范式转变

通用 Agent 框架(比如基于 ReAct、Plan-and-Execute 或者多模态 Agent 的那些)之所以能下沉到测试场景,是因为它天然具备几个测试需要的特性。

感知层:Agent 通过截图(视觉)+ 页面结构(DOM/accessibility tree)双通道理解当前界面状态。这比单纯依赖 DOM 更接近人的操作方式——人看手机就是看屏幕,不是看 XML。

决策层:Agent 根据当前状态和目标,规划下一步动作。这个动作不是“点击坐标 (320, 480)”,而是“点击登录按钮”这种语义级动作。语义动作到具体执行的映射由工具层完成,页面变了只需要更新映射,不需要改决策逻辑。

执行层:通过统一的动作空间(点击、输入、滑动、返回、等待等)操作设备。动作空间的设计直接决定了 Agent 的能力边界。

反馈层:执行后重新感知状态,判断目标是否达成。没达成则进入下一轮循环,达成则记录结果。失败时触发自我修正,比如换一种定位方式重试。

这个闭环跑起来之后,测试用例的形态就变了。不再是“步骤1、步骤2、步骤3”的硬编码,而是“目标:完成登录流程并验证首页加载”这样的声明式描述。Agent 自己决定怎么走。

2.3 方案选型的几个关键权衡

把通用 Agent 框架往移动端搬,绕不开几个选型决策。我把当时对比的维度整理成表,方便你参考。

维度纯视觉方案视觉+DOM 混合纯 DOM 方案
感知信息仅截图截图+结构树仅结构树
定位鲁棒性中(依赖视觉模型精度)高低(结构变动即失效)
实现复杂度高(需要强视觉模型)中低
对 WebView 支持好好差
对游戏/Canvas 支持好差不支持
Token 消耗高(图像 token 贵)中低
适合场景复杂 UI、游戏常规 App简单表单

我最终选的是视觉+DOM 混合为主、纯视觉为兜底的策略。原因很直接:常规 App 测试里,DOM 能提供精确的控件边界和文本信息,成本低;遇到 WebView、Flutter 自绘、游戏这些 DOM 拿不到内容的场景,自动降级到纯视觉。这个降级逻辑用 Agent 的 tool 选择机制实现,不需要人工干预。

提示:不要一上来就追求纯视觉。纯视觉方案对模型能力要求极高,而且 token 成本在批量测试时会失控。混合方案是性价比最高的起点。

3. 核心细节拆解:感知、动作空间与状态管理

3.1 感知层:截图与结构树怎么融合

感知层是整个 Agent 的眼睛。移动端和 Web 最大的区别是屏幕小、信息密度高、层级深。一张 1080x2400 的截图直接丢给模型,模型很难精确定位到某个小按钮。我的做法是分三步处理。

第一步,截图预处理。把原始截图按设备像素比缩放到模型友好的尺寸,通常宽度控制在 720 到 1080 之间。同时保留原始坐标映射关系,因为后续动作执行需要真实坐标。缩放比例记在上下文里,动作执行时反向换算。

第二步,结构树提取与裁剪。通过 accessibility API 拿到当前页面的控件树,每个节点包含类名、文本、resource-id、bounds、可点击性等属性。原始树往往有几百个节点,直接塞给模型会爆 token。裁剪策略是:只保留可见节点(bounds 在屏幕内)、只保留有文本或可交互的节点、合并同层级无意义容器。裁剪后通常能压到 30 到 80 个节点。

第三步,双通道对齐。把裁剪后的结构树节点按 bounds 在截图上标注序号,生成一张带标注的截图。模型看到的是“标注截图 + 节点列表”,节点列表里每个序号对应一个控件的文本和属性。这样模型既能看整体布局,又能精确引用某个控件。

# 感知层核心逻辑示意(伪代码) def perceive(device): screenshot = device.screenshot() tree = device.get_accessibility_tree() visible_nodes = filter_visible(tree, device.screen_bounds) interactive_nodes = filter_interactive(visible_nodes) merged = merge_containers(interactive_nodes) annotated_img = draw_annotations(screenshot, merged) node_list = serialize_nodes(merged) return { "image": annotated_img, "nodes": node_list, "scale": compute_scale(screenshot, annotated_img) }

这套感知逻辑跑下来,单次感知的 token 消耗大概在 1500 到 3000 之间,比纯截图方案省一半以上,而且定位精度明显更高。

3.2 动作空间设计:语义动作到原子操作的映射

动作空间是 Agent 的手。设计得好,Agent 操作流畅;设计得差,Agent 会频繁做出无效动作。我把动作空间分成三层。

语义动作层:Agent 直接输出的动作,比如tap(element_id)、input(element_id, text)、swipe(direction)、back()、wait(seconds)、assert(element_id, condition)。这一层是 Agent 的“语言”,要足够抽象,让模型容易理解和选择。

原子操作层:语义动作映射到设备能执行的原子操作。tap(element_id)需要先查 element_id 对应的 bounds,算出中心坐标,再调用设备的 tap 接口。input需要先 tap 聚焦,再调用输入法接口,最后可能需要收起键盘。

容错层:每个原子操作都包一层重试和降级。tap 失败(比如元素被遮挡)时,尝试滚动到可见区域再 tap;input 失败时,尝试清空后重新输入。容错层的存在让 Agent 不需要处理太多底层异常,专注在决策上。

语义动作原子操作序列常见失败与降级
tap(id)查bounds→算中心→tap被遮挡→滚动→重试
input(id, text)tap→清空→输入→收键盘输入法未就绪→等待→重试
swipe(dir)计算起止点→滑动滑动距离不足→加大距离
assert(id, cond)查节点→比对属性节点未出现→等待→重试

注意:动作空间不要设计得太细。我一开始把“长按”“双击”“拖拽”都做成独立动作,结果模型经常选错。后来合并成tap带参数、swipe带参数,模型的选择准确率明显提升。动作越少越抽象,模型越不容易犯错。

3.3 状态管理:怎么让 Agent 知道自己走到哪了

移动端测试有个容易被忽略的问题:Agent 需要知道自己当前在哪个页面、目标完成了多少、下一步该干什么。没有状态管理,Agent 会在页面间来回乱跳。

我的做法是维护一个轻量状态机,但不预设页面流转,而是让 Agent 自己维护“任务进度”。具体来说,上下文里始终保留三样东西:原始任务描述、已完成步骤摘要、当前页面状态快照。每轮循环开始时,Agent 先根据当前快照和任务描述判断“我是否已经完成”,没完成则规划下一步。

已完成步骤摘要很关键。它不是简单记录“点了登录按钮”,而是记录“已进入登录页,已输入账号密码,已点击登录,当前等待首页加载”。这个摘要让 Agent 在长流程测试里不会迷失。摘要由 Agent 每轮自己更新,我只需要在 prompt 里明确要求它维护这个字段。

状态管理还有一个作用是循环检测。如果 Agent 连续三轮感知到的页面状态几乎一样,且动作也相似,说明它卡住了。这时候触发干预:要么提示 Agent 换策略,要么直接判定失败并记录现场。这个机制救了我很多次,否则 Agent 会在一个死循环里烧掉大量 token。

4. 实操过程:从零搭一个移动端测试 Agent

4.1 环境准备与设备连接

先说环境。我用的是一台 Android 真机加一台 iOS 真机,通过 USB 连接开发机。Android 侧需要开启开发者模式和 USB 调试,iOS 侧需要配置好 WebDriverAgent。这些基础操作网上资料很多,不展开。

关键点是设备抽象层。不要让 Agent 直接调用 Appium 或 adb,而是包一层统一的 Device 接口,提供 screenshot、get_accessibility_tree、tap、input、swipe、back 这些方法。Android 和 iOS 各自实现这套接口,Agent 层完全不感知平台差异。

class DeviceInterface: def screenshot(self) -> Image: ... def get_accessibility_tree(self) -> list: ... def tap(self, x: int, y: int): ... def input_text(self, text: str): ... def swipe(self, x1, y1, x2, y2, duration): ... def back(self): ... class AndroidDevice(DeviceInterface): # 基于 uiautomator2 或 Appium 实现 ... class IOSDevice(DeviceInterface): # 基于 WebDriverAgent 实现 ...

这层抽象看起来简单,但价值很大。后面换设备、加平台、做并行测试,都只需要改这一层。

4.2 Agent 主循环的实现

主循环是整个系统的心脏。我用的是 ReAct 风格的循环,但做了针对测试场景的改造。每一轮的结构是:感知 → 思考 → 动作 → 观察 → 更新状态。

def run_test_agent(task_description, device, max_steps=50): context = { "task": task_description, "completed_steps": [], "history": [] } for step in range(max_steps): perception = perceive(device) context["current_state"] = perception decision = llm_decide(context) if decision["type"] == "finish": return {"status": "pass", "summary": decision["summary"]} if decision["type"] == "fail": return {"status": "fail", "reason": decision["reason"]} action = decision["action"] result = execute_action(device, action, perception) context["history"].append({ "step": step, "action": action, "result": result }) context["completed_steps"].append(decision["step_summary"]) if detect_loop(context): context["loop_warning"] = True return {"status": "timeout", "context": context}

llm_decide是核心。它的 prompt 里包含任务描述、当前感知结果、已完成步骤、历史动作。要求模型输出 JSON 格式的决策,包含动作类型、目标元素、参数、以及本步骤的摘要。用结构化输出约束模型,比让它自由发挥稳定得多。

4.3 一个完整测试用例的跑通记录

拿一个电商 App 的下单流程做验证。任务描述是:“打开 App,搜索‘无线耳机’,进入第一个商品详情页,选择黑色款式,加入购物车,进入购物车确认商品存在,提交订单到支付页面。”

第一轮,Agent 感知到首页,识别出搜索框。决策:tap 搜索框。执行成功。

第二轮,感知到搜索页,键盘弹出。决策:input “无线耳机”。执行时遇到问题——输入法没弹出来,input 失败。容错层重试一次,等待 1 秒后成功。

第三轮,感知到搜索结果,识别出第一个商品卡片。决策:tap 商品卡片。执行成功。

第四轮,感知到详情页,识别出颜色选择区域。这里有个坑:颜色选项是横向滚动的,黑色款式在可视区域外。Agent 第一轮决策是 tap 一个不存在的元素,失败。容错层返回“元素不可见”。Agent 收到反馈后,第二轮决策改为 swipe 横向滚动,然后 tap 黑色。成功。

第五轮到第七轮,加入购物车、进入购物车、确认商品,都顺利。

第八轮,提交订单。这里遇到登录拦截——App 要求先登录。Agent 感知到登录页,但任务描述里没提登录。这时候状态管理起作用了:Agent 判断“当前页面与任务目标不符,检测到登录拦截”,决策为“执行登录流程”。它自己规划了输入账号密码、点击登录的动作。登录完成后继续原任务。

整个流程跑了 14 轮,耗时约 3 分钟,token 消耗约 4 万。对比人工写脚本,这个用例的脚本大概 80 行,维护成本高;Agent 方案零脚本,任务描述 50 字。

实操心得:任务描述要写“目标”而不是“步骤”。写“进入购物车确认商品存在”比写“点击购物车图标,等待 2 秒,检查商品列表”效果好得多。Agent 自己会规划步骤,你写死步骤反而限制它。

4.4 断言与结果验证怎么做

测试没有断言就等于没测。Agent 场景下的断言有两种做法。

一种是显式断言,在任务描述里写明验证点,比如“确认购物车中商品数量为 1”。Agent 在感知到购物车页面后,会主动检查节点里有没有数量信息,然后判断是否满足。

另一种是隐式断言,Agent 在每轮决策时都会判断“当前状态是否符合任务预期”。如果发现异常(比如页面报错、商品下架),它会主动标记失败。

我倾向于两者结合。关键验证点用显式断言,保证不漏;过程异常用隐式断言,保证能捕获意外。断言结果统一记录到测试报告里,包含截图、页面结构快照、Agent 决策历史。这份报告在排查失败原因时非常有用。

5. 常见问题与排查技巧实录

5.1 Agent 卡在某个页面反复操作怎么办

这是最常见的问题。表现是 Agent 连续多轮感知到相似页面,动作也相似,但任务没有推进。原因通常有三个。

一是元素定位歧义。页面上有多个相似控件,Agent 选错了。排查方法是看感知结果里的节点列表,确认目标元素是否有唯一标识。如果没有,需要在感知层给节点加更多区分信息,比如父容器文本、相对位置。

二是动作执行失败但 Agent 不知道。比如 tap 了一个被遮挡的元素,系统没报错但实际没点到。排查方法是看动作执行后的页面快照,如果和动作前一样,说明动作没生效。解决方法是容错层增加“动作后状态校验”,没变化就返回失败。

三是任务描述有歧义。比如“进入设置页面”,App 里可能有多个入口。Agent 选了一个但走不通。解决方法是把任务描述写得更具体,或者允许 Agent 在失败后尝试其他路径。

我加了一个循环检测机制,连续三轮页面相似度超过阈值就触发警告,提示 Agent 换策略。这个机制把卡死率降了大概七成。

5.2 视觉模型识别不准小控件怎么办

移动端很多控件很小,比如关闭按钮、勾选框、开关。纯视觉方案下模型经常点偏。我的解决办法是视觉+结构树交叉验证。结构树提供精确 bounds,视觉提供语义理解。Agent 决策时引用的是结构树节点 ID,执行时用 bounds 算坐标,不依赖视觉坐标估计。

如果结构树也拿不到(比如 Flutter 自绘),那就放大局部区域。把可疑区域裁剪出来放大后再让模型识别,识别完把坐标映射回原图。这个技巧对游戏和自绘 UI 特别有效。

5.3 Token 消耗太快怎么优化

Agent 跑测试的 token 消耗主要在三块:感知结果、历史上下文、模型输出。优化也是从这三块入手。

感知结果优化:结构树裁剪要狠,只留有文本和可交互的节点。截图分辨率不要超过 1080 宽。节点列表用紧凑格式,别用 JSON 美化。

历史上下文优化:不要保留全部历史。只保留最近 5 轮的动作和结果,更早的压缩成摘要。已完成步骤摘要要精炼,一句话说清楚。

模型输出优化:用结构化输出约束,减少模型自由发挥。决策 JSON 的字段固定,不要让它输出解释性文字。

我优化前单用例消耗约 8 万 token,优化后降到 3 到 4 万,成本降了一半多。

5.4 常见问题速查表

问题现象可能原因排查方法解决方向
Agent 反复点同一元素动作未生效或定位歧义对比动作前后快照加动作校验、加节点区分信息
小控件点不准视觉坐标估计偏差检查 bounds 是否可用用结构树 bounds、局部放大
Token 消耗过高感知结果或历史过大统计各部分 token 占比裁剪结构树、压缩历史
长流程中途迷失状态摘要不清晰检查已完成步骤摘要强化摘要维护 prompt
遇到弹窗卡死未处理意外分支看感知结果有无弹窗节点加弹窗检测与处理策略
跨平台行为不一致设备抽象层未统一对比两端接口实现统一 Device 接口

避坑技巧:测试任务描述里加一句“遇到任何弹窗或权限请求,先处理再继续”。这一句话能省掉大量意外卡死。Agent 会自己识别弹窗并点击允许或关闭。

6. 这套方案适合什么场景,不适合什么场景

Agent 下沉到移动端测试,不是万能药。我用了几个月,摸清了它的能力边界。

适合的场景:流程长、分支多、UI 频繁变动的功能测试。比如电商下单、社交发帖、金融开户这类流程。这些场景人工写脚本维护成本极高,Agent 的语义级操作优势明显。还有探索性测试,让 Agent 随机操作并记录异常,能发现一些人工想不到的问题。

不适合的场景:性能测试、稳定性测试、需要精确计时的场景。Agent 的决策有延迟,每轮循环几百毫秒到几秒不等,做不了精确的性能断言。还有大量重复的回归测试,如果页面稳定、脚本维护成本低,传统脚本方案反而更快更便宜。

成本考量:Agent 方案的单次执行成本高于脚本,主要是 token 费用和设备占用时间。但如果算上脚本的编写和维护人力成本,在 UI 变动频繁的项目里,Agent 方案的总成本反而更低。我的经验是,一个用例如果每月需要修改两次以上,就值得用 Agent 方案。

混合策略:实际项目里我采用的是混合策略。核心稳定流程用传统脚本,保证速度和确定性;易变流程和探索性测试用 Agent,保证覆盖和适应力。两套体系共用设备抽象层和报告系统,统一管理。

最后分享一个我在实操中体会很深的点:Agent 测试的价值不在于替代脚本,而在于把测试的粒度从“操作步骤”提升到“业务目标”。当你用目标描述测试时,测试用例的可读性和可维护性会有质的变化。一个不懂代码的产品经理也能看懂“完成下单并验证支付页面”这样的用例,这在传统脚本时代是做不到的。这个转变带来的协作效率提升,可能比技术本身更有意义。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:30:43

WMSST结合MCNN-BiGRU的轴承故障诊断全流程实践

最近我把WMSST结合MCNN-BiGRU这条故障诊断路线从头到尾完整跑通了一遍,从时频变换的原理推导、Matlab代码实现,到网络结构设计、训练调参和对比实验,前前后后折腾了好几轮,踩了不少文档里查不到的坑。这篇文章就把这套方法的原理、…

作者头像 李华
网站建设 2026/10/20 3:30:42

从SGD到AdamW:深度学习优化器核心原理与实战避坑指南

训练深度学习模型这几年,我见过太多人把精力都花在网络结构设计上,却在优化器选择上特别随便。尤其当你从 SGD 一路试到 Adam、再换成 AdamW,会慢慢意识到一件事:优化算法不是流水线上一个无关紧要的旋钮,它几乎决定了…

作者头像 李华
网站建设 2026/10/20 3:30:39

RAG构建全攻略:从原理到实操、调优与避坑指南

做了大半年RAG相关的东西,从最初只会照教程把流程跑通,到后来能针对不同业务场景自己定方案、调参数、解决线上问题,中间踩的坑确实不少。今天这篇就围绕“RAG构建”好好梳理一遍,覆盖原理、实操、调优和常见问题排查,…

作者头像 李华
网站建设 2026/10/20 3:30:30

ArcGIS拓扑对齐边详解:消除多边形缝隙与重叠的完整流程

做地类图斑、宗地图、行政区划这类面状数据的时候,最让人头疼的就是相邻多边形的边界不咬合。明明两个图斑在实地是同一条边界,到了图面上却错开几厘米甚至几十厘米,产生一条条细长的缝隙,或者干脆叠在一起。这种问题在小比例尺下…

作者头像 李华
网站建设 2026/10/20 3:29:26

C语言链表逆序打印:栈、递归与内存管理详解

1. 题目拆解与思路定调1.1 这道题到底在考什么“剑指 Offer 06. 从尾到头打印链表”是链表专题里非常经典的入门题,也是面试高频题。表面上看只是“倒着输出一串数字”,但真正考察的点其实有三个层次:第一,你知不知道链表的物理结…

作者头像 李华
网站建设 2026/10/20 3:28:15

基于OpenIddict 6.4.0搭建企业授权UI管理平台实践指南

做.NET开发的,尤其是负责过企业内部系统对接的,大概率碰到过这种场景:系统越来越多,每个系统一套账号体系,登录来登录去,改密码还得挨个通知,员工离职后账号权限还得逐个回收。于是很多团队把OA…

作者头像 李华