ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AX 失灵后,如何让 AI 继续操作 macOS?多通道感知与 LLM 决策实战

AX 失灵后,如何让 AI 继续操作 macOS?多通道感知与 LLM 决策实战 1. 当 AX 失灵时AI 操作 macOS 的破局思路做过 macOS 自动化的人大概都踩过同一个坑昨天还跑得好好的脚本今天系统一升级AXUIElement返回的树结构就变了样按钮找不到、窗口层级错乱、某些 Electron 应用干脆返回一个空的无障碍树。这时候你盯着满屏的kAXErrorCannotComplete心里只有一个念头——这活儿还能不能干下去了。我最近在折腾一个让 LLM 驱动 macOS 桌面操作的小项目核心场景是用户用自然语言描述一个任务比如帮我把 Safari 里那个 PDF 下载下来并重命名AI 需要自己看懂屏幕、找到控件、点击、输入、验证结果。整个链路里最脆的一环就是AXAccessibility 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 视觉通道截图 多模态 LLMAX 失效时最直接的替代方案就是截图。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。它真正擅长的是理解意图、规划步骤、从模糊信息里做判断。所以我的架构是这样的感知层AX 优先失败降级到截图输出结构化的屏幕状态描述决策层LLM 接收状态描述 任务目标输出下一步动作点击某坐标、输入某文本、等待执行层用 CGEvent 或 AX Action 执行动作验证层重新感知判断是否达成子目标反馈给 LLMLLM 只负责第 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按组合键如 CmdSscroll滚动wait等待用于加载其他复杂操作都可以用这六种组合出来。动作空间越小模型的决策越稳定。5. 实操搭一套能跑的混合方案5.1 环境准备与权限配置先把基础环境搭起来。macOS 上做自动化辅助功能权限是绕不开的。你的程序需要在系统设置 → 隐私与安全性 → 辅助功能里被勾选否则 AX 和 CGEvent 都会被系统拦截。# 检查当前进程是否有辅助功能权限 # 可以用 AXIsProcessTrusted() 这个 APIPython 环境下我推荐用pyobjc直接调原生 API比封装库更可控pip install pyobjc-framework-Quartz pyobjc-framework-ApplicationServices截图用Quartz.CGWindowListCreateImage点击用Quartz.CGEventCreateMouseEventAX 用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, threshold0.02): diff compute_pixel_diff(before, after) return diff thresholdthreshold别设太小光标闪烁、时钟走动都会造成微小差异。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 最痛苦的就是它为什么这么点有了日志一切都有迹可循。
返回列表