
先说结论我在一台 4GB 显存的旧笔记本上做 GUI Agent(让 AI 看屏幕、点鼠标)。纯视觉方案定位一个按钮要 12.9 秒,换成无障碍树优先后只要 0.3 秒。这不是理论推导,是我真跑出来的数据,包括中间踩的三个坑。如果你也在做 computer use / GUI automation,这篇可能帮你省几天。背景:我要解决什么问题我有个自用的 AI 助手(命令行工具),我给它加了视觉操控能力:截图 → 云端视觉模型识别 → xdotool 点击。核心痛点是慢。实测:定位屏幕上的关闭按钮 12.5 秒 描述屏幕内容 6.5 秒 一个 3 步的多步任务 25 秒12 秒什么概念?我让它点个按钮,得等十几秒。这个延迟在真实使用里基本不可接受 —— 用户盯着黑屏等,不知道它在干嘛。更糟的是不准:视觉模型给的是大概位置,偶尔会点偏。转机:一次框架考古我去翻了业界几个成熟框架的源码和文档,重点看三家的设计:UI-TARS(字节)动作输出用 Python 函数调用串而不是 JSON;解析失败有多层修复;prompt 要求先写小步计划,末句总结下一步动作。Anthropic Computer Use 最佳实践截图必须预缩放到 1280x720(别让 API 自动缩,会糊);文本指令要放在图片之前(先知道要找什么,再看图);用rolling buffer只保留最近 3 张图。UACC 和 screenpipe(这两个是关键)UACC 提出结构化 UI 文本地图——纯文本模型也能定位元素;screenpipe 的原则是Accessibility tree 优先于截图。第 3 条点醒了我:为什么非要看图猜?操作系统本来就知道每个按钮在哪啊。实测:Linux 的 AT-SPI 能直接读出元素树AT-SPI 是 Linux 的无障碍接口。它的设计初衷是给读屏软件用的,但它暴露的能力正好是 GUI Agent 需要的:元素名 角色 屏幕精确坐标我实测了一下(一个终端窗口):frame | 终端 | 中心(993,556) push button | 最小化 | 中心(1817,55) push button | 还原 | 中心(1857,55) push button | 关闭 | 中心(1897,55) toggle button | 菜单 | 中心(1769,55) label | 终端 | 中心(993,54) push button | 新建标签页 | 中心(90,55)注意:中文元素名原生保留,不需要 OCR,不需要翻译,不需要猜。坐标精确到什么程度?关闭按钮的 bbox 是 (1880,40,34x30),中心点 (1897,55) —— 这是窗口管理器自己报的位置,不是模型估的。关键实现(踩了坑)坑一:不能用 AT-SPI 自己的活动窗口状态我第一版是这么写的:遍历所有应用,找状态是 ACTIVE 的窗口。结果它一直返回 gnome-shell —— 因为 gnome-shell 永远处于活动状态。读出来的元素是桌面图标和活动概览,不是我要找的应用。正确做法:用 X11 的焦点窗口 PID 去匹配 AT-SPI 的应用进程 ID。# 拿焦点窗口 PID wid subprocess.run([xdotool, getwindowfocus], ...).stdout focus_pid subprocess.run([xdotool, getwindowpid, wid], ...) # 在 AT-SPI 里找同 PID 的应用 for app in atspi_apps: if app.get_process_id() focus_pid: return app这个 PID 匹配是关键。AT-SPI 的应用对象有 get_process_id() 方法,和 X11 报的窗口 PID 是同一个。坑二:有些应用不给 AT-SPI 暴露内容微信(WINE 程序)只返回一个空 frame,内部元素读不到。这类应用必须回退到视觉方案。所以最终做成了三级定位:级别 1: AT-SPI 0.3 秒 精确,但需要应用支持 a11y 级别 2: OCR 0.5-8 秒 文本类目标可用(tesseract 中文包) 级别 3: 视觉模型 4-13 秒 万能兜底实测结果(同一台机器,同一个关闭按钮):AT-SPI: 322ms / 343ms / 332ms (三次) 纯视觉模型: 12920ms (一次)40 倍差距。第二个优化:坐标缓存微信这类 WINE 应用用不了 AT-SPI,只能走视觉模型(5 秒一次)。但它的界面元素位置是稳定的。我加了个坐标缓存,key 是窗口标题几何目标描述:冷启动(第一次定位): 5060ms 缓存命中: 14ms ← 367 倍窗口移动/缩放时 key 会变,缓存自动失效,不会点错地方。第三个优化:元素地图注入多步任务里,我把 AT-SPI 读到的元素地图直接喂给模型:界面元素地图(精确定位可直接用, 坐标已是屏幕真实坐标): push button | 关闭 | 中心(1897,55) push button | 新建标签页 | 中心(90,55) ...然后在 prompt 里写一句:“如果界面元素地图里有目标元素的精确坐标,优先用它而不是看图猜”。实测效果很直接:一个读取终端窗口标题的任务,之前(纯看图): 25 秒,3 步 现在(带地图): 3 秒,1 步模型的原话是:“地图中 label「终端」位于 (993,54) 也印证了这一点。”它真的在用这个地图。三个值得说的失败教训教训 1:我的自动化工具把自己打了我在测按 Escape 键时,焦点正好在我自己运行的终端上。而这个终端里跑的就是 AI 助手本身,它的 Escape 键是打断当前回合。结果:我按的 Escape 打进了我自己,把自己正在跑的命令中断了两次。修法:发键前先检查焦点窗口的 PID,如果在自己的进程链里就拒绝。这是个 fail-closed 设计 —— 拿不到信息就拒绝,而不是放行。教训 2:一个 60 秒的卡死,根因是管道继承我写了个粘贴中文功能(xdotool 对中文支持不好,用剪贴板更稳)。结果它卡死 60 秒不返回。根因很隐蔽:xclip 会 fork 一个 daemon 来保持剪贴板内容,而这个 daemon 继承了本进程的 stdout —— 那个 stdout 是调用方捕获输出的管道。本进程退出了,但 daemon 还攥着管道写端,调用方读端等 EOF 永远等不到。修法一行:subprocess 加 stdoutsubprocess.DEVNULL。有意思的是,我一开始以为用 xclip -loops 1能解决,实测那是错的(内容即失)。真正的修法是切断管道继承。教训 3:验证太早,把成功的操作当成失败我给微信发消息,输入1后立即截图验证,模型说输入框是空的。我就又输入了一次。结果查出来输入框里是11 —— 第一次其实成功了,只是 WINE 应用响应有延迟,验证时还没渲染出来。修法:不再用固定 sleep,改成连续两次截图指纹相同来判断界面稳定。写在最后三个优化加起来,我这个 GUI Agent 的定位从 12.9 秒降到 0.3 秒。而且这些优化都不需要换硬件 —— 我用的还是一台 4GB 显存的旧笔记本。核心思路其实就一句话:AI 不需要看得更好,它需要问得更聪明。 操作系统知道的东西,就别让模型去猜。代码用的都是系统自带组件(gi.repository.Atspi、xdotool、tesseract),没有额外依赖。完整代码已开源(MIT):https://github.com/asd369932/gui-agent-fast里面有个 benchmark.py,可以复现上面所有数据。如果你在做类似的事,建议先花半天时间看看 AT-SPI 能给你什么 ——可能比调 prompt 有用得多。