ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI操作手机三层架构:感知、决策、执行与闭环校验

AI操作手机三层架构:感知、决策、执行与闭环校验

手机上那些枯燥的重复操作——每天签到、批量改设置、跨应用搬数据、把一串固定流程走完——一直卡在一个尴尬的位置。写脚本吧,应用一改版选择器全废;用现成的自动化工具吧,碰上自绘界面、WebView 混合渲染、动态弹窗就直接歇菜。最近社区里讨论得挺热的一套 AI 操作手机方案,把这个事拆成了三层来做:感知、决策、执行。我第一眼看到这个分层时的感觉是“朴素得有点过分”,但真照着复现了一遍才发现,它的价值不在于用了多大的模型,而在于每一层都给自己留了退路——感知看不懂就换另一条通道,决策不确定就多问一次,执行没生效就重来一轮。这篇文章就把这三层掰开讲清楚,包括每一层具体在干什么、为什么这么切、参数怎么定、坑在哪。不管你是想自己搭一套,还是只想搞明白这类工具背后的逻辑,看完应该都能有收获。

1. 先把“三层”这个切法讲透

1.1 为什么不是“一个大模型端到端搞定”

很多人第一反应是:既然有多模态大模型,直接把截图丢进去,让它输出“点哪里”不就行了,为什么要分三层?我一开始也这么想,直到实测跑了一周才发现问题。端到端最大的毛病不是能力不够,而是不可观测。任务失败了,你不知道是模型没看清屏幕、还是看清楚了但决策错了、还是决策对了但点击没落到位。这三种故障的修复手段完全不同,混在一起就是一个黑盒,调都没法调。

分层之后,每一层都有明确的输入输出契约。感知层吐出来的是“结构化元素列表”,决策层吐出来的是“结构化动作指令”,执行层回报的是“动作是否生效”。任何一步出问题,你在日志里一眼就能定位到是哪一层掉了链子。这就像修车,你不会只被告知“车坏了”,而是知道是油路、电路还是传动的问题。

另一个更现实的原因是可替换性。多模态模型的迭代速度太快了,三个月换一代很正常。如果模型和操作逻辑揉在一起,换模型等于重写。分层之后,决策层换模型只需要保证输入输出的 schema 不变,感知层和执行层完全不用动。我实际做过一次替换测试,从一个小参数量的视觉模型换到一个更大的,改动量控制在提示词和两个字段映射上,半天搞定。

1.2 三层各自的边界与职责划定

这三层的职责边界,我习惯用一句大白话概括:感知层负责“现在屏幕上有啥”,决策层负责“下一步该干啥”,执行层负责“怎么把手伸过去”。听起来简单,但边界划不清就会互相甩锅。

感知层的输出必须是与业务无关的客观描述。它不该判断“这个按钮该不该点”,只负责告诉上层“这里有个按钮,文字是‘确定’,可点击,位置在屏幕下方”。一旦感知层开始做价值判断,后面就没法复用了。

决策层是唯一知道“任务目标”的一层。它接收任务描述、当前屏幕状态、历史动作记录,输出下一步动作。它不关心动作怎么实现,只说“点这个元素”或者“往上滑一屏”。

执行层是纯体力活,不做任何判断。给它一个动作,它执行,然后回报成功或失败、耗时多少、屏幕有没有变化。它也不知道自己在干什么任务。

这个划分有个好处:每层都可以单独做单元测试。感知层可以用一批固定截图做回归,决策层可以用固定的历史序列测输出稳定性,执行层可以在固定设备上测动作成功率。这种可测试性在调试阶段省的时间,比架构设计本身花的时间多得多。

1.3 三层之间靠什么串起来

串起来的是一个循环,不是一条直线。完整的链路是:截图 → 感知解析 → 决策推理 → 执行动作 → 再截图 → 校验结果 → 判断是否完成。这个闭环是整个方案的骨架,任何一步省略都会导致错误累积。

我见过有人为了省时间,做完动作不重新截图,直接让模型基于上一帧继续决策,理由是“反正我知道点了会跳转”。这个做法在前三步可能没问题,到第五步就开始漂移,因为页面加载有快慢、弹窗会随机出现、列表滚动位置会变。实测下来,省略重新感知的版本任务成功率大概在四成左右,老实做闭环的能到八成五以上。

循环还需要一个终止条件,通常是两个:任务判定完成,或者达到最大步数。最大步数我一般设 40,复杂任务设 60。设太小会导致长流程被截断,设太大则单次失败要等很久才报错,体验很差。

2. 感知层:让 AI 知道屏幕上有啥

2.1 无障碍树与截图,两条腿走路

感知层最核心的选择是:用无障碍节点树,还是用截图做视觉理解。这两个方案各有各的死角,成熟的做法是两个都用,按优先级降级。

无障碍树的优势是精确、轻量、语义清晰。系统提供的节点树里,每个元素带着文本、描述、资源 ID、类名、是否可点击、是否可滚动、边界坐标这些属性,拿到就是结构化的,不需要模型去猜。一个中等复杂度的页面,节点数在 200 到 800 之间,裁剪之后喂给模型的信息量很小,推理也快。

但无障碍树有三个死穴。第一,自绘界面的节点是空的,很多用跨平台框架渲染的页面,整棵树只有一个大节点,里面啥也没有。第二,WebView 里的内容经常只暴露一个容器节点,具体按钮拿不到。第三,游戏和视频类界面基本没有可用节点。这三种情况只能退回截图走视觉路线。

视觉路线的流程是:截图 → 元素检测或 OCR → 得到候选区域和文字。OCR 负责把界面上的文字提取出来加位置,元素检测负责找出按钮、输入框、图标这些可交互区域的轮廓。两条信息合并之后,输出和节点树尽量保持一样的格式,这样上层的决策逻辑不用改。

我自己实测的经验是,能用树就用树,树拿不到再退视觉,因为视觉路线的坐标误差和识别漏检明显更多。判断“树能不能用”有个简单阈值:一次 dump 下来,可点击节点数量少于 3 个,或者页面根节点的子节点数少于 5 个,就判定这棵树不可用,直接切视觉。

2.2 元素索引法:给模型一份“可点清单”

这里有个特别值得说的实现细节。让模型直接输出坐标,还是让它输出元素编号?我两种都试过,编号法的准确率明显高。

具体做法是:感知层把当前屏幕上所有可交互元素整理成一个列表,每个元素给一个从 1 开始的编号,附带文字、类型、大致位置描述。截图上也把这些编号画上去,或者直接把编号和描述以纯文本形式喂给模型。模型只需要回一个编号,剩下的坐标换算由代码来做。

{ "screen_id": "step_07", "elements": [ {"id": 1, "text": "搜索", "type": "input", "bounds": [0.06, 0.08, 0.94, 0.14], "clickable": true}, {"id": 2, "text": "推荐", "type": "tab", "bounds": [0.0, 0.16, 0.33, 0.21], "clickable": true}, {"id": 3, "text": "关注", "type": "tab", "bounds": [0.33, 0.16, 0.66, 0.21], "clickable": true}, {"id": 4, "text": "消息列表", "type": "list", "bounds": [0.0, 0.22, 1.0, 0.95], "scrollable": true} ] }

为什么编号法更稳?因为坐标是个连续的回归问题,模型很容易输出一个“看起来合理但偏了几十像素”的值,而编号是个离散的分类问题,只要元素在列表里,选错的概率低得多。另外坐标法还有个隐患:模型对归一化坐标的理解不稳定,有时候给你 0 到 1 的小数,有时候给你 0 到 1000 的整数,格式一乱就全废。

编号法要注意的是列表长度。元素太多会撑爆上下文,也会让模型选择困难。我的做法是设一个上限,一般 60 到 80 个,超了就按“是否可点击 > 是否有文字 > 面积大小”排序截断。同时在提示词里明确告诉模型:如果目标不在列表里,返回 -1 表示需要滚动或换页面,不要让它在列表里硬选一个最像的。

2.3 坐标归一化与多机型适配

坐标这块踩过的坑最多,单独拎出来讲。不同设备的分辨率从 720×1280 到 1440×3200 都有,还有各种刘海、挖孔、曲面屏,如果直接用绝对像素,换台设备就全乱。

解决方案是全链路归一化。感知层输出坐标时,除以当前屏幕的宽高,统一成 0 到 1 的浮点数。决策层拿到的、输出的都是归一化值。执行层在真正点击前,再乘回当前设备的实际分辨率。这样一套逻辑可以在任意设备上跑,不需要为每台机器单独调。

# 感知层:绝对坐标转归一化 def to_normalized(x, y, screen_w, screen_h): return round(x / screen_w, 4), round(y / screen_h, 4) # 执行层:归一化转绝对坐标,并做越界保护 def to_absolute(nx, ny, screen_w, screen_h): x = int(nx * screen_w) y = int(ny * screen_h) x = max(0, min(x, screen_w - 1)) y = max(0, min(y, screen_h - 1)) return x, y

这里有个容易忽略的细节:截图区域和屏幕区域可能不一致。如果截图只截了应用区域、去掉了状态栏,那么截图坐标系的原点比屏幕坐标系低了状态栏的高度。这个偏移量必须记录下来,在计算点击位置时补回去,否则所有点击都会整体上移几十像素,表现为“总能点到上面那个元素”。我第一次遇到这个现象时排查了很久,因为点击是“成功”的,只是点错了东西。

还有一个坑是缩放。为了减少传输和推理开销,截图经常会被压缩,比如原图 1080 宽压到 720 宽。如果压缩后的坐标没有反算回原始尺寸,误差会按比例放大。我的做法是截完图立刻记录缩放因子,后面所有坐标处理都带上这个因子参与换算。

2.4 感知层的三个典型翻车现场

第一个是动画未结束就截图。页面跳转有过渡动画,列表滚动有惯性,这时候截到的图是模糊的、元素位置是中途的。解决办法是在执行动作后等一个稳定信号,或者退而求其次固定等 600 到 1000 毫秒。我一般两个都上:先监听界面内容变化事件,静默超过 500 毫秒就认为稳定,同时设一个 3 秒的上限兜底,防止某些页面一直有动画(比如轮播图)导致永远等不到静默。

第二个是浮层遮挡。弹窗、广告、权限请求、通知横幅,这些东西出现在截图里,会让模型误以为它们是主界面的一部分。感知层要做一层简单过滤:检测是否有覆盖面积超过屏幕 30% 的浮层节点,如果有,把它单独标记出来并优先处理(通常是找个关闭按钮点掉)。

第三个是长列表的可见性判断。屏幕上只显示 8 条,实际列表有 200 条,模型看到的是 8 条,但它可能以为这就是全部。感知层需要告诉模型“当前是列表的第几屏,是否还有更多内容”,这个信息可以从滚动容器的属性里拿到,拿不到就用截图边缘有没有被“切一半”的元素来判断。

3. 决策层:把一句话翻译成一串动作

3.1 动作空间怎么设计才不容易出岔子

动作空间就是模型能输出的指令集合。设计原则是够用就好,越少越稳。我见过把动作设计成二十几种的方案,结果是模型经常选错,因为它分不清“长按”和“按住拖拽”的区别。

我自己用的最小集合是八种:

动作参数说明
tap元素编号点击某个元素
input元素编号 + 文本向输入框填内容
swipe方向 + 距离比例上下左右滑动
back无返回上一级
home无回桌面
wait毫秒数等待加载
scroll_to元素编号滚动直到某元素可见
done结果描述任务完成

关键点是每个动作的语义要互斥。比如“点击”和“长按”必须明确区分成两个动作,不能让模型在参数里写个时长自己决定。因为模型对“多久算长按”没有稳定概念,一会儿给你 300 毫秒一会儿给你 2000 毫秒,行为完全不可控。

另一个设计要点是done这个动作必须有。很多方案只让模型输出操作,完成判断靠外部逻辑,结果就是模型在一个已经完成的页面上反复找事做,直到撞上最大步数。让模型自己说“我干完了”,配合一个校验逻辑(比如检查某个预期元素是否存在),成功率会高很多。

3.2 提示词里的四个固定槽位

决策层的提示词我固定成四块:角色约束、动作规范、当前状态、历史动作。这四块缺一块都会出问题,我分别说一下。

角色约束这块要写清楚“你是一个手机操作助手,你的任务是通过一系列动作完成用户目标,你不做任何与目标无关的操作”。这句话看着废话,但能有效降低模型乱点的概率。我做过对照实验,去掉这句之后,模型在等待加载时会“顺手”点一些看起来相关的元素,导致流程跑偏。

动作规范就是上面那张表,要写得非常明确,包括参数格式、取值范围、什么情况下用哪个动作。我建议把 JSON schema 直接写进提示词,比自然语言描述准确。

当前状态包括:任务目标、当前屏幕的元素列表、当前页面的大致描述(可以用节点树的路径概括,比如“设置 > 显示 > 亮度”)。这个路径信息特别有用,能给模型一个“我在哪”的定位感。

历史动作是最近 5 到 8 步的动作记录,格式是“第几步、做了什么、结果如何”。这块的作用是防止死循环。没有历史记录的时候,模型在一个加载不出来的页面上会不停地重复同一个动作,因为它每次看到的都是同一帧。加上历史之后,它能看到“我已经点了三次这个按钮都没反应”,就会倾向于换策略,比如等待或者返回。

3.3 上下文与记忆:别让模型忘了自己在哪

任务步数一多,上下文就会变长。全量塞进去既慢又贵,还可能超出窗口限制。我的处理是滑动窗口 + 关键节点摘要。

滑动窗口保留最近 6 步的完整信息(动作、结果、当时的元素列表摘要)。更早的步骤压缩成一句话,比如“第 1-8 步:从桌面进入设置,依次点击了 WLAN、高级设置,当前处于高级设置页”。这个摘要在每次窗口滑动时更新一次,用一个轻量调用生成,成本很低。

摘要里必须保留三类信息:已经走过的页面路径(防止绕回原点)、已经确认无效的操作(防止重复尝试)、已经填过的内容(防止重复输入)。第三点特别重要,一个表单分好几屏填,如果模型忘了前面填过什么,会在返回上一屏时把已填内容覆盖掉。

3.4 闭环校验:模型说点了,不代表真点了

这是整个方案里我认为最关键的一条经验。模型的输出是意图,不是结果。它说“点击了搜索框”,实际的执行可能是失败的——元素被其他东西挡住、坐标算错了、应用响应慢没接住。所以每一步执行完,必须重新感知一次,然后做一次轻量校验。

校验逻辑分三级,成本从低到高:

第一级是结构校验,比较动作前后的元素列表差异。如果点击了一个“进入下一级”的按钮,元素列表应该有明显变化(超过 30% 的节点不同)。如果完全没变,八成是没点中。

第二级是预期校验,让模型在输出动作时顺便给一个预期结果,比如“点击后应该出现搜索输入框”。执行完检查这个预期是否成立。

第三级是模型复核,把执行后的截图再丢给模型问一句“刚才那个操作生效了吗”。这一级最贵,只在前后差异模糊的时候才触发。

三级校验下来,误判率能压得很低。我统计过一段时间的日志,纯靠第一级能拦住大约七成的失败,加上第二级能到九成,第三级补上剩下的。

4. 执行层:动作怎么稳稳落到屏幕上

4.1 三种注入方式的选择

执行层要把“点击坐标 (540, 1200)”变成真实的触摸事件。常见的有三条路,各有适用范围。

第一条是系统调试通道的命令注入,直接发一条点击指令。优点是接入最简单,不需要应用配合,几乎所有场景都能用。缺点是延迟稍高,单次大概 50 到 150 毫秒,而且不支持多点触控和复杂手势。

第二条是无障碍服务的事件注入,通过节点直接触发点击动作。优点是不依赖坐标,元素移动了也能点中,而且更“像”真实用户操作。缺点是对自绘界面无效,因为压根没有节点。

第三条是底层事件注入,构造触摸事件序列直接投递。优点是能力最全,能模拟长按、拖拽、多指手势。缺点是需要更高的权限,接入成本也最高。

我的策略是按元素是否有节点来决定走哪条路:有节点优先走第二条,没节点退回第一条,需要复杂手势时才用第三条。这样能在成功率、速度和适配性之间拿到一个比较好的平衡点。

4.2 中文输入这个老大难

输入文字这块,英文和数字相对简单,中文是个专门要处理的问题。原因是最常见的命令式输入方式只支持 ASCII 字符,中文直接发过去会变成一堆问号或者干脆没反应。

我试过三种方案,按推荐程度排:

方案一是走无障碍的文本设置接口。找到输入框节点,直接设置它的文本属性。这个方案最快最干净,但兼容性一般,原生输入框通常没问题,跨平台框架渲染的输入框经常不生效,设置完界面上还是空的。

方案二是临时切换输入法。装一个支持外部指令输入的输入法,把当前输入法切过去,发指令让它把文字“打”进去,完事再切回来。这个方案兼容性最好,什么输入框都能用,代价是要多装一个组件,而且切换输入法本身有几百毫秒开销。

方案三是剪贴板加粘贴。先把文字写进剪贴板,然后长按输入框触发粘贴菜单,再点粘贴。这个方案不用装额外组件,但流程长、容易在长按菜单那步出岔子。

我实际用下来,方案二做主力,方案一作为快速路径。先试方案一,检查输入框内容有没有变成目标文字,没变就切方案二。这个兜底逻辑写起来不复杂,但能省掉大量“输入失败”的排查时间。

还有个小细节:输入完成后要主动收起键盘,否则键盘会遮挡下半屏,影响下一步的感知和点击。收起的方式通常是发一个返回键事件,但要注意有的页面返回键会直接把页面也退掉,所以更稳的做法是点击键盘外的空白区域。

4.3 动作后的等待策略

等待策略看起来是个小问题,实际对成功率影响巨大。等太短,页面没加载完就截图,模型基于半成品做决策;等太长,整个任务耗时翻倍。

我用的是一套组合等待:事件监听为主,固定时长兜底,元素探测为辅。

主逻辑是监听界面内容变化事件。动作发出后开始计数,每收到一次内容变化事件就重置计时器,连续 500 毫秒没有新事件就认为页面稳定。这个策略对大部分页面都有效,平均等待时间在 600 到 900 毫秒。

兜底逻辑是设一个最长等待上限,一般 3 秒。有些页面有持续的动画(视频播放、轮播图、进度条),事件永远不会静默,这时候到点就继续,不能无限等。

辅助逻辑是关键元素探测:如果知道下一步大概率要找某个元素,可以轮询检查这个元素是否已经出现,出现了立刻继续,不用等满静默时间。这能把常见场景的平均等待压到 400 毫秒以内。

4.4 失败重试与降级路径

执行失败是常态,关键是失败之后怎么处理。我的策略是同一动作最多试两次,两次都失败就换策略。

第一次失败通常重试原动作,因为可能是偶发的响应延迟。重试前会重新感知一次,确认坐标或元素编号还是有效的。如果元素已经消失或者位置变了,就不重试,直接进入换策略。

换策略的顺序是:先尝试滚动一下看目标是不是在屏幕外,再尝试按返回键看是不是进了错误的页面,最后才判定这一步失败并向用户报错。这个降级链条覆盖了我遇到的大部分失败场景。

另外有个容易被忽略的点:重试要有间隔。连续快速重试两次,中间只隔几十毫秒,对于响应慢的应用来说等于只试了一次。我一般第一次重试间隔 300 毫秒,第二次间隔 800 毫秒,给应用足够的反应时间。

5. 一次完整任务的链路实录

5.1 任务下发与首帧感知

拿一个具体例子走一遍:任务描述是“把系统的字体大小调到最大”。这个任务有明确的终点,步骤数中等,适合用来观察链路。

第一步是回桌面,保证起点一致。发一个 home 动作,等 800 毫秒,截第一帧。感知层解析出桌面上的元素列表,大概 20 到 30 个可点击项,包括各个应用的图标和底部的搜索栏。

决策层拿到任务和元素列表,判断需要先打开设置应用,输出{"action": "tap", "target": 12, "expect": "进入设置页面"},其中 12 是设置图标的编号。

执行层换算坐标,调用点击,回报成功,等待 900 毫秒(系统设置启动稍慢),然后重新截图。

5.2 逐步推进与耗时拆解

进入设置页之后,感知层解析出列表,能看到“网络”“显示”“声音”“存储”这些条目。决策层判断字体大小属于显示相关,点击“显示”。这里有个细节:设置页的列表可能需要滚动才能看到目标项,如果当前屏没有“显示”,模型应该输出滚动动作而不是硬选一个最像的。这就是前面说的“目标不在列表返回 -1”机制起作用的地方。

进入显示页之后,找到“字体大小”或者“文字大小”这一项,点进去,通常会看到一个滑块或者一组单选按钮。滑块类控件的处理比较特殊,节点树里它可能只是一个 seekable 节点,没有具体的档位信息。这时候要么用滑动动作按比例拖动,要么先截图看当前滑块位置,再计算需要滑动的距离。

这一步的耗时明显比其他步长,因为多了两次感知和一次坐标计算。我把单步耗时拆开统计过,平均情况是这样的:

环节平均耗时
截图与压缩120 ms
感知解析(节点树)60 ms
决策推理1800 ms
坐标换算与执行90 ms
等待页面稳定750 ms
校验与重新感知150 ms
单步合计约 2.9 s

一个 12 步的任务,总耗时大概 35 秒。听起来不快,但对比人工操作其实差不多,而且它能挂着跑,人去干别的。

5.3 收尾校验与结果确认

最后一步是把滑块拖到最右端。执行完之后,不能直接说“完成”,要做一次确认:重新感知,检查滑块的位置属性或者旁边显示的档位文字是不是已经是最大值。确认无误后输出done动作,附带一句结果描述。

如果校验发现没到位,会再补一次滑动,最多补两次。两次还不行就报错,把当前的截图和元素列表存下来,方便事后排查。这个“失败也留证据”的习惯很重要,我早期没做这个,出了问题只能靠回忆复现,效率极低。

6. 问题排查速查表与避坑心得

6.1 高频故障对照表

跑了一段时间之后,我把遇到的问题整理成了一张表。这张表基本覆盖了九成以上的故障,遇到问题先对着查一遍,能省不少时间。

现象大概率原因处理方式
点击总是偏移固定距离截图区域与屏幕区域不一致,缺状态栏偏移记录截图区域偏移量,点击时补回
换设备后全部点偏坐标没做归一化,用了绝对像素全链路改成 0 到 1 归一化
中文输入变成问号命令式输入不支持非 ASCII切换输入法方案或走剪贴板
模型反复点同一个位置缺历史动作记录,或校验没生效补历史动作,加前后帧差异校验
卡在加载页不动等待策略只等固定时长,没等稳定信号加事件监听和最长超时兜底
无障碍树几乎为空自绘界面或 WebView 混合渲染降级到视觉感知路线
弹窗打断流程感知层没识别浮层检测大面积覆盖节点,优先关闭
长任务后期开始乱操作上下文超长,早期信息丢失加滑动窗口和路径摘要
输入框填完内容又被清空重复输入或页面重新渲染输入后校验内容,避免重复操作
任务判定完成但其实没完成只信模型输出,没做结果校验加终点元素探测

6.2 几条踩出来的经验

第一条,日志一定要存全链路快照。我一开始只存文本日志,出问题只能看到“第 7 步点击失败”,完全不知道当时屏幕长什么样。后来改成每步都存一份截图加元素列表的 JSON,排查效率提升了不止一个档次。存储成本其实不高,一次任务 40 步,压缩后的截图总共也就几兆。这些数据还能反过来做优化,比如统计哪些页面耗时最长、哪些动作失败率最高。

第二条,别让模型做它不擅长的事。模型擅长语义理解和模糊匹配,不擅长精确计算。所以坐标换算、距离计算、滚动比例这些,全部放到代码里做,模型只负责“选哪个”。我早期让模型直接输出滑动距离的像素值,结果它给的数字完全没法用,一会儿 200 一会儿 800,毫无规律。改成让它输出“滑动半屏”这种相对描述,代码再换算成具体像素,稳定性立刻上来了。

第三条,给每个应用设一个包名白名单。这套东西跑起来之后,最怕的是误操作——本来想点自己应用里的按钮,结果跳到了别的应用里乱点一通。加一个包名检查,发现当前前台应用不在白名单里就立刻停下并回桌面。这个检查成本极低,但能避免很多尴尬。

第四条,最大步数和单步超时都要设。任务卡死的时候,如果没有超时保护,整个流程会一直挂着,占着设备不说,日志还会疯狂增长。单步超时我设的是 15 秒(包含模型推理和等待),整体超时按任务复杂度设 3 到 5 分钟,超了就中断并保存现场。

6.3 关于模型选型的一点实际感受

最后聊聊选型。这类任务对模型的要求其实挺特殊的:不需要它有很强的知识储备,但需要它对界面元素的空间关系和语义关系判断准确,还需要它的输出格式极其稳定。

我试过用参数量很大的模型,效果确实好一些,但单步推理从 1.8 秒涨到 4 秒以上,一个 30 步的任务要跑两分多钟,实用性大打折扣。反而是中等参数量的模型配合精心设计的提示词和元素索引法,在成功率只差几个百分点的情况下,速度能快一倍多。

另一个感受是输出格式的稳定性比理解能力更重要。一个理解能力稍弱但每次都能严格按 schema 输出的模型,实际可用性远高于一个聪明但经常给你加两句解释、格式跑偏的模型。所以在选型阶段,我会专门跑一批固定输入,看输出的 JSON 解析成功率,这个指标不过关的直接排除,不管它理解能力多强。

还有一个能显著提升稳定性的技巧:把温度参数调到很低。这类任务不需要创造性,需要的是可重复性。同样的屏幕状态下,模型应该给出同样的决策,这样出问题才能复现。温度调高之后,同一个页面两次决策不一样,排查起来就是噩梦。


这套三层架构跑下来,我最大的体会是:AI 操作手机这件事,难点从来不在“AI”两个字上,而在工程细节里。坐标偏移几十像素、输入法不支持中文、等待少了两百毫秒、历史记录少存了三步,任何一个细节出问题,整体成功率就会掉一大截。分层的好处就是把这些细节拆开,让每一层的问题都能被单独发现、单独修复。至于后面还能怎么扩展,我最近在试的是把成功任务的完整轨迹存下来,下次遇到同类任务直接检索复用,跳过一部分推理步骤——这个方向看起来挺有戏,等跑一段时间有数据了再单独写一篇。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进