
做手机 GUI Agent 最难的从来不是“把点击发出去”而是“让 Agent 知道自己该点哪里”。过去一年我在折腾这件事时把无障碍树和屏幕投屏像素两条通道都试了个遍最终沉淀出一套以无障碍树为主、投屏像素兜底的感知方案。这篇文章就是完整复盘包括为什么这么选、每一步怎么落地以及那些文档里不会告诉你的坑。先说结论如果你也准备做手机端 GUI Agent无障碍树绝对是性价比最高的感知主通道它结构化、低成本、易解释但树不是万能的遇到自定义绘制控件、WebView、动效刷新混乱的时候树会瞎报、漏报甚至直接超时。这时候屏幕像素流就成了最可靠的兜底——虽然它慢、笨、语义弱但它永远贴近“用户真正看到的东西”。两条通道配合起来Agent 的感知才谈得上可信。1. 为什么是“树像素”而不是二选一1.1 只靠无障碍树踩过的坑我最早做的版本是纯无障碍树方案通过系统无障碍服务拿到当前页面的节点快照把文本、坐标、可点击属性整理成一个结构化的界面描述然后交给决策模型去选目标节点。听起来很顺原生 Android 应用也确实跑得不错但一到真实场景就露馅了。第一类是“树里根本没有节点”。小程序框架、Flutter 的某些渲染引擎、游戏引擎、以及部分自定义重度绘制的 View要么不暴露无障碍节点要么暴露出来的节点缺少文本和语义信息。我印象很深的一次某自绘图表页面的整块内容只有一个根节点文本全是 null坐标是整个屏幕决策模型看到这样的数据基本等同于盲操作。第二类是“树有节点但内容是错的”。列表快速滑动时Android 的虚拟化机制经常让树里的文本和屏幕实际显示不一致。列表项还没来得及绑定新数据树里还是上一个位置的旧文案但截图里已经滚到下一屏了。Agent 信了树的结果按旧文案去匹配意图匹配率直接崩。第三类是性能问题。纯无障碍树的 Dump 动作非常昂贵最坏情况下一次要 500 毫秒到 1 秒。对响应式决策来说这个延迟已经大到没法忽略尤其是连续多步操作时界面状态早就在变化树引用的节点可能已经销毁或移出屏幕。用一个过期的树去做决策等于用上周的导航地图开今天的车。这三类问题单靠树通道是无解的。要么接受结构信息的残缺要么想别的办法补充真实视觉状态。1.2 只靠像素感知的更残酷现实于是我把主通道换成了投屏像素方案用 scrcpy 或 minicap 拿到实时屏幕帧对画面做目标检测和 OCR 识别反推出可交互控件的位置。思路没错但代价也非常直观。逐帧跑视觉模型对算力的消耗是树方案的几十倍不止。普通 OCR 加目标检测的组合在手机上做一次完整的界面理解轻松超过两秒这在交互 Agent 里基本不可用。而且像素坐标映射天然带误差物理分辨率到显示分辨率的换算、设备边框、投屏裁切、输入法弹窗引起的尺寸变化每一样都能让你的点击偏差几个像素。细小按钮和紧凑列表里的缝隙更是重灾区。更麻烦的是语义识别质量。OCR 只解决“文字是什么”不解决“文字能不能点”。一张截图里密密麻麻的文本哪些是可点击入口哪些是纯展示说明纯视觉方案很难判断准确。我试过用视觉模型硬训可点击区域识别小规模标注还行一换应用类型泛化能力立刻下降维护了个寂寞。所以我的结论很明确树和像素各有不可替代的价值。树提供准确的结构和语义像素提供真实的视觉快照。双通道组合不是锦上添花而是刚需。2. 无障碍树通道从主通道到可信通道2.1 获取树的几种路径与我的选择Android 阵营获取无障碍树的常用路径有三条自己写 AccessibilityService、调三方库的 Dump 接口、用 ADB 命令拉取。我最终采用的是以uiautomator2的 Dump 接口为基础、再包一层自己写的加工管线的路子。理由不复杂自己写 AccessibilityService 权限太敏感且要处理大量事件回调适合做系统级自动化框架不适合快速迭代的 Agent 项目ADB 命令方式延迟太高三方库的 Dump 接口已经在真实设备上被广泛验证过稳定性有保障。拿到底层 XML 之后我先做三件事解析、过滤、标准化。解析自然不必说过滤是把不可见节点、零尺寸节点、纯布局容器节点全部剔除。标准化则是把每个节点抽成一个统一结构体核心字段包括node_id、type、text、content_desc、bounds、clickable、visible、enabled、child_count。这里有个容易忽略的细节优先把content-desc作为语义文本的最大来源它常常比text更干净text兜底两者都没有的叶子节点再用可见子节点的文本做拼接。这样处理的好处是很多图片按钮和图标类控件的语义一下就补全了。2.2 把原始节点加工成 Agent 可用状态原始树并不能直接交给 Agent 决策因为信息太杂、太底层、太容易被噪音干扰。我做了三层加工。第一层是坐标归一化。原始bounds是基于设备物理分辨率的但同一套 Agent 要跑在不同分辨率的设备上所以我统一把坐标缩放到 0-1000 的相对坐标系决策模型只认这个相对坐标。投放执行时再乘回设备分辨率。这一步看起来简单实际救了我无数次换了台新设备Agent 完全不需要重新训练策略。第二层是候选节点筛选。屏幕上的可点击元素往往只有几十个但原始树里有几百个节点。我针对“当前任务可能操作的目标”做过滤保留clickabletrue的控件同时把它们的可点击区域放大 6% 到 8% 做容差抵消手指点击的物理误差。对非可点击但有明确语义的文本节点也会单独留一份做参考上下文。第三层是语义压缩。决策模型不可能读懂 500 个节点的完整 JSON我会把整理后的节点压缩成一行一行的文本表示类似一个排版好的列表每个候选者一行包含编号、文本、坐标范围。实验下来这种紧凑格式对 LLM 的提示和理解效率都远优于原始 XML。2.3 树通道的三个高频翻车点这个通道踩坑极多我挑影响最大的三个来讲。第一个是 Dump 超时。某些页面存在持续动画无障碍树的生成逻辑会一直处于忙碌状态Dump 请求迟迟得不到响应。处理办法是设超时阈值通常 3 到 5 秒还没返回就强制用像素通道接管。同时我自己做了一个缓存机制同页面的树在 0.5 秒内的重复请求直接复用缓存避免不必要的性能浪费。第二个是动态刷新导致的节点引用失效。比如 Swipe 列表滑动时列表项的节点 id 在树刷新后全部变化之前选中的节点瞬间变成空引用。我的做法是不只用 id 定位同时记录节点的语义特征和相对位置索引刷新后先在相邻帧里去匹配“相同文本相似位置”的节点匹配到了就复用原计划匹配不到才重新规划。第三个是动态小部件和弹窗的树污染。系统的悬浮球、Toast、底部弹窗有时候不会立刻出现在树里导致 Agent 以为界面还是旧的。这时候不能只信树必须用像素去做一次“画面变化检测”强制触发重试 Dump。后面讲融合策略时会展开说。3. 投屏像素通道不是截图是动态视觉巡检3.1 像素流怎么拿延迟比清晰度重要投屏像素方案的选型要优先考虑延迟而不是清晰度。Agent 是交互系统画面延迟每多 100 毫秒整个任务链路的误差就放大一倍。可选方案里scrcpy协议的综合表现最好它既能拿低延迟视频流又能反向注入触摸事件一举两得。minicap也能用但要额外解决触控注入问题。我建议的分辨率策略是默认请求 720p 的竖屏流必要时再升级到 1080p不建议直接上原生分辨率。原因有两个一是视频解码开销小、帧率高Agent 对画面的处理目标更多是“判断变化、确定区域、验证状态”不是做高清图片存档二是很多状态验证只需要看清局部区域全图高清纯属浪费算力。拿到视频流后我并不是把每一帧都送进模型而是做抽帧处理。正常情况下每 300 到 500 毫秒抽一帧作为感知输入在检测到界面剧烈变化或者 Agent 刚执行完点击操作时临时加速到每 100 毫秒一帧持续 1 秒左右。这种动态抽帧策略让像素通道的成本降到纯逐帧方案的 1/5。3.2 坐标映射与 ROI 裁剪模型像素流最关键的工程问题是坐标映射。视频流里的坐标和设备原始坐标之间是缩放关系缩放系数很容易算但坑在于很多设备有圆角、刘海、状态栏和底部手势条。如果不裁剪Agent 会误以为屏幕下方有可用空间把一个“半截按钮”当成完整按钮。我的做法是建立一份设备配置档包括屏幕原始尺寸、视频流尺寸、顶部刘海高度、底部安全区高度、左右黑边宽度。所有从像素通道得到的目标坐标都要经过这套配置投影回真实屏幕坐标。更实用的是 ROI 裁剪模型。界面理解不需要分析整张图只需要围绕目标节点或疑似变化区域做局部裁剪。比如树通道说某个弹窗按钮在坐标 (320, 600)像素通道只需要把 (300, 580) 到 (340, 620) 这个小区域裁出来做 OCR 验证既快又准。我给这个机制起了个内部代号叫“验证切片”后面融合策略里还会提到。3.3 像素通道到底“兜底”了什么像素通道不是做全页面的视觉理解它只负责四类任务。第一类是状态确认。点击操作发出后Agent 需要确认画面变化了没有。这时候截图对比上一个画面的差值区域就能快速判断是跳转新页面、弹出弹窗还是原地没响应。这比重新取树快得多。第二类是树缺失时的视觉入口发现。当树通道返回“无可点击节点”时像素通道会对整个画面做一次轻量目标检测识别明显的按钮外观区域把这些区域作为候选再配合 OCR 提取文案反哺给决策层。第三类是遮挡检测。某些悬浮窗、系统弹窗、Toast 会遮住目标区域树里不一定看得到但像素对比能发现前景覆盖。我会用“画面静止期多帧对比”的方法找出从无到有出现的前景区域。第四类是文本兜底验证。树节点的文本和像素 OCR 出来的文本不一致时以 OCR 为准。比如某些 WebView 里节点文本是对的但实际渲染被 CSS 截断或隐藏OCR 能暴露真相。4. 双通道融合策略互相纠错而不是简单拼接4.1 主从分工与触发切换条件双通道不能做成“两个结果同时上屏让决策模型二选一”那样信息冲突太多模型会犯迷糊。我的设计是严格的主从结构默认以树通道为唯一决策依据像素通道只做验证和兜底但在特定条件下允许像素通道“夺权”。触发切换的三大条件是硬编码规则树 Dump 超时、树节点全空或全不可点击、以及 Agent 刚做完一次点击后像素验证发现画面纹丝不动。这几种情况本身就是树通道不可信的强信号此时直接进入像素主导模式Agent 所有的决策输入都来自视觉结果直到下一个稳定页面被成功构建出树。我一开始也想过用置信度分数来决定由哪个通道主导实现后发现太复杂而且置信度本身不可靠。后来改成条件触发加超时熔断简单粗暴但稳定得多。Agent 系统的第一要务从来不是高深算法而是确定性。4.2 跨通道对齐打分机制当树和像素同时可用时我会对每个候选节点做一次对齐打分分数由四个维度组成文本匹配度、坐标重合度、尺寸接近度、时间新鲜度。文本匹配度用编辑距离的归一化值1.0 表示树文本和像素 OCR 完全一致。坐标重合度用两个通道给出的目标框的 IoU交并比来衡量重合越高分越高。尺寸接近度看目标框长宽比例是否相似防止树里的 clickable 区域和实际按钮视觉面积差太远。时间新鲜度则惩罚过期数据树的 Dump 时间离现在越近分越高。只有当对齐分数超过阈值时节点才会被正式加入候选集未达到阈值的节点进入“待观察区”由像素通道继续跟踪两帧再做决定。这套评分的最大价值是减少决策模型被脏数据误导的概率。分数低于 0.7 的节点决策模型看了也会犹豫不如直接过滤掉。4.3 一个实际执行流程的走读我拿“点击设置页的 WLAN 入口”这个动作来走一遍流程。Agent 先向树通道请求当前页面结构拿到一个包含多个候选节点的列表其中 WLAN 节点的文本是“WLAN”坐标在屏幕约 28% 高度处。这不是最终答案。像素通道立刻裁出以该坐标为中心的一小块验证切片对切片做 OCR识别出文字确实是“WLAN”且按钮轮廓可见。对齐评分通过节点被采纳。Agent 发送点击。300 毫秒后像素抽帧对比发现画面变化明显确认点击已生效。整套流程下来树负责给我答案像素负责给我底气。如果切片 OCR 识别结果不是“WLAN”而是“Wi-Fi”评分不达标节点不会进入候选集。Agent 转而检查树里有没有其他语义相近的节点同时像素通道扩大扫描范围寻找名称相近的其他入口。这种互相纠错的模式单靠任何一个通道都实现不了。5. 落地实战问题清单与参数调优5.1 我踩过的十个典型坑双通道系统在真实设备上跑稳定需要跨过不少暗礁。我把高频问题整理成一张速查表。症状根因解决方式树 Dump 偶尔返回空数据页面正在重建或动画未结束加 1-2 秒重试延长超时上限相机类页面点击位置偏移表面尺寸和实际触摸区域不一致优先使用content-desc覆盖的 bounds不用整块区域像素流画面扫码区过暗视频编码码率太低导致暗部失真提升码率到 5Mbps 以上或改用 RAW 帧点击后树和画面状态不一致页面跳转动画仍在播放动画结束后再请求新树等待时间约 300 毫秒WebView 里文本缺失前端未暴露无障碍语义切换到像素主导OCR 全屏读取输入法弹出导致树错乱布局 resizing 触发了树的重建输入法弹窗期间暂停取树用像素判断输入框快速滑动时树内容滞后列表虚拟化导致数据绑定延迟滚动中只信像素等静止后再恢复树通道多页面栈存在遮挡层弹窗和底层页面同时在树中取树时保留最顶层窗口的信息过滤底层低频设备解码卡顿硬解码器不支持当前编码格式降级到更低的帧率和分辨率兜底树引用节点已失效刷新过程中节点 id 全部变化用语义特征与坐标位置做节点重匹配这些问题没有一个能让系统完全瘫痪但积累多了Agent 的成功率会从 98% 一路掉到 70% 以下。排查的顺序我建议是先看树是否新鲜再看坐标是否对齐最后查像素流是否退化。按这个顺序能省一半时间。5.2 可复用的参数与调优建议参数调优这块我不给死数字只给每个参数的调节思路。超时阈值建议设置成动态的不同设备性能差异很大中端机能跑进 3 秒的 Dump 重型机可能要 6 秒。我的做法是记录最近十次 Dump 耗时取 90 分位数再乘 1.2 作为当前超时阈值。像素抽帧间隔默认 400 毫秒是最稳的既能及时发现页面变化又不会给解码器和 CPU 带来持续高压。如果 Agent 运行设备性能较好可以缩到 250 毫秒性能较差的旧设备宁可放大到 600 毫秒也不能让系统发热降频否则整个任务都会卡死。坐标容差按目标控件尺寸的 8% 到 12% 来设置。小按钮取偏大值大按钮取偏小值避免把小按钮的点按误判成相邻按钮。还有一个容易被忽略的参数是“稳定判定帧数”像素画面连续两帧差异低于阈值才算静止动画比较重的应用建议三帧否则会在动画中间误判为静止。更重要的是一条元原则优先级永远是树优先但树的权威性来自它的新鲜度和校准质量。每次新页面构建完成后我都会用像素通道快速做一次“树校检”——随机抽 3 到 5 个候选节点加验证切片确认树和画面一致才标记为可信状态。这个过程耗时约 50 毫秒对决策质量影响却是决定性的。最后分享一个小技巧。给 Agent 配感知日志时一定要同时记录树通道的原始节点数和像素通道的状态确认结果。出问题复盘时联合这两份日志能很快定位到是感知层的责任还是决策层的责任。我见过太多人日志只记决策层的思考过程感知层出了问题全是一团黑复盘效率极低。这个双通道方案后续还可以继续扩展。目前我这边已经有人在尝试加入音频事件的感知通道用来判断语音助手、视频播放的状态。不过我个人觉得先把手头的树和像素融合做到极致比急着加第三通道更有价值。感知是地基地基稳了上层决策才有发挥空间。