☰
AX 失灵后,如何让 AI 继续操作 macOS?多通道感知与 LLM 决策实战
2026/9/26 5:55:50 网站建设 项目流程

1. 当 AX 失灵时,AI 操作 macOS 的破局思路

做过 macOS 自动化的人大概都踩过同一个坑:昨天还跑得好好的脚本,今天系统一升级,AXUIElement返回的树结构就变了样,按钮找不到、窗口层级错乱、某些 Electron 应用干脆返回一个空的无障碍树。这时候你盯着满屏的kAXErrorCannotComplete,心里只有一个念头——这活儿还能不能干下去了。

我最近在折腾一个让 LLM 驱动 macOS 桌面操作的小项目,核心场景是:用户用自然语言描述一个任务,比如"帮我把 Safari 里那个 PDF 下载下来并重命名",AI 需要自己看懂屏幕、找到控件、点击、输入、验证结果。整个链路里最脆的一环就是AX(Accessibility API)。它本该是 AI 的"眼睛和手",但现实是它经常瞎、经常瘸。所以这篇文章要聊的就是:当 AX 不可靠时,怎么让 AI 继续把 macOS 上的活干完。

内容会覆盖 AX 为什么会失效、失效后有哪些替代感知手段、坐标体系怎么建立、LLM 在其中扮演什么角色、以及一套我实测能跑通的混合方案。适合正在做 AI Agent、桌面自动化、RPA 工具,或者单纯想让 LLM 帮你操作 Mac 的开发者。哪怕你之前没碰过 AX,我也会把基础概念用生活化的方式讲清楚,保证能看懂、能抄作业。

2. 先搞清楚 AX 到底是个什么东西

2.1 AX 的本质:macOS 给辅助工具开的一扇窗

AX 全称 Accessibility,是 macOS 提供的一套跨进程通信接口。它的设计初衷是给视障用户用的——屏幕阅读器需要知道"当前窗口里有哪些按钮、哪些文本框、它们叫什么名字、在什么位置"。后来做自动化的人发现,这套接口拿来控制应用简直太方便了,于是就有了各种基于 AX 的自动化工具。

从技术上讲,AX 是一棵树。根节点是应用,往下是窗口、分组、按钮、文本字段等等。每个节点是一个AXUIElement,你可以查询它的属性(kAXRoleAttribute、kAXTitleAttribute、kAXPositionAttribute)、执行动作(kAXPressAction)、设置值。听起来很美好,对吧?

问题在于,这棵树是应用自己上报的。应用愿意报多少、报得准不准,完全取决于开发者。系统只能"请求",不能"强制"。

2.2 AX 失效的五种典型场景

我把实际遇到的 AX 翻车情况归了类,基本跑不出这五种:

失效类型典型表现常见应用
树结构缺失返回空树或只有根节点部分 Electron 应用、游戏
属性不准position/size 返回 0 或错误值自绘 UI 的应用
层级错乱控件嵌套关系与视觉不符复杂 Web 应用
响应超时kAXErrorCannotComplete卡顿或沙盒严格的应用
权限受限需要辅助功能授权但拿不到系统级安全应用

最坑的是第一种。你调用AXUIElementCreateApplication拿到应用对象,然后遍历子节点,结果发现整棵树就一个根,下面什么都没有。这时候你连"屏幕上有个按钮"这件事都不知道,更别说点了。

提示:判断 AX 是否可用,不要只看有没有报错。要实际遍历一层子节点,检查返回的数组长度和 role 分布。空树往往不报错,只是静默返回空。

2.3 为什么不能死磕 AX

有人可能会想:AX 不行,那我就想办法修 AX 呗。方向错了。AX 的可靠性不由你控制,它取决于目标应用的实现质量、系统版本、甚至当前的内存状态。你花三天适配好的树结构,应用发个版本就全废了。

正确的思路是:把 AX 当成一个"高优先级但可能失败"的感知通道,失败时自动降级到其他通道。这就是所谓的"多模态感知"。AI 操作桌面,本质上和人类操作桌面是一回事——眼睛看(视觉)、手摸(坐标)、记忆(状态),AX 只是其中一种"看"的方式,而且是最省事的那种。

3. 多通道感知:给 AI 装上备用的眼睛

3.1 视觉通道:截图 + 多模态 LLM

AX 失效时,最直接的替代方案就是截图。macOS 上用CGWindowListCreateImage或者screencapture命令都能拿到屏幕图像,然后丢给多模态 LLM(比如支持视觉的模型),让它描述"屏幕上有什么、目标控件大概在哪个位置"。

这条路的好处是通用——只要人眼能看见,模型就能看见,不依赖应用配合。坏处是精度和成本:模型给出的坐标往往是粗略的,而且每次调用都要传图,token 消耗不小。

我的做法是分层使用:先用 AX 尝试定位,失败后截图,让模型输出目标控件的归一化坐标(0~1 之间的相对值),再换算成屏幕绝对坐标。归一化坐标的好处是分辨率无关,换台显示器也不用改代码。

3.2 坐标通道:从归一化到屏幕绝对坐标

坐标这块是重灾区,我单独拎出来讲。macOS 的坐标体系有几个坑:

  • 原点在左上角,y 轴向下,和数学坐标系相反
  • Retina 屏有缩放因子,逻辑坐标和物理像素不是 1:1
  • 多显示器有各自的坐标系,主屏原点是 (0,0),副屏可能是负值
  • 窗口坐标和屏幕坐标要转换,AXPosition给的是屏幕坐标,但有些 API 给的是窗口相对坐标

换算公式其实不复杂,但容易搞混。假设模型给出归一化坐标(nx, ny),屏幕逻辑尺寸是(W, H),那么:

screen_x = nx * W screen_y = ny * H

如果要做点击,还得考虑 Retina 缩放。用CGEventCreateMouseEvent创建点击事件时,传入的是逻辑坐标,系统会自动处理缩放。但如果你是从截图的像素坐标反推,就要先除以缩放因子:

scale = NSScreen.main.scale // 通常是 2.0 logical_x = pixel_x / scale logical_y = pixel_y / scale

注意:多显示器场景下,NSScreen.screens的顺序和坐标系不一定对应。建议遍历所有屏幕,用frame判断目标点落在哪个屏幕内,再做局部换算。

3.3 状态通道:用辅助信息交叉验证

光有视觉和坐标还不够,AI 需要知道"我上一步操作有没有生效"。这时候可以引入状态通道:

  • 操作前后各截一张图,做像素级 diff,判断界面是否变化
  • 读取剪贴板内容,验证复制/粘贴是否成功
  • 监听窗口标题变化,判断页面是否跳转
  • 查询进程列表,确认应用是否启动

这些信号单独看都很弱,但组合起来就能给 LLM 提供足够的反馈,让它知道该继续还是该重试。

4. 让 LLM 做决策:从感知到行动的闭环

4.1 LLM 在链路里的角色定位

很多人一上来就想让 LLM "直接控制电脑",这是误区。LLM 不擅长精确的坐标计算,也不擅长处理底层 API。它真正擅长的是理解意图、规划步骤、从模糊信息里做判断。

所以我的架构是这样的:

  1. 感知层:AX 优先,失败降级到截图,输出结构化的"屏幕状态描述"
  2. 决策层:LLM 接收状态描述 + 任务目标,输出下一步动作(点击某坐标、输入某文本、等待)
  3. 执行层:用 CGEvent 或 AX Action 执行动作
  4. 验证层:重新感知,判断是否达成子目标,反馈给 LLM

LLM 只负责第 2 步,其他都是确定性代码。这样既发挥了 LLM 的语义理解能力,又避免了它在精确计算上的短板。

4.2 提示词设计:让模型输出可执行的坐标

给 LLM 的提示词里,最关键的是约束输出格式。我实测下来,让模型输出 JSON 比输出自然语言靠谱得多:

{ "action": "click", "target": "下载按钮", "normalized_x": 0.85, "normalized_y": 0.12, "confidence": 0.9, "reasoning": "右上角有一个向下的箭头图标,符合下载按钮特征" }

confidence字段很重要。低于阈值(比如 0.6)时,不要盲目执行,而是让模型重新观察或者请求人工确认。reasoning字段则是为了调试——出问题时你能看到模型"当时在想什么"。

4.3 动作空间设计:少即是多

动作类型不要设计太多。我一开始搞了二十几种动作,结果模型经常选错。后来砍到六种核心动作,准确率立刻上来了:

  • click:单击某坐标
  • double_click:双击
  • type:输入文本
  • key:按组合键(如 Cmd+S)
  • scroll:滚动
  • wait:等待(用于加载)

其他复杂操作都可以用这六种组合出来。动作空间越小,模型的决策越稳定。

5. 实操:搭一套能跑的混合方案

5.1 环境准备与权限配置

先把基础环境搭起来。macOS 上做自动化,辅助功能权限是绕不开的。你的程序需要在"系统设置 → 隐私与安全性 → 辅助功能"里被勾选,否则 AX 和 CGEvent 都会被系统拦截。

# 检查当前进程是否有辅助功能权限 # 可以用 AXIsProcessTrusted() 这个 API

Python 环境下,我推荐用pyobjc直接调原生 API,比封装库更可控:

pip install pyobjc-framework-Quartz pyobjc-framework-ApplicationServices

截图用Quartz.CGWindowListCreateImage,点击用Quartz.CGEventCreateMouseEvent,AX 用ApplicationServices.AXUIElementCreateApplication。这三个模块基本覆盖了所有需求。

5.2 感知层的降级逻辑实现

核心是一个perceive()函数,返回统一的屏幕状态描述:

def perceive(app_pid): # 第一优先级:AX ax_tree = try_ax_tree(app_pid) if ax_tree and len(ax_tree.children) > 0: return {"source": "ax", "tree": ax_tree} # 降级:截图 + 视觉模型 screenshot = capture_screen() description = vision_model.describe(screenshot) return {"source": "vision", "image": screenshot, "desc": description}

关键在try_ax_tree里要加超时和深度限制。有些应用遍历 AX 树会卡死,必须设个上限,比如最多遍历 500 个节点、单次查询超时 200ms。

5.3 坐标换算的完整代码

这块我踩坑最多,直接上代码:

import Quartz def get_screen_size(): main = Quartz.NSScreen.mainScreen() frame = main.frame() return frame.size.width, frame.size.height def normalized_to_screen(nx, ny): w, h = get_screen_size() return nx * w, ny * h def click_at(x, y): point = Quartz.CGPointMake(x, y) down = Quartz.CGEventCreateMouseEvent( None, Quartz.kCGEventLeftMouseDown, point, Quartz.kCGMouseButtonLeft) up = Quartz.CGEventCreateMouseEvent( None, Quartz.kCGEventLeftMouseUp, point, Quartz.kCGMouseButtonLeft) Quartz.CGEventPost(Quartz.kCGHIDEventTap, down) Quartz.CGEventPost(Quartz.kCGHIDEventTap, up)

提示:点击事件之间加 50~100ms 延迟,太快了应用可能来不及响应。我实测 80ms 是个比较稳的值。

5.4 验证层的像素 diff 实现

操作后判断界面是否变化,最简单的办法是截图做差:

def screen_changed(before, after, threshold=0.02): diff = compute_pixel_diff(before, after) return diff > threshold

threshold别设太小,光标闪烁、时钟走动都会造成微小差异。2% 是个经验值,能过滤掉噪声又不会漏掉真实变化。

6. 常见问题与排查技巧实录

6.1 AX 返回空树怎么办

先确认权限。没授权的话 AX 会静默返回空,不报错。授权后还是空,那就是应用本身没实现无障碍。这时候别浪费时间,直接走视觉通道。

6.2 点击位置总是偏一点

九成是坐标换算错了。检查三件事:屏幕缩放因子、多显示器坐标系、窗口是否全屏。我建议写个调试模式,把点击位置画个红点截图出来,一眼就能看出偏在哪。

6.3 模型给的坐标不稳定

同一个界面,模型两次给的坐标可能差几十像素。解决办法是多次采样取中位数,或者让模型输出一个区域而不是点,取区域中心。另外提示词里明确要求"输出目标控件的中心点"也能提升稳定性。

6.4 操作太快导致失败

macOS 的 UI 动画有延迟,点击后立刻截图可能还是旧画面。加个wait_for_stable函数,连续截三张图,直到两张之间 diff 小于阈值再继续。

问题排查方向快速修复
AX 空树权限 / 应用实现降级视觉
坐标偏移缩放 / 多屏画点调试
坐标抖动模型采样取中位数
操作无效动画延迟等待稳定
权限丢失系统更新重新授权

6.5 密钥与鉴权信息泄露风险

用 LLM 时,提示词里千万别塞 API Key、密码这类东西。截图也可能包含敏感信息,传给模型前最好做区域裁剪或打码。我一般只截目标窗口,不截全屏,减少信息暴露面。

7. 一些实测下来的经验

这套方案我跑了大概两个月,覆盖了浏览器、Finder、部分原生应用和几个 Electron 应用。AX 能搞定的场景大概占六成,剩下四成靠视觉兜底。整体成功率从纯 AX 方案的七成出头,提到了九成以上。

最深的体会是:别追求单一通道的完美,要追求多通道的冗余。AX 快但脆,视觉慢但稳,坐标直接但需要校准。三者组合起来,AI 才真正能在 macOS 上"看得见、点得准、干得成"。

另外,LLM 的提示词要持续迭代。我每周都会把失败的 case 捞出来,看模型当时收到的状态描述和它的决策,然后针对性调整提示词。这个过程没有终点,但每轮都能涨几个点的成功率。

最后分享一个小技巧:给每个动作加一个唯一 ID 和日志,出问题时能完整回放整个决策链路。调试 AI Agent 最痛苦的就是"它为什么这么点",有了日志,一切都有迹可循。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询