前阵子跑 OpenGUI,在真机上试了一个长程任务:打开 X,搜索 mobile AI agents 相关的近期讨论,收集主要观点,再总结大家关心的问题。
用自然语言描述只有一句话,执行起来却拆成了几十个判断和动作。App 打开了吗?是不是首页?搜索框点中了吗?结果加载了吗?中间有没有登录弹窗?有没有推荐关注?页面跳走了是回退还是重试?
在传统手机自动化的思路里,这种任务很难稳定跑完。点击本身不难,麻烦的是真实手机不按脚本走。
为了验证这个判断,我用三种方案各跑了三次同一个任务。
纯脚本(Appium):三次全失败。一次卡在更新弹窗,两次搜索后页面结构变化导致 xpath 失效。平均存活 4 步。
VLM 截图循环(v2 Agent):三次里一次成功,耗时 18 分钟,中间在推荐关注弹窗上重试了 7 次。另外两次分别在第 12 步和第 23 步陷入循环:截图显示没变化,模型继续点同一个位置,再截图还是没变化。
OpenGUI:三次全部成功,平均耗时 11 分钟。最长一次遇到登录弹窗 + 网络加载慢 + 推荐关注,supervisor 做了两次重新规划,没有人工干预。
这组对比说明的不是 OpenGUI"更聪明",而是它把任务状态显式地维护在系统里,而不是依赖模型隐式地记住上下文。
传统手机自动化:假设世界会配合你
目前市面上主流的方案大致分三类:
录制回放,你在手机上操作一遍,工具把点击坐标、滑动轨迹、输入内容录下来,之后按原样回放。
UI 自动化框架,如 Appium、UIAutomator,通过 accessibility tree 或 xpath 定位元素,然后执行操作。
RPA 平台,可视化编排,把上述能力封装成流程图,加上条件判断和异常捕获。
这些方案在简单场景下都很好用。每天自动打卡、定时抢券、批量处理固定流程,只要页面不变,它们能跑得很稳。页面一变,或者流程稍长,问题就冒出来了。
v1:脚本的天敌是弹窗
录制回放是最直观的方案。你操作一遍,它记下来,下次照做。
拿那个 X 搜索的任务来说,录制下来的流程大概是:点击 X 图标,等待 3 秒,点击搜索框,输入 "mobile AI agents",点击搜索按钮,再等 3 秒,滑动浏览,截图保存。
这个脚本在理想情况下可以跑通,但现实世界不合作。打开 X 时弹了一个更新提醒,点击坐标就错位了。网络慢,3 秒不够,页面还在加载 skeleton。搜索前让你登录,脚本不知道这是哪一步。结果页中间插了一个推荐关注,滑动被拦截了。某个博主的内容需要点击"显示更多",但脚本没有这条分支。
脚本没有状态。每一步都预设了上一页的结果和当前页的状态,现实一偏离就报错退出,没有恢复能力。
v2:视觉理解让脚本变聪明一点,但没解决状态问题
坐标和 xpath 不可靠,能不能让机器看懂屏幕?
这是近一两年手机 Agent demo 的主流思路:截图,传给多模态模型(VLM),模型返回下一步动作,执行,再截图。
这个循环比脚本灵活。模型能看到当前屏幕,能识别搜索框在哪,甚至能处理一些弹窗。不需要预先定义坐标,告诉它"打开 X 搜索 mobile AI agents"就行。
问题是,VLM 每次只看当前这一张截图。短任务里没问题,三步之内打开 App、点一个按钮、输一段文字,模型通常能搞定。任务一长,短板就暴露了:
模型不知道前面做了什么,第十步失败时不知道要回退到第几步。它也不知道整体目标,只看当前截图,容易被局部最优带偏。看到一个不顺眼的 UI,可能会顺手"优化"一下,偏离主线。任务完成了,它可能还在继续点点点。
v2 解决了看懂屏幕的问题,但没有解决维护长程状态的问题。
更底层的问题是上下文窗口的硬限制。VLM 处理截图 + prompt 的 token 消耗很大,一张 1080p 截图编码后可能占 1000-3000 token。5-10 轮循环后,前面做了什么、最初的目标是什么,物理上就被挤出上下文了。这不是"忘了",是装不下了。
很多手机 Agent demo 停在 v2。三分钟的 demo 很惊艳,三十分钟的实际任务,大概率会在某个弹窗或加载状态上卡住,然后陷入截图、识别、点击、没变化、再截图的死循环。
v3:把目标变成状态流
OpenGUI 的做法是把任务放进一个有状态的后端 graph 里,而不是让模型在本地做一个无状态的截图循环。
看过源码,架构很清晰。核心链路大概是这样:
User/IM 命令 → Plan Supervisor → Executor Graph → Android Client → 真实设备
↑ ↓
└──── 执行结果 + 设备状态 ────┘
主 graph 在 mobile-agent.graph.ts 里构建,用的是 LangGraph 的 StateGraph:
const graph = new StateGraph(AgentStateSchema)
.addNode("supervisor", supervisorNode)
.addNode("extract_todo", extractTodoNode)
.addNode("fallback_extract", fallbackExtractNode)
.addNode("gui_executor", executorSubgraph)
.addNode("summarizer", summarizerNode)
.addEdge(START, "supervisor")
.addConditionalEdges("supervisor", routeAfterSupervisor)
.addConditionalEdges("extract_todo", routeAfterExtractTodo)
.addConditionalEdges("gui_executor", routeAfterExecutor)
.addEdge("summarizer", END);
Plan Supervisor 维护任务状态。复杂任务进来,supervisor 先拆成可执行的子任务,形成计划文档,然后逐个派发给 Executor。它本身也是一个 LLM agent,带有 write_todos 和 read_todos 两个 tool,可以动态调整任务列表。第一次调用时生成计划,后续调用时评估 Executor 回传的结果,决定标记完成、重试还是重新规划。
Supervisor 的路由逻辑很简单,但足够说明问题(routing.ts):
export function routeAfterExtractTodo(state: AgentState) {
if (state.isCancelled) return "summarizer";
if (state.planTodoComplete) return "summarizer";
if (state.todoFound) return "gui_executor";
return "fallback_extract"; // 用 Haiku 做兜底提取
}
export function routeAfterExecutor(state: AgentState) {
if (state.isCancelled) return "summarizer";
if (isExecutionConnectionLostMessage(state.executorOutput?.fail_reason)) {
return "summarizer"; // 设备断连,停止重试
}
return "supervisor"; // 把执行结果送回 supervisor 评估
}
Executor Graph 负责把子任务落到设备上,本身也是一个 subgraph。执行循环在 executor.graph.ts 里定义:
const subgraph = new StateGraph(AgentStateSchema)
.addNode("entry", entryNode)
.addNode("sense", senseNode, { retryPolicy: { maxAttempts: 3 } })
.addNode("vision_model", visionModelNode)
.addNode("parse_action", parseActionNode)
.addNode("execute_action", executeActionNode, { retryPolicy: { maxAttempts: 3 } })
.addNode("post_execute", postExecuteNode)
.addEdge(START, "entry")
.addEdge("entry", "sense")
.addEdge("sense", "vision_model")
.addConditionalEdges("vision_model", routeAfterVisionModel)
.addConditionalEdges("parse_action", routeByAction)
.addConditionalEdges("execute_action", routeAfterExecuteAction)
.addConditionalEdges("post_execute", routeAfterPostExecute);
Entry 节点初始化执行状态;Sense 节点从设备获取截图和当前 App 信息;Vision Model 节点把截图和上下文发给 VLM,获取下一步动作;Parse Action 节点把 VLM 的输出解析成结构化动作;Execute Action 节点通过 WebSocket 把动作发到 Android 设备执行;Post Execute 节点做异常检测(后面细说),然后决定回退到 Sense 继续循环,还是退出 subgraph。
Summarizer 在收尾时介入,把执行过程的关键信息整理成结构化结果返回给用户。
这几个组件的协作,让目标从一个 prompt 里的文字,变成了一整套可以被引用、暂停、恢复和清理的状态。
状态具体存在哪里?看 state.types.ts 里的 AgentStateSchema:
-
planDocument:supervisor 生成的计划文档(Markdown) -
executorInput/executorOutput:当前子任务的输入和输出 -
executor字段:Executor subgraph 的内部状态,包括截图 URI、当前预测、循环计数、异常标记、消息历史、token 用量等 -
todoFound/planTodoComplete:supervisor 决策用的布尔标志 -
isCancelled/isPaused:用户中断状态
这个状态不是存在模型上下文里,而是存在后端 graph 的 state 对象中,每步执行完都由 LangGraph 的 reducer 合并更新。
Android 端通过 WebSocket 和后端保持连接。StandbySocketManager.kt 负责设备待命,GestureService.kt 负责把动作执行到真实设备上。设备不是被脚本驱动的傀儡,而是一个持续反馈状态的 worker。
异常检测:executor 里的保险丝
v2 方案最容易出现的是"循环":截图没变化,模型继续点同一个位置,再截图还是没变化。OpenGUI 在 Post Execute 节点里做了显式的异常检测。
看 post-execute.node.ts 里的检测逻辑:
动作重复检测:检查最近 10 个动作里是否有连续 5 个相似动作(同类型 + 坐标距离小于 50 像素)。如果是,标记为可能循环。
动作周期检测:检查是否存在 A-B-A-B 的交替模式。比如点击返回再点进去,再点返回再点进去,模型在两个页面间来回跳。
截图异常检测:用 perceptual hash(pHash)比较最近几张截图。如果连续 3 张截图完全相同且动作不是 wait/scroll,说明页面没响应。如果截图呈现 A-B-A-B 的交替模式,说明页面在两个状态间切换。
连续滚动检测:连续 scroll 超过 8 次,判定当前搜索策略没有进展,强制退出 executor 让 supervisor 重新规划。
检测到异常后,Post Execute 节点会设置 needRemind = true,并在下一轮 Vision Model 调用时注入提醒:
const remindMessage = new HumanMessage(
`The current task may be stuck in a loop or drifting from the goal.
Execution anomaly: ${exec.remindReason}
Original task: ${instruction}
Check whether the execution is drifting from the original goal or stuck in a loop.`
);
这个设计的关键是:异常不是报错终止的理由,而是被消费掉的输入。检测到循环 → 注入提醒 → VLM 下一轮输出修正动作 → 继续执行。整个链路在系统内部闭环,不需要人工干预。
关键差异:状态管理
传统脚本没有状态,只有"下一步该做什么"。v2 的 Agent 也没有状态,只有"当前屏幕该怎么处理"。OpenGUI 的状态分布在计划文档、当前子任务、执行结果、失败分类这些数据结构里,supervisor 每一步都能基于完整状态做决策。
走迷宫的比喻很贴切:传统脚本手里只有一张路线图,走错一步就迷路。v2 Agent 每到一个路口抬头看四周,但记不住来时的路。OpenGUI 手里有一张实时更新的地图,知道自己在哪、目标在哪、哪条路试过不通。
另一个关键差异是模型角色的分离。v2 通常用一个模型做所有决策,OpenGUI 把规划、监督、VLM 执行拆到不同模型。
从 README 里的数据,全 Claude Opus 配置跑一个中等长度任务(约 60 次截图分析),VLM + Planner + Supervisor 全部用 Opus,预估费用在 $8-15 区间。换成 Qwen 3.6 Plus 做 Planner 和 Supervisor,Doubao Pro 做 VLM,同样任务降到 $0.6-1.2,成本差 10-15 倍。
这个成本差异来自两个因素:一是 Qwen/Doubao 的单价远低于 Opus,二是 OpenGUI 的架构允许不同角色用不同模型。Planner 和 Supervisor 处理的是文本计划,不需要多模态能力,可以用便宜的文本模型。只有 Executor 里的 VLM 需要看图,这部分费用被隔离在 subgraph 里。
一个具体的例子
X 搜索的任务在 OpenGUI 里会经历这些状态:
Plan Supervisor 先生成计划:打开 X,搜索关键词,浏览结果,收集观点,总结。然后派发子任务"打开 X"给 Executor。Executor 截图,VLM 判断当前是桌面还是其他 App,执行点击。结果回传:X 已打开,但弹出了登录框。Supervisor 判断这是异常,需要处理登录,无法处理就标记失败并尝试跳过。登录处理完,继续派发"搜索关键词"。Executor 执行搜索,网络慢页面没加载完,内部重试,等待再截图再判断。搜索完成,进入"浏览结果"子任务。中间遇到推荐关注弹窗,Executor 识别为干扰,尝试关闭或跳过。所有子任务完成,Supervisor 调用 Summarizer 生成结构化总结。
没有一个环节假设页面会按顺序走。每一步都基于当前真实状态判断,失败被当作正常输入消费,不是异常终止的理由。
执行完成后,Summarizer 返回的结果大致长这样:
## Task Summary
**Goal**: Search X for recent discussions on "mobile AI agents" and summarize key concerns.
**Execution**:
- Opened X app successfully
- Searched "mobile AI agents"
- Scrolled through top 20 results
- Collected 8 relevant posts/threads
**Key Findings**:
1. Privacy concerns dominate: users worried about screen recording and data access
2. Reliability: agents failing on non-standard UI patterns (custom keyboards, overlays)
3. Cost: VLM per-screenshot pricing makes long tasks expensive
4. Latency: 5-15s per action too slow for real-time interaction
**Blocked Items**:
- Login prompt appeared; task continued after handling
- One result required app switch to Safari; skipped per constraints
**Conclusion**: Mobile AI agents are technically feasible but face UX, cost, and trust hurdles before mainstream adoption.
代价是什么
这套设计更重,代价在三方面。
模型成本。VLM 每次分析截图都要调 API,一张 1080p 截图编码后可能占 1000-3000 token。一个 10 分钟的任务如果有 60 次截图分析,总 token 消耗可能在 15-30 万之间。全 Opus 配置下这是不可忽视的开销,混合模型配置能把费用压到可接受范围,但代价是模型能力的差异:Qwen 的规划质量不如 Opus,Doubao 的视觉理解在某些场景下会漏掉细节。
延迟。截图 → 后端 → VLM 推理 → 动作解码 → 网络传输 → 设备执行 → 等待 UI 稳定 → 再截图,这个链路单轮通常在 5-15 秒。一个 60 步的任务,纯等待时间就 5-15 分钟。v2 方案如果把 VLM 放在本地或近端可以更快,但 OpenGUI 的后端 graph 架构天然引入了一层网络跳转。对延迟敏感的任务(比如实时游戏辅助),这套架构不适用。
系统复杂度。你需要跑后端(NestJS + LangGraph)、数据库(PostgreSQL)、缓存(Redis)、WebSocket gateway,还要维护 Android 客户端的待命连接。部署一套 OpenGUI 比跑一个 Python 脚本重得多。设备会休眠,后台会被系统杀掉,WebSocket 会断线。standby 机制不是"连上就行",要处理心跳(35 秒间隔)、重连、状态同步。
但手机端很难绕开这些复杂度。真实 App 会弹窗,会卡加载,会误触,会把你带到一个完全不同的页面。只靠循环 prompt,稍微长一点的任务就会退化成截图版的 while true。
OpenGUI 把复杂度显式地放进系统里。一次没点对,变成 supervisor 要消费的执行结果。设备掉线,被 standby gateway 检测到并停止重试。任务跑了一半被暂停,可以在恢复时从当前子任务继续。这个设计更重,但它给了长程任务一个可以调试、可以恢复、可以复盘的位置。
Checkpoint:任务中断后能恢复
LangGraph 提供了 checkpointing 机制,OpenGUI 把它接进了 PostgreSQL(PostgresCheckpointerService)。这意味着:
- 任务执行到第 20 步,后端重启了,重启后可以从第 20 步的 checkpoint 继续,而不是从头来
- 用户手动暂停了任务,恢复时 supervisor 重新评估当前状态,决定继续执行还是重新规划
- 多个子任务共享同一个 thread ID,状态在 graph 节点之间持久化
这个功能对长程任务很关键。一个跑了两小时的任务,如果因为后端滚动更新而丢失全部进度,是不可接受的。checkpoint 把"状态在内存里"变成"状态在数据库里",牺牲了性能,换来了可靠性。
贤者时刻
自动化系统的能力边界,不是由"能执行什么动作"决定的,而是由"能维护多少状态"决定的。
脚本只能维护一步的状态(下一步做什么)。v2 Agent 能维护一轮的状态(当前屏幕怎么理解)。OpenGUI 维护的是整个任务生命周期的状态:计划、进度、异常、恢复。
Codex 的 /goal 在 coding agent 里做了类似的事:把目标从一轮对话里的文字,变成会话里可恢复的状态。OpenGUI 在手机端走了更远,它不仅保存目标,还把设备反馈、执行结果和失败处理接成了一条完整的状态流。场景不同,问题很接近:长程 agent 不能只会执行下一步,还要持续维护"我在哪、要去哪、边界是什么"这些信息。
如果你只是每天自动签到一次,脚本就够了。要让 AI 在真实手机上跑一个持续几十分钟、涉及多 App 切换和复杂判断的任务,就需要把目标从 prompt 里抽出来,变成一个可以被持续维护的状态。这个选择更重,但它是从 demo 走向 production 的必经之路。
参考
- OpenGUI 官网:https://opengui.ai
- OpenGUI 源码:https://github.com/Core-Mate/open-gui
- OpenAI Codex 0.128.0 release: https://github.com/openai/codex/releases/tag/rust-v0.128.0
- Simon Willison on Codex goals: https://simonwillison.net/2026/Apr/30/codex-goals/
Top comments (0)