大语言模型没法点屏幕。要驱动一台安卓手机,它需要一套框架:把屏幕变成模型能读懂的东西,再把模型的决定变成触控和按键事件。研究用的框架、测试工具,以及替用户预订或下单的产品,用的都是同一个循环的某种变体。
本文一步步拆解这个循环,以及它底层的ADB命令,然后讲MCP处于什么位置,以及每个真实项目都会遇到的失败:弹窗、权限、日期、长列表,以及Agent根本读不了的屏幕。
循环
手机上Agent的每一步都有四个部分:
- 观察。截取当前屏幕:截图、UI树,或者两者一起。
- 决策。把目标、之前几步的简短历史和当前观察结果发给模型。模型回复一个结构化的操作:点这个元素、输入这段文字、滑动、按返回、打开某个应用、等待,或者给出答案并结束。
- 执行。框架把这个操作转换成手机上的输入事件。
- 检查。再观察一次,确认屏幕按操作预期发生了变化。
写成代码,骨架很短:
steps = 0
while steps < max_steps:
obs = phone.observe() # screenshot + elements with text and bounds
action = model.next_action(goal, history, obs)
if action.kind == "done":
return action.answer
phone.act(action, seq=obs.seq) # refuse if the screen changed since obs
history.append((obs.summary, action))
steps += 1难的部分藏在observe和act里,还有你往history里放什么。历史要精简:每一步一行,写明操作和变化就够了。把之前每一步的完整截图都塞进去只会消耗上下文,对模型做决定没有帮助。
看屏幕:像素和UI树
把屏幕展示给模型有两种方式,大多数实际可用的Agent会把两者结合起来。
| 来源 | 提供什么 | 在哪里失效 |
|---|---|---|
| 截图 | 一切可见内容,包括图标和图片;需要视觉模型 | 小目标、长得很像的图标、精确坐标 |
| UI树 | 文字、resource id、类名、边界、是否可点击;精确且成本低 | 游戏和画布绘制、部分WebView、没有无障碍标签的自定义视图 |
| 带编号元素的截图 | 一张提供上下文的图,外加每个目标的精确编号 | 元素重叠的拥挤页面 |
用纯ADB,原始素材是这样的:
adb exec-out screencap -p > screen.png adb shell uiautomator dump /sdcard/ui.xml && adb pull /sdcard/ui.xml
导出结果里包含每个节点的text、resource-id、content-desc和bounds。点击节点边界的中心,比让视觉模型给出原始坐标可靠得多。有一个坑:UI导出会等屏幕进入空闲状态,而一直在动的屏幕,比如正在播放的视频或跳动的计时器,可能让它失败。这时框架应该退回到截图。
执行:点击、文字和时机
基础的输入命令:
adb shell input tap 540 1650 adb shell input swipe 540 1600 540 600 300 adb shell input text "hello%sworld" adb shell input keyevent KEYCODE_BACK
注意input text里的%s:它代表空格,而且这个命令处理不了大多数非ASCII字符。需要输入其他文字的框架,会把文字放进剪贴板再粘贴。
可靠的Agent和不稳定的Agent,差在三个细节上:
- 先聚焦再输入。先点输入框,再输入文字。这时键盘大约会盖住屏幕下半部分,所以下一次点击前要重新观察。
- 等稳定,别干等。执行操作后,等到屏幕不再变化,最多等几秒。固定的sleep不是浪费时间,就是在页面加载完之前就动手。
- 拒绝过期的操作。如果模型所依据的观察之后屏幕变了,操作可能点错元素。好的框架会给每次观察打上序号,拒绝根据旧观察算出来的操作,逼它重新看一眼,而不是去猜。
MCP处于什么位置
模型上下文协议(Model Context Protocol,MCP)是给模型提供工具的标准方式。手机的MCP服务器会暴露列出设备、观察屏幕、执行操作这类工具,这样任何MCP客户端(Claude Code、Codex、Cursor等)都能驱动手机,不需要定制胶水代码。循环本身不变,MCP只是把调用标准化了。
控制分两个层级。在底层,你的Agent调用observe和act,每一步都自己决定。在高层,它把一个目标交给运行在手机旁边的Agent:“打开应用,把一袋2 kg的大米加入购物车,报告总价”。Clousd两个层级都支持:通过REST API和MCP服务器使用observe和act;以及任务,你用大白话给手机一个目标,Clousd的Agent完成各个步骤并返回答案。AI Agent页面把两者放在一起对比。
什么会让Agent出错
弹窗和打断
权限对话框、“给这个应用评分”提示、更新通知、新手提示、WebView里的Cookie横幅,以及“应用无响应”对话框,都会在随机时刻出现。盲目按计划走的Agent会点错东西。在提示词里加一条规则:继续执行计划之前,先关闭任何不属于任务的对话框。在测试环境里,用adb shell pm grant com.example.app android.permission.POST_NOTIFICATIONS提前授予权限。
因Android版本而异的权限
Android 13加入了通知的运行时权限。Android 14加入了照片部分访问,用户可以选“选择照片”而不是全部允许。位置权限有“仅在使用该应用时允许”和“仅限这一次”。同一个任务在不同版本上会弹出不同的对话框,所以要在不止一个版本上测试Agent。
日期和时间
“下周二加一个会议”取决于手机的时钟和时区,而模型对“今天”的认知来自训练数据。在每次观察里都带上设备的日期、时间和时区。检查带日期结果的测试套件,需要把手机时钟设成任务预期的样子。
长文本和长列表
输入好几段文字会耗光步数预算。在一个需要滚动五次的列表里数条目,或者找出重复项,都要求Agent记住它已经看过什么。这些是Agent目前最常失败的任务,我们基准测试页面上的AndroidWorld结果按类型拆解了这些失败。
Agent读不了的屏幕
给窗口设置了安全标志的应用,在银行和密码页面很常见,截图出来是一片黑。在画布上绘制的游戏没有UI树。围绕这些限制来规划任务,别让Agent对着一块空屏幕兜圈子。
在多次运行之间泄漏的状态
上一次运行留下的登录会话、缓存数据和通知,会改变Agent下一次看到的东西。做评估时,每次运行之间把手机重置到全新状态,让每次尝试都从同一个起点开始。
建立一个有意义的评估
- 一组固定的任务,每个任务都有一个可以从外部检查的结束状态:数据库里的一行、一个文件、最顶层的activity(
adb shell dumpsys activity top)。 - 每个任务只尝试一次,步数预算固定。重试会掩盖真实的失败率。
- 每一步都保存一张截图和对应的操作,这样你能看懂任务为什么失败。
- 两三款手机型号和Android版本。在一种屏幕尺寸上能用的Agent,换一种往往就失败。
对于屏幕之外还需要的检查,同一台手机也可以通过安全中继用ADB连接。


