Cell Stack

← 返回·

macOS 下实现 AI 操控电脑(Computer Use)的思考

截图识别不是 AI 操控电脑的最优解——macOS Accessibility API 提供结构化 UI 树,结合 AppleScript 和本地视觉兜底,构建可靠的四层 Computer Use 架构。

202601

从一次黑客松的"伪 Computer Use"说起

去年公司黑客松,我们组选了一个听起来很酷的赛题:《让 AI 操作你的电脑》。

两天时间,我们最终交付了两个能力:一个是用 Playwright 配合 MCP 来操控浏览器,另一个是在 VNC 沙箱里通过截图视觉识别来做键鼠操作。演示效果还不错,评委也觉得有意思。但说实话,我心里清楚——这两个方案都不算真正的 Computer Use。

Playwright 操作浏览器,本质上是在一个高度结构化的环境里调 API,跟"操作电脑"差得很远。VNC 沙箱里的截图识别倒是更接近"通用操作"的感觉,但它慢、不稳定、且依赖一个隔离环境,离"在我自己的 Mac 上让 AI 帮我干活"这个真实需求还有很大距离。

黑客松结束后,这个问题就搁下了。直到最近,我在用 Swift 生态开发一个 macOS Agent 应用时,Computer Use 又成了一个绑不开的核心需求。这一次没有 48 小时的限制,也没有"最小可演示"的压力,我开始认真想:在 macOS 上,这件事到底应该怎么做?

重新面对问题:两条经典路线

调研一圈下来,社区里的方案大体可以归为两类。

第一类是基于截图的视觉识别。 典型代表是 Anthropic 的 Computer Use 演示,以及 UI-TARS 这类方向。核心思路是:截一张全屏图,发给多模态模型,让模型"看懂"当前界面,然后输出下一步操作(通常是坐标点击或键盘输入)。

这条路线很直觉,因为它模拟的是人类使用电脑的方式——看屏幕、理解内容、移动鼠标、点击。但我对它有比较明显的保留:

  • 视觉识别的效果不稳定,尤其是面对复杂界面、深色模式、或者非标准 UI 时,模型经常犯低级错误;
  • 每一步都要发送一张甚至多张截图,上下文和 token 开销非常大;
  • 更根本的问题是——GUI 是给人看的,不是给 AI 用的。人类需要颜色、布局、图标这些视觉线索来理解界面,但 AI 真正需要的,往往是更简单粗暴、更结构化、更可执行的信息。

让 AI 去"看"一个本来就是为人类视觉系统设计的界面,然后从像素里反推出结构和语义,这件事本身就有一种"绕远路"的感觉。

第二类是结构化控制。 类似 Playwright 对浏览器做的事情——不是去"看"网页长什么样,而是直接读取 DOM 树,拿到每个元素的类型、属性、状态,然后通过编程接口去操作。

进一步调研后我发现,macOS 在这方面其实非常友好。它原生提供了一套完整的 Accessibility(无障碍)能力,包括 AXUIElement API,可以读取几乎任何应用的 UI 层级树、元素属性和可执行动作。这套能力原本是为辅助障碍人群使用电脑而设计的,但它恰好也为 AI 提供了一层结构化的、可读写的、可执行的界面抽象。

这让我意识到一件事:macOS 上真正有潜力的路线,也许不是"让 AI 看懂 GUI",而是"绕过 GUI 的表象,直接读取 GUI 背后的结构化描述"。

为什么我越来越倾向于结构化控制

这个判断不是一开始就有的,而是在持续调研和动手验证的过程中逐渐清晰的。

我让 Codex 帮我基于 AXSwift(一个对 macOS Accessibility API 的 Swift 封装)写了一个操作计算器的 demo。跑通之后,我对 Accessibility 路线的信心明显增强了。通过 AX API,我可以拿到计算器窗口里每一个按钮的 role(角色)、title(标题)、enabled(是否可用)、frame(位置大小),以及它支持的 action(比如 AXPress)。我不需要截图,不需要让模型去"找到数字 7 在哪里",我直接就知道哪个元素是 7,它能不能点,点它的动作叫什么。

这就是结构化控制的核心优势:对 AI 来说,最有价值的不是像素,而是语义。 一个 AX 元素告诉你的信息——

role: buttontitle: "7"enabled: trueaction: AXPressframe: (120, 340, 60, 60)

——远比一张截图里"第三行第一列那个灰色方块上写着 7"要清晰、确定、低成本得多。

而且这不只是一个效率问题,更是一个可靠性问题。截图识别可能因为分辨率、主题、字体渲染、窗口遮挡等各种原因出错,但 AX 树给出的信息是确定性的——它就是系统 UI 框架内部暴露出来的真实状态。

macOS 为什么是一个特殊而友好的平台

做 Computer Use 这件事,平台差异非常大。macOS 在这方面有几个独特的优势,值得单独说一下。

首先是 Accessibility 的完整性。 macOS 的 Accessibility 框架(基于 AXUIElement C API)是系统级的,几乎所有使用标准 AppKit / SwiftUI 框架开发的应用都会自动获得基本的无障碍支持。这意味着大量 Mac 应用天然就有一棵可读的 UI 树,不需要应用开发者做额外适配。

其次是 AppleScript 和 Scripting Bridge 的存在。 很多 macOS 应用(尤其是 Apple 自家应用和一些老牌第三方应用)支持 AppleScript 脚本化控制。这是一个经常被忽略但非常重要的能力层。比如你想让 AI 在 Finder 里创建一个文件夹,或者在 Mail 里发一封邮件,用 AppleScript 可能比用 AX 去"找到菜单栏→点击文件→点击新建文件夹"要高层得多、稳定得多。

再次是 Apple 近年来在本地视觉能力上的持续投入。 Vision 框架提供了高质量的本地 OCR、文本识别、甚至一些图像理解能力,ScreenCaptureKit 提供了现代化的屏幕和窗口捕获接口。这些能力都在本地运行,不需要网络请求,不需要消耗 token。

这几层能力叠加在一起,让 macOS 成为一个做 Computer Use 时非常友好的平台——不是因为它有什么"一站式 AI 操控框架"(Apple 并没有提供这样的东西),而是因为它在系统层面提供了足够丰富的、可组合的基础能力。

Accessibility 作为主路:它真正解决了什么问题

回到 Accessibility 本身。当我说"以 AX 为主路"时,我指的不只是"能读到 UI 树"这一件事。AX 真正解决的问题有几个层面:

第一,Observation:获取当前界面状态。 通过 AXUIElement,可以遍历任意应用的 UI 层级,拿到每个元素的 role、title、value、enabled、focused、frame、children 等属性。这就是界面的"结构化快照"。

第二,Action:执行操作。 AX 元素不只是可读的,还是可写的、可执行的。你可以对一个按钮执行 AXPress,对一个文本框设置 AXValue,对一个窗口执行 AXRaise。这意味着 AI 不需要通过坐标点击来操作界面,而是可以直接调用元素级别的动作。

第三,Notification:监听变化。 这一点是我在调研中逐渐意识到其重要性的。macOS 提供了 AXObserver 机制,可以监听特定元素或应用的无障碍通知——比如窗口创建、焦点变化、值变化、菜单打开等事件。

AXObserver 的意义在于:AI 不需要每一步都全量重新抓取整棵 UI 树。 全量抓取不仅慢,而且会生成大量冗余信息。如果能通过事件通知来感知增量变化,Agent 就可以只关注"发生了什么变化",而不是每次都从头理解整个界面。这对降低 token 消耗、减少延迟、提升 Agent 的反应速度都非常关键。

但我也要诚实地说,AXObserver 的实际使用体验并不算特别好。通知的粒度有时不够细,不同应用的实现质量参差不齐,有些通知可能丢失或延迟。这是工程中需要处理的现实问题,但方向上,"事件驱动 + 增量更新"一定比"轮询 + 全量快照"更合理。

仅有 AX 还不够

如果 Accessibility 能解决所有问题,这篇文章就不用写这么长了。现实是,AX 有明确的边界。

不是所有应用都 AX 友好。 老旧应用、自绘界面(比如用 OpenGL/Metal 直接渲染的应用)、Canvas/WebGL 类内容、远程桌面窗口、游戏——这些场景下,AX 树可能是空的、残缺的、或者完全不可用的。Electron 应用的 AX 支持虽然在改善,但质量仍然参差不齐。

AX 树的信息可能不完整。 有些控件虽然在 AX 树里有节点,但缺少关键属性——比如没有 title、没有 value、role 是 generic 的 AXGroup。这种情况下,光靠 AX 信息不足以让 AI 理解这个元素是什么、能做什么。

有些操作在 AX 层面不好表达。 比如拖拽、多指手势、复杂的画布交互,AX 的 action 模型覆盖不了。

所以,视觉方案是必要的。但我的判断是:视觉应该作为兜底,而不是默认路径。

AppleScript:一个经常被忽略的重要中间层

在讨论视觉兜底之前,我想先说一个经常被忽略的能力层:AppleScript 和 ScriptingBridge。

很多 macOS 应用——尤其是 Finder、Safari、Mail、Calendar、Notes、Pages、Keynote、Numbers,以及 iTerm2、OmniFocus、BBEdit 等第三方应用——都支持 AppleScript 脚本化控制。这套机制提供的是比 AX 更高层的语义接口。

举个例子。如果你想用 AI 在 Finder 里把某个文件移动到另一个文件夹,用 AX 的方式可能是:找到 Finder 窗口→定位到文件列表→选中目标文件→触发菜单操作或拖拽。这个过程步骤多、容易出错、且依赖 UI 布局。

而用 AppleScript:

applescript
tell application "Finder"    move file "Document.pdf" of folder "Downloads" of home to folder "Archive" of homeend tell

一条命令,语义明确,不依赖 UI 状态。

对于 scriptable 的应用,AppleScript / ScriptingBridge 应该是 优先于 AX 的控制方式。它更高层、更稳定、更不容易因为 UI 变化而失效。AI Agent 在决定如何操作一个应用时,应该先检查这个应用是否支持脚本化,如果支持,优先走脚本接口。

当然,AppleScript 的覆盖面有限,不是所有应用都支持,支持的深度也不同。但把它作为"第一优先级"来考虑,是一个很多 Computer Use 方案忽略了的重要设计决策。

视觉兜底怎么做:先本地,再远程

当 AX 和 AppleScript 都不够用时,视觉方案就需要登场了。但这里有一个关键的设计选择:视觉兜底应该优先考虑 Apple 原生的本地能力,而不是一上来就把截图发给外部多模态大模型。

这里需要厘清几个概念,因为它们经常被混为一谈:

  • ScreenCaptureKit:负责高效地捕获屏幕或特定窗口的图像。它是"抓图"的工具,本身不做任何理解。
  • Vision / VisionKit:Apple 的本地视觉框架,提供 OCR(文本识别)、文本检测、条形码识别等能力。它在本地运行,速度快,不消耗 token,对中英文文本的识别质量相当不错。
  • Foundation Models:Apple 在 WWDC25 上介绍的设备端模型能力,目前更偏向文本理解和生成,并不等于一个现成的"截图理解 VLM"。
  • 外部多模态模型(如 GPT-4o、Claude 等):能力最强,但成本最高、延迟最大。

我认为更合理的视觉兜底路线是这样的:

  1. 先用 ScreenCaptureKit 抓取目标窗口(而非全屏)的图像。 窗口级别的截图比全屏截图信息密度更高、噪音更少。
  2. 用 Vision 框架在本地做 OCR 和文本检测。 很多时候,AX 缺失的信息只是某个控件的文本标签,本地 OCR 就能补上,根本不需要请求外部模型。
  3. 只有在本地视觉处理仍然不足以理解界面时,才把裁剪后的局部图像发给外部多模态模型做高层语义判断。 注意是"裁剪后的局部图像",不是每次都发全屏。

这个分级策略的核心考量是:能在本地解决的问题就不要发到云端,能用结构化信息解决的问题就不要用像素。 每一次外部模型调用都意味着延迟、成本和不确定性,应该尽量减少。

结构化快照:面向 LLM,而不是面向调试工具

在调研过程中,我看到一个小项目 application-use,它的规模很小,成熟度存疑,我不会把它当作现成依赖。但它有一个思路引起了我的注意:把 Accessibility tree 和 Vision OCR 的结果结合起来,形成一个更适合 LLM 消费的"结构化快照"。

这个思路本身比具体实现更重要。

原始的 Accessibility tree 往往非常庞大——一个中等复杂度的应用窗口可能有几百甚至上千个 AX 节点。如果把这棵树原封不动地序列化后丢给 LLM,会有几个问题:

  • token 爆炸:大量节点信息会迅速填满上下文窗口;
  • 噪音过高:很多节点对当前任务毫无意义(比如装饰性的分隔线、不可见的容器、禁用的菜单项);
  • 缺乏可执行性:原始树结构对 LLM 来说不够"可操作",模型需要额外推理才能知道"我应该对哪个元素做什么"。

所以,结构化快照应该是面向 LLM 设计的,而不是面向开发者调试工具设计的。 具体来说:

  • 稀疏化:只保留与当前任务相关的、可交互的、有语义信息的节点;
  • 扁平化:对深层嵌套做适度展平,减少 LLM 需要理解的层级深度;
  • 标注可执行动作:每个节点不只是描述"它是什么",还要明确"能对它做什么";
  • 附加上下文:对 AX 信息不完整的节点,用 OCR 结果做补充标注;
  • 提供稳定引用:给每个可交互元素一个稳定的 ID 或句柄,这样 LLM 输出的动作可以直接引用元素,而不是输出坐标。

最后一点尤其重要:坐标点击应该是 fallback,不应是默认的动作形式。 默认情况下,AI 应该输出类似"对元素 #btn-7 执行 press"这样的结构化动作,而不是"在坐标 (120, 340) 处点击"。坐标点击不仅不可靠(窗口移动、缩放后就失效),而且丢失了语义信息,让整个操作链路变得脆弱。

只有在元素无法通过 AX 定位时,才退回到坐标点击——而且即便如此,也应该尽量结合 OCR 结果来确认点击目标,而不是盲目信任模型输出的坐标。

一个更合理的分层架构

把前面的思考串起来,我目前倾向的架构是一个四层结构,从高到低依次是:

第一层:App-specific Automation

对于支持脚本化的应用,优先使用 AppleScript / ScriptingBridge / 应用自带的脚本接口。

这是最高层、最稳定、语义最明确的控制方式。Agent 在面对一个任务时,应该先判断目标应用是否 scriptable,如果是,直接走脚本接口。

第二层:Structured UI Automation

对普通 GUI 应用,使用 Accessibility / AXUIElement 获取结构化 UI 树、元素状态和可执行动作。

这是覆盖面最广的通用层。大多数标准 macOS 应用都能在这一层获得足够的信息和控制能力。AXSwift 可以作为实现入口,但底层设计不应该被某个特定库绑定——核心抽象应该是 observation(获取状态)和 action(执行操作)这两个接口,具体实现可以替换。

同时,这一层应该利用 AXObserver 做事件监听和增量更新,而不是每一步都全量遍历 UI 树。

第三层:Local Visual Augmentation

当 AX 信息不足时,使用 ScreenCaptureKit 抓取目标窗口图像,配合 Vision/VisionKit 做本地 OCR 和文本检测。

这一层的目标不是"理解整个界面",而是"补全 AX 缺失的局部信息"。比如某个按钮在 AX 树里没有 title,但 OCR 能识别出它上面写着"提交"——这就够了。

第四层:Remote Multimodal Reasoning

只有在前三层仍然无法解决问题时——比如面对一个完全自绘的界面、一个 AX 树为空的应用、或者一个需要复杂视觉理解的场景——才让外部多模态模型介入。

而且即便到了这一层,也不应该是"发一张全屏截图让模型自己看"。更合理的做法是:发送裁剪后的局部图像,附上已知的上下文信息(比如当前应用名称、窗口标题、已经从 AX 拿到的部分信息),让模型做有针对性的判断,而不是从零开始理解整个屏幕。

这四层不是互斥的,而是可以组合的。一个复杂任务的执行过程中,可能同时用到多层能力。关键是有一个清晰的优先级:能用高层接口解决的就不要退到低层,能在本地解决的就不要发到云端。

关于接口形式:MCP、CLI,还是内嵌 Tool

最近社区里关于 MCP 的讨论有所降温,CLI 的讨论反而多了起来。不少人认为,让 LLM 调用外部系统时,一个设计良好的 CLI 可能比 MCP 更直接、更透明、更容易调试。

我对 MCP 也有一些保留。MCP tools 的 schema 会随着工具定义一起注入模型上下文,当工具数量增多时,大量实际用不到的工具描述也会进入上下文,对 LLM 形成噪音和干扰。这不是 MCP 协议本身的问题,而是"工具发现与注入"这个环节的设计问题,但在实践中确实会遇到。

不过,我不想把文章引向"协议之争"。MCP、CLI、内嵌 tool,这些都只是能力的暴露形式。 真正重要的是底层能力的抽象是否清晰、是否可组合、是否低噪音。

如果底层的 observation / action 抽象设计得好,那么无论是包装成 MCP tool、CLI 命令、还是 Agent 框架里的内嵌函数,都只是接口层的选择,不影响核心架构。反过来,如果底层抽象混乱,换什么协议都救不了。

我目前倾向的工程实现方式

回到我自己的项目。目前我的想法是:

  1. 把 Computer Use 封装成 Agent 的一组 Tool,底层核心基于 Accessibility API,用 Swift 实现。AXSwift 可以作为起步的参考和实现入口,但我不打算让业务层直接依赖它的 API 设计——中间会有一层自己的抽象。

  2. 定义两个核心接口:observe 和 act。 observe 负责获取当前界面的结构化快照(经过稀疏化、面向 LLM 优化的),act 负责执行结构化动作(优先元素级动作,fallback 到坐标操作)。

  3. 在 observe 的实现里,混合使用 AX 遍历、AXObserver 事件、以及必要时的 Vision OCR 补全。 输出的快照格式需要反复调优,这可能是整个实现里最需要迭代的部分——什么信息该保留、什么该丢弃、怎么组织才能让 LLM 最高效地理解和决策。

  4. 对 scriptable 应用维护一个优先级列表,Agent 在规划操作时先查这个列表,能走 AppleScript 的就不走 AX。

  5. 视觉兜底作为独立模块存在,不与 AX 主路耦合。只有当 AX 模块明确报告"信息不足"时才触发。

这个方案不算特别复杂,但每一层都有不少工程细节需要处理。比如:AX 遍历的性能优化(有些应用的 UI 树非常深)、快照格式的设计与迭代、AppleScript 能力的探测与适配、Vision OCR 结果与 AX 节点的对齐与融合……这些都不是写个 demo 能覆盖的,需要在真实场景里反复打磨。

结尾

回过头看,从去年黑客松的"伪 Computer Use"到现在,我对这个问题的理解经历了几次明显的转变:

最初觉得"截图 + 多模态模型"是最自然的路线,因为它最像人类使用电脑的方式。后来意识到,"像人一样"未必是 AI 的最优路径——AI 不需要通过眼睛来理解界面,它可以直接读取界面背后的结构化描述。

最初想找一个现成的库或框架来解决问题。后来发现,这个领域还没有成熟的现成方案,更重要的是自己定义一套合理的分层能力抽象,然后在每一层选择合适的实现。

最初把"Computer Use"理解为"让 AI 像人一样操作电脑"。现在更倾向于认为,Computer Use 的本质不是"让 AI 模拟人类的 GUI 操作",而是"让 AI 通过比 GUI 更底层、更结构化的系统接口来控制计算机"。

GUI 是人机交互的界面,不是机机交互的界面。对 AI 来说,Accessibility tree 比截图更好用,AppleScript 比鼠标点击更可靠,结构化动作比坐标定位更稳定。

macOS 恰好在这些方面提供了丰富的系统级支持。这不是偶然——无障碍能力本来就是为了让"非视觉用户"也能使用电脑而设计的,而 AI Agent 在某种意义上,正是一个"非视觉用户"。

把这条路走通,也许比让 AI"学会看屏幕"更有价值。

(完)


留言讨论