从 12.9 秒到 0.3 秒:我把 GUI Agent 的定位速度干掉了 40 倍
先说结论
我在一台 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 加 stdout=subprocess.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 有用得多。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)