
很多人第一次看手机上的 AI 替你点屏幕这类演示第一反应都是模型又变强了。我自己动手搭过几套自动化操作流程之后越来越确定这个判断是错的。真正把建议联系人工和自己动手把事情办完分开的不是参数量而是有没有一条完整的闭环链路观测屏幕 → 生成动作 → 执行动作 → 再观测验证。你问一个只会聊天的 AI帮我把这个自动续费关掉它会输出一段图文教程一个真正的AI agent它会截图、认出那个开关、点下去、再截图确认开关已经变灰。这三步里AI大模型只是其中一个零件剩下的全是工程活。这篇东西写给两类人一类是想知道自己手机上的 AI 为什么看着聪明其实不会干活另一类是准备自己动手做屏幕操作自动化的开发者我会把从环境准备到误点回滚的完整链路拆开讲。1. 建议联系人工和自己动手点之间的那道墙1.1 纯对话型 AI 到底缺了哪两样东西把大模型接进业务系统最省事的做法就是给它一个输入框和一个输出框。用户说一句话模型回一段话中间最多挂个知识库或者几个查询接口。这套架构我做过很多次它在回答问题这个任务上确实够用但一旦用户的需求变成帮我把事情办了它立刻就露出短板。短板不在语言能力而在通道。它只有语言输入通道和语言输出通道没有屏幕观测通道也没有动作执行通道。它能知道设置里有自动续费选项因为训练数据里有但它不知道此刻你手机屏幕上那个开关是开的还是关的也不知道它在屏幕的第几行第几列。这就是为什么它最后只能吐出一句具体路径可能因版本不同建议联系人工客服——这句话不是它偷懒是它真的没有任何手段去确认状态。还有一个更隐蔽的问题没有状态。对话型 AI 每次回答都是一次性的它不承担上一步做错了下一步要修正的责任。而操作屏幕这件事天然就是一个多步、有状态、会出错的流程。你点错了一个按钮就必须回到上一页重来这个回到上一页的决策纯对话模型是给不出来的。提示判断一个产品是不是真的在做 agent看它有没有回看自己上一步结果的能力。只能一次性输出、不能根据执行结果调整下一步的本质上还是问答系统套了个壳。1.2 能点屏幕的 Agent 多做的三件事真正的屏幕操作 Agent在对话模型之外至少多做了三件事缺任何一件都跑不起来。第一件是屏幕观测。它需要一个稳定的方式把当前屏幕状态读进来。常见的有两条路一条是截图把图像交给多模态模型或者 OCR 去理解另一条是读系统的无障碍树Accessibility Tree拿到每个控件的位置、文本、可点击属性。前者通用但慢后者快且精确但对自绘界面无能为力。生产环境里我一般是两条路都用无障碍树优先树里找不到目标再退回截图识别。第二件是动作生成。模型不能输出帮用户点击设置按钮这种自然语言必须输出机器能执行的结构化指令比如{action: click, x: 540, y: 1820}或者{action: click, element_id: 17}。这一步是整个系统里最考验设计的地方后面会单独展开。第三件是结果验证。动作发出去之后必须再截一次屏判断我期望的变化发生了没有。期望开关变灰结果没变那就说明这一步失败了需要重试或者换策略。这一步是能演示和能上线的最大分界线绝大多数玩具级 Demo 死在这里。把这三件事串起来就是经典的那个循环截图 → 推理 → 动作 → 截图 → 判断是否达成目标 → 达成则结束未达成则继续。听起来简单但每一步都有大量细节能把人坑到怀疑人生。2. 拆开一个点屏幕的 Agent观测层、决策层、执行层2.1 观测层把一屏像素翻译成可操作元素观测层的输出质量直接决定了后面所有环节的天花板。我见过太多项目模型明明没问题就是观测层喂进去的信息太烂导致 Agent 一直在猜。目前主流做法有四种各有明确的适用边界方案能拿到什么优点明显短板无障碍树控件类型、文本、坐标、可点击性精确、快、不耗流量游戏、H5 自绘界面基本拿不到截图 OCR屏幕上的文字及其位置通用纯文本界面友好拿不到图标语义无文字按钮抓瞎截图 多模态模型整体语义理解能认图标最接近人类理解慢、贵、坐标定位精度不稳混合方案树 图 OCR 融合覆盖率最高工程复杂度高需要去重合并我在实际项目里采用的是混合方案先拉无障碍树把可点击节点全部提取出来给每个节点编号然后把截图缩放后连同节点清单一起交给模型。模型如果能在清单里找到目标就直接返回节点编号如果找不到比如目标是个自绘图标才让它直接给归一化坐标。这样既保证了常见场景的精确度又保留了对特殊界面的兜底能力。一个容易忽略的细节是屏幕分辨率。不同机型分辨率差别巨大模型如果直接吃绝对像素坐标换个设备就全偏了。所以坐标一定要归一化统一映射到 0 到 1000 的区间执行的时候再乘回真实分辨率。这个转换看起来是小事但它是跨设备可用的前提。2.2 决策层模型该怎么写出下一步决策层的核心问题是给模型的输出空间应该多大。这里有个反直觉的经验——动作空间越窄Agent 越可靠。我早期的版本让模型自由输出 JSON结果它经常自创字段比如输出{action: long_press_and_drag}这种我根本没实现的动作或者把坐标写成x: 大概中间偏上。后来我把动作集合硬约束成有限的几个click、swipe、input_text、press_back、wait、done、fail。模型只能在这几个里选参数格式也做了强校验。改完之后无效输出的比例从百分之十几掉到了几乎为零。另一个关键是给模型足够的上下文。只给当前这一屏它不知道我从哪来、我做到哪一步了。我的做法是在提示词里带上最近三步的动作和结果再加上一句当前任务目标。比如prompt f 你是手机操作助手。当前任务{task_goal} 已完成步骤{history_summary} 当前屏幕可交互元素 {element_list} 请输出下一步动作只能是以下 JSON 之一 {{action:click,id:元素编号}} {{action:click,x:0-1000,y:0-1000}} {{action:input_text,id:元素编号,text:...}} {{action:swipe,x1:..,y1:..,x2:..,y2:..}} {{action:press_back}} {{action:wait,seconds:2}} {{action:done,reason:...}} {{action:fail,reason:...}} 这段提示词里最重要的其实是最后两个动作done和fail。没有它们Agent 会永远循环下去因为它不知道什么叫做完了。让模型有明确的宣告结束的出口是控制成本和控制风险的基础。2.3 执行层动作到底怎么落到屏幕上执行层是最枯燥但最不能出错的一层。Android 上最通用的方案是 ADB因为不需要在设备上装任何东西插上就能用。# 获取屏幕分辨率用于坐标换算 adb shell wm size # 截屏并直接拉回本地注意用 exec-out 避免换行符损坏 adb exec-out screencap -p screen.png # 点击坐标为真实像素 adb shell input tap 540 1820 # 滑动 adb shell input swipe 540 1600 540 600 300 # 查看当前前台应用用于判断我在哪个页面 adb shell dumpsys window | grep mCurrentFocus # 保持屏幕常亮避免跑到一半息屏 adb shell svc power stayon true几个必须知道的坑adb shell input text不支持中文和空格输入中文要么用 ADBKeyboard 这类输入法要么先设剪贴板再触发粘贴。空格需要转义成%s。另外input tap是瞬时点击某些需要长按的控件要改用input swipe x y x y 800来模拟长按。如果是 Python 项目我会用 uiautomator2 而不是裸调 ADB因为它把元素查找、等待、点击封装好了还能直接读无障碍树省掉大量重复代码import uiautomator2 as u2 d u2.connect(设备序列号) d.settings[wait_timeout] 10.0 # 直接按文本点击内部会自动等待元素出现 d(text设置).click() # 读取当前页面的全部可交互元素用于喂给模型 for node in d.dump_hierarchy(): pass # 解析 XML提取 clickabletrue 的节点执行层还有一个经常被忽视的事情执行完要等一下再截图。界面有动画点完立刻截图往往拍到的是过渡状态导致验证逻辑误判。我的做法是在每个动作后固定等 0.8 到 1.2 秒再对页面做一次是否还在变化的检测稳定了再截图。3. 从零搭一个最小可用的屏幕操作 Agent3.1 环境准备里那些没人告诉你的细节在开始写代码之前有几件事必须先弄好否则后面会浪费大量时间在莫名其妙的错误上。设备端的开发者选项和 USB 调试要打开这个大家都知道。但**USB 调试安全设置**这一项经常被忽略——不打开它模拟点击会被系统拦截表现为命令执行成功但屏幕毫无反应。这个问题我第一次遇到时排查了两个多小时一直以为是坐标算错了。屏幕刷新率和息屏策略也建议提前固定下来。跑长流程的时候屏幕一会儿熄一会儿亮截图内容完全不可控# 延长息屏时间跑流程期间不要让屏幕睡过去 adb shell settings put system screen_off_timeout 1800000 adb shell svc power stayon true # 固定刷新率减少截图撕裂和动画抖动带来的干扰 adb shell settings put system peak_refresh_rate 60 adb shell settings put system min_refresh_rate 60还有输入法的问题。如果你的流程涉及输入文字建议先在设备上装一个可以通过 ADB 广播直接注入文本的输入法并把它设为默认同时关掉输入法的联想、纠错和自动大写否则你输入的内容会被输入法自作主张地改掉。这个坑在自动化登录场景里特别常见验证码输对了却被输入法改了大小写然后一直提示错误。3.2 主循环的代码骨架把前面三层串起来主循环其实不长但每一行的顺序都不能乱import time, base64, json def run_task(goal, max_steps25): history [] for step in range(max_steps): # 1. 等界面稳定后截图 wait_until_stable() img capture_screen() elements parse_accessibility_tree() # 2. 组装提示词交给模型决策 action llm_decide(goal, history, elements, img) # 3. 执行 if action[action] done: return True, action.get(reason) if action[action] fail: return False, action.get(reason) execute(action) # 4. 验证 time.sleep(1.0) ok verify(action, capture_screen()) history.append({action: action, success: ok}) # 5. 连续失败要刹车别让它无限刷 if len(history) 3 and not any(h[success] for h in history[-3:]): return False, 连续三步未生效主动终止 return False, 达到步数上限骨架里有三个地方是保命的wait_until_stable防止拍到过渡帧max_steps防止死循环烧 token连续三步失败主动终止防止把界面点成一团乱麻。最后这一条我强烈建议每个人都加上因为 Agent 一旦陷入错误的循环它会非常有耐心地一直点下去点到账户被风控为止。3.3 让 Agent 知道我在哪一页多步任务最容易出问题的地方是 Agent 走着走着就不知道自己走到哪了。人类操作手机时心里有个地图我在设置页 → 进到应用管理 → 进到某个应用详情。Agent 没有这个地图它每一步都是重新看图。解决办法是给每个页面做一个指纹。最简单的指纹是包名 Activity 名 页面顶部三条文本做一次哈希。Agent 每次截图后先算指纹在历史记录里查一下这个指纹出现过没有。如果同一个指纹在最近几步里出现了两次基本可以判定——它在原地打转走进了死循环。这个机制非常划算几十行代码就能挡住一大类故障。更进一步可以把指纹和这个页面上有哪些可点元素一起缓存起来同一个页面第二次进来直接复用之前的元素清单省掉一次模型调用成本和延迟都能降下来。4. 点错了之后怎么办才是真功夫4.1 故障的三种典型征兆Agent 出问题不是随机的它有非常明显的征兆。摸清这三种排查效率能提高一大截。第一种是点击无响应。命令返回成功屏幕纹丝不动。原因通常是坐标超出了实际可点区域比如点到了状态栏、控件此时不可点击、或者有透明遮罩层挡住了。排查方法是把点击坐标画到截图上肉眼看看到底点在了哪。第二种是原地打转。两个页面之间来回跳或者一直在同一个页面重复同一个动作。这多半是验证逻辑写错了——Agent 以为没成功实际上已经成功了于是又退回去重做做完又觉得没成功。这种模式会非常安静地消耗掉所有步数。第三种是越点越偏。前几步正常后面开始点一些完全不相关的元素。这种情况十有八九是坐标换算出错比如截图被缩放过、屏幕有旋转、或者挖孔屏/虚拟导航栏导致截图尺寸和实际触控区域不一致。有旋转需求的机型比如平板尤其要当心adb shell wm size返回的尺寸和截图尺寸可能不一致必须实测校准。4.2 每一步都要能对账我在后来的版本里加了一个强制规则任何一个动作发出前先写下预期结果动作发出后必须验证预期是否成立。没有写成预期的动作不允许执行。具体做法是让模型在做决策时多输出一个字段{ action: click, id: 12, expect: 页面标题变为自动续费管理 }然后验证环节拿expect和实际截图做对比。对比可以很简单检查关键文本是否出现在屏幕上就够了。对不上的时候不要立刻重试而是先做一次当前在哪的判断——因为很可能动作其实生效了只是页面变了样。确认确实没生效再重试而且重试时要在提示词里明确告诉模型上一次点击没有效果请换一个元素或换个策略。这比无脑重试有效得多。4.3 三类中断的处理方式真实环境里流程被中断是常态。我总结下来主要是三类每一类都要有独立的处理逻辑。超时类网络请求、页面加载超过了预期时间。处理方式是分段等待加轮询不要用一次性的长 sleep。每隔半秒看一次目标元素出现没有出现了立刻继续最多等十秒。弹窗类广告弹窗、版本更新提示、权限申请、青少年模式提醒。这类最烦人因为它会随时冒出来。我的做法是维护一个弹窗特征库每次截图前先跑一遍检测命中就用统一的关闭逻辑处理掉然后再继续主流程。特征库用文本关键词加控件类型匹配比如页面上同时出现跳过和关闭两个按钮就认为是可关闭弹窗。验证类滑动拼图、短信验证码。这一类我的建议很明确——不要试图绕过。技术上很难做稳而且绝大多数平台的服务条款都禁止自动化绕过验证机制。合理的做法是检测到验证环节就暂停流程把控制权交还给用户等用户手动处理完再继续。Agent 的定位应该是替人做重复劳动不是替人过验证。5. 把大模型约束成一个老实员工5.1 动作空间要窄参数格式要死提示词工程在 Agent 场景里的作用和聊天场景完全不同。聊天场景追求发挥Agent 场景追求收敛。你希望模型像一个非常守规矩的员工只在规定范围内做事。除了把动作枚举限制住参数格式也要做双层校验。第一层是模型输出时用结构化格式约束第二层是代码里再做一次硬校验坐标超出 0 到 1000 范围、元素编号不在清单里、文本长度为 0一律判为无效输出直接走重试而不是往下执行。我踩过一次坑模型输出了y: 2500这种越界坐标代码没校验换算完直接点到了屏幕外后面所有推理都建立在错误状态上。还有一个细节不要在提示词里给模型留自由发挥的口子。早期我在提示词末尾加了一句如有其他更合适的操作也可以使用结果模型开始输出各种我实现不了的自定义动作。把这句话删掉之后异常输出立刻消失了。约束越明确模型表现越稳。5.2 坐标和元素编号到底选哪个这是设计 Agent 时最纠结的一个取舍我的结论是两者都要但优先级不同。元素编号优先。因为编号背后是真实的控件有明确的边界和属性点击命中率高而且可以做元素存在性校验。编号的缺点是覆盖不全自绘界面、画布、游戏里拿不到。坐标兜底。当模型在元素清单里找不到目标时允许它输出归一化坐标。但坐标点击之后必须额外做一次验证因为这个动作的失败率明显更高。同时我建议在坐标模式下给模型提供网格辅助——在截图上叠加一层淡色的九宫格或十六宫格并标上编号模型对第几行第几列的判断会比直接给绝对坐标准很多。两种模式混用的时候要保证元素编号和坐标在同一个坐标系里换算否则会出现编号点对了、坐标点偏了的诡异现象。5.3 一个可以直接抄的重试提示词失败之后的第二次尝试提示词要明显不同于第一次。下面这段是我用了很久的模板效果比简单重发好很多上一次尝试{last_action_json} 执行结果未达到预期期望{expect}实际页面{actual_summary} 请分析可能原因并从以下策略中选择一种重试 1. 换一个语义相近的可点击元素 2. 先滚动页面让目标进入可视区域再点击 3. 先返回上一级再重新进入 4. 等待更长时间后重新尝试 如果判断任务无法完成请输出 fail 并说明原因。 禁止重复上一次完全相同的动作。最后那句禁止重复上一次完全相同的动作很关键。没有它模型在失败后大概率会把一模一样的动作再输出一次白白浪费一轮。加上之后它会主动去换策略。6. 哪些场景值得让 AI 点屏幕哪些纯粹是自找麻烦6.1 值得做的三类场景第一类是高重复、低变异的批量化操作。比如在一批设备上重复执行同一套初始化设置、批量给测试机安装配置、批量导出某个应用里的数据。这类任务流程固定页面变化小Agent 的准确率会非常高投入产出比也最好。第二类是跨应用的数据搬运。从一个应用里读出信息切到另一个应用里填进去。这类任务人做起来极其厌烦但每一步都很明确非常适合交给 Agent。我做过一个把订单信息从聊天记录搬到表格里的流程人工一天做两百条Agent 跑一晚上能做完还不用休息。第三类是测试用例的自动回放。把人工写好的操作步骤交给 Agent 执行每一步加断言跑完输出结果。这类场景的好处是即使 Agent 某一步走错了影响也局限在测试环境里风险可控。6.2 碰之前必须想清楚的红线有几类场景我会直接劝退。涉及支付、转账、签约、删除数据的操作不要让 Agent 独立完成。这类操作的结果不可逆一次误点的代价可能是真金白银。如果确实有必要做至少要加一道人工确认环节让 Agent 走到最后一步时停下来等人按确认。涉及个人隐私数据批量读取和导出的也要非常谨慎。即使技术上跑得通也要确认你做的事情符合平台规则和数据合规要求。再就是上面提到的验证机制。绕过验证这件事技术上不稳定规则上也不允许我的建议是直接不做遇到就交还给人。注意Agent 的能力边界不只是技术边界。一个技术上能跑通的流程不等于可以放手让它自动跑。上线之前先把最坏情况会发生什么想一遍。6.3 模型放本地还是调云端最后一个绕不开的问题是模型放在哪。如果全部走云端大模型延迟是一个绕不过去的坎。一次决策往返加上截图上传两三秒很正常一个二十步的任务就要一小分钟。页面变化快的应用等你决策完页面早就不一样了。我目前的组合是分层简单的元素定位和判断放在本地小模型或者纯规则里做比如页面上有没有这段文本这个按钮的可点击属性是什么这些根本不需要大模型只有需要语义理解和策略决策的时候才调用大模型。这样大部分步骤是毫秒级的只有关键节点走一次模型整体延迟能压到可以接受的范围。另外提醒一句无论本地还是云端截图里可能包含账号、手机号、地址等敏感信息。如果走云端要么先做区域遮挡要么只上传必要的元素清单而不是完整图像。这一点在做企业级流程的时候尤其要注意别等出了问题再补。本地部署的另一个好处是可控。你可以固定模型版本不用担心某天服务端模型悄悄升级导致提示词全部失效、本来能跑通的流程突然开始乱点。这个坑我实实在在踩过——某次模型更新之后它对点击和轻触的理解变了输出格式也跟着变了一晚上跑挂了几十台设备。跑到现在我的体会是这套东西的难度分布非常反直觉写主循环只花了我一个下午让它在真实环境里稳定跑完一整套流程花了我大半个月。这大半个月里绝大部分时间不是花在模型上而是花在等界面稳定、处理弹窗、校准坐标、给失败动作想兜底策略这些看起来一点都不智能的地方。如果你准备动手做类似的东西我的建议是从一个极度确定的窄场景开始把失败率和验证逻辑磨透再考虑往复杂流程上扩。先把一件事做到能跑一百次不出错比让它能演示十种不同任务有用得多。