CUA框架实战:视觉感知+输入模拟的桌面自动化
1. 从“cua”这个标题说起一个被低估的自动化交互框架第一次看到“cua”这个标题的时候我脑子里蹦出来的第一反应是——这大概率又是一个缩写词。在技术圈混久了你会发现大家特别喜欢用三个字母来命名项目仿佛三个字母就能承载一整套技术哲学。但“cua”这个组合有点意思它不像“api”“sdk”“cli”那样一眼就能猜到含义反而带着一种刻意模糊的调性。我后来花了不少时间去翻社区讨论、看相关项目文档才慢慢拼凑出“cua”在这个语境下的真实面目它指向的是一类计算机使用代理Computer Use Agent的实践方向。说白了就是让程序像人一样去操作电脑——移动鼠标、点击按钮、敲键盘、读取屏幕内容完成一系列原本需要人工介入的桌面操作任务。这个方向为什么突然热起来了因为大模型的能力边界正在从“对话”往“执行”迁移。以前我们让AI写一段代码它给你文本你自己去跑。现在大家想要的是AI直接帮你把代码跑了把结果截图给你看。这中间的鸿沟就是“cua”这类框架要填的坑。我写这篇东西的出发点很简单网上关于cua的讨论要么太学术满屏的论文术语要么太浅就贴个demo视频说“看它能点按钮了”。真正从工程落地角度讲清楚“这东西怎么搭、坑在哪、什么场景值得用”的内容少得可怜。所以我想把自己在这块折腾的经验整理出来给正在观望或者已经入坑的朋友一些参考。这篇文章适合谁看如果你是对桌面自动化感兴趣的后端或全栈开发者或者你在做RPA相关的产品选型再或者你只是好奇“让AI操作电脑”这件事到底靠不靠谱那接下来的内容应该能帮你省下不少试错时间。我会从整体设计思路讲到具体实现细节再到踩过的坑和排查方法尽量做到看完就能动手。2. 整体设计思路为什么是“看屏幕模拟输入”这条路2.1 核心架构的选型逻辑做桌面自动化摆在面前的路其实有好几条。第一条是系统级API调用比如Windows的UI Automation、macOS的Accessibility API直接通过系统暴露的接口去操控控件。这条路最稳速度最快但问题是跨平台适配成本极高而且很多现代应用尤其是Electron套壳的根本不暴露标准控件树你拿到的是一堆无意义的容器节点。第二条路是注入式方案往目标进程里注入代码直接调用内部函数。这条路效率最高但风险也最大——容易被安全软件拦截不同版本的应用内部结构一变就全废维护成本高得吓人。第三条路就是cua框架普遍采用的视觉感知输入模拟方案。它的逻辑很朴素截屏拿到当前画面用视觉模型理解画面上有什么然后决定点哪里、输入什么最后通过操作系统的输入事件接口模拟鼠标键盘动作。这条路的好处是通用性极强——只要是人能用鼠标键盘操作的应用它就能操作不依赖任何内部接口。代价是速度慢、精度受视觉模型能力限制、对动态画面的处理比较吃力。我选择这条路作为主要实践方向原因很实际通用性带来的长尾价值远大于效率损失。你不可能为每个目标应用都写一套适配层但你可以写一套通用的视觉交互层让它去适应所有应用。这个 trade-off 在大多数场景下是划算的。2.2 感知-决策-执行循环的设计cua框架的核心是一个不断循环的三段式流程我把它叫做PDA循环Perceive-Decide-Act感知阶段截取当前屏幕画面可能还需要获取鼠标位置、活动窗口信息、剪贴板内容等辅助信号。这一步的关键是降噪——原始截屏包含大量无关信息需要做区域裁剪、缩放、格式转换才能喂给视觉模型。决策阶段把感知到的画面和当前任务目标一起送给模型让模型输出下一步动作。动作空间通常包括点击某个坐标、输入文本、按键组合、滚动、等待。这里最大的挑战是坐标映射——模型看到的是缩放后的图像输出的坐标需要还原到真实屏幕坐标系。执行阶段把决策结果翻译成操作系统能理解的输入事件。这一步看似简单实际上坑很多后面会详细讲。这个循环的频率直接决定了整个系统的响应速度。我实测下来如果视觉模型走本地推理单次循环大概在800毫秒到2秒之间如果走远程API受网络延迟影响可能到3到5秒。这个速度对于“帮我填个表单”这类任务勉强够用但对于需要实时响应的场景就完全不够看了。2.3 与纯脚本自动化的本质区别很多人会问这和AutoHotkey、Selenium这类传统自动化工具有什么区别区别在于决策的来源。传统脚本的每一步都是人预先写死的——“点击坐标(100,200)等待500毫秒输入‘hello’”。cua框架的每一步是模型根据当前画面动态生成的。这意味着它能处理脚本无法覆盖的情况弹窗位置变了、按钮文字改了、页面加载慢了它都能自己调整。但这也带来了新的问题不确定性。脚本的行为是完全可预测的cua的行为不是。同一个任务跑两次可能走的路径完全不同。这在生产环境里是个大问题需要额外的机制来约束和校验。3. 核心细节解析感知层、决策层、执行层的关键实现3.1 感知层截屏不只是截屏截屏这件事听起来简单但要做好其实有很多讲究。最基础的实现就是调用系统截屏接口拿一张全屏图但直接拿到的图往往不能直接用。首先是分辨率问题。现在显示器动辄2K、4K全屏截图可能达到3840x2160甚至更高。直接把这么大的图送给视觉模型一是token消耗巨大二是模型对高分辨率图像的细节理解反而可能下降。我的做法是等比缩放到一个固定宽度比如1280或1920保持宽高比。这样既保留了足够的视觉信息又控制了输入尺寸。缩放比例需要记录下来因为后面要把模型输出的坐标还原回去。计算公式很简单真实坐标_x 模型输出坐标_x * (原始宽度 / 缩放后宽度) 真实坐标_y 模型输出坐标_y * (原始高度 / 缩放后高度)但这里有个坑多显示器场景。如果用户接了多个屏幕截屏接口返回的可能是所有屏幕的拼接图坐标系原点在左上角。你需要先确定目标窗口在哪个屏幕上然后做相应的坐标偏移。我在这上面栽过跟头模型点得好好的结果点到另一个屏幕上去了。另一个细节是截屏时机。如果在上一个动作执行后立刻截屏可能画面还没更新完拿到的是旧画面。我的经验是至少等待300到500毫秒再截屏对于动画较多的界面可能需要等更久。更稳妥的做法是检测画面变化——连续截两张图做差异对比等差异小于某个阈值再进入决策阶段。3.2 决策层提示词工程与动作空间设计决策层的核心是如何让模型稳定地输出可执行的动作。这里提示词的设计至关重要。我试过很多版本最后稳定下来的结构大概是这样的你是一个桌面操作助手。当前屏幕截图如下。 你的任务是[具体任务描述] 请输出下一步动作格式为JSON {action: click|type|key|scroll|wait, params: {...}} 可用动作说明 - click: 点击指定坐标params: {x: int, y: int, button: left|right} - type: 输入文本params: {text: str} - key: 按键params: {keys: [ctrl, c]} - scroll: 滚动params: {x: int, y: int, direction: up|down, amount: int} - wait: 等待params: {ms: int}这个格式看起来简单但有几个关键点第一动作空间要克制。一开始我设计了十几种动作结果模型经常选错。后来砍到五种核心动作准确率明显提升。动作越少模型越不容易迷惑。第二坐标要明确要求整数。模型有时候会输出浮点数或者带单位的字符串解析起来很麻烦。在提示词里明确说“整数像素坐标”能减少很多解析错误。第三要加“思考”字段。我让模型在输出动作之前先用一句话说明它看到了什么、为什么要这么做。这个字段不参与执行但能帮我调试——当模型做错事的时候看它的“思考”就知道是感知错了还是决策错了。还有一个容易被忽略的点历史动作的上下文。如果只给模型当前画面它不知道上一步做了什么可能会陷入死循环——点一个按钮没反应它就反复点。我的做法是把最近3到5步的动作和结果附在提示词里让模型知道“我已经点过这个按钮了但画面没变化应该试试别的”。3.3 执行层输入模拟的精度控制执行层是把决策翻译成实际输入事件的地方。这部分看起来最直接实际上坑最多。鼠标点击的坑在于点击位置和点击时机。模型输出的坐标是图像上的像素位置但实际点击时你需要确保目标窗口是活动窗口否则点击可能落到别的窗口上。我的做法是在每次点击前先通过窗口标题或进程名找到目标窗口把它激活并置顶然后再执行点击。另一个坑是双击和拖拽。很多界面需要双击打开文件或者拖拽移动元素。这些复合动作需要精确控制按下和释放之间的间隔。我实测下来双击的间隔在50到100毫秒之间比较合适太快了系统识别为两次单击太慢了又变成两次独立点击。拖拽则需要在中途加入几个移动事件让系统认为这是一个连续的拖拽动作而不是瞬间跳跃。键盘输入的坑在于输入法和特殊字符。如果目标应用当前处于中文输入法状态你模拟键盘输入英文字符可能会触发输入法的候选框导致输入内容不对。我的做法是在输入前先发送一个切换输入法的快捷键比如Shift确保处于英文状态。对于特殊字符比如、#这些不同键盘布局下对应的按键可能不同需要根据系统布局做映射。滚动的坑在于滚动量和滚动位置。不同应用的滚动灵敏度不一样有的滚一格走三行有的走十行。我的做法是先小量滚动观察画面变化如果变化不够再追加滚动。这比一次性滚动一个固定值要稳妥得多。4. 实操过程从零搭建一个可用的cua原型4.1 环境准备与依赖选型先说环境。我是在一台普通开发机上做的配置不算高16GB内存没有独立显卡。这个配置跑本地视觉模型比较吃力所以我采用的是混合方案——轻量级的画面理解用本地小模型复杂的决策走远程API。如果你有显卡可以考虑全部本地化延迟会低很多。依赖方面核心是这几个截屏库我用的是mss跨平台速度快支持多显示器。比PIL.ImageGrab快不少尤其是在高分辨率下。输入模拟库pynput和pyautogui我都试过。pyautogui的API更友好但pynput的底层控制更精细尤其是对按键事件的控制。最后我选了pynput做键盘pyautogui做鼠标。图像处理opencv-python主要用来做缩放和差异对比。视觉模型这个选择面比较广我试过几种方案后面会细说。安装命令大概是这样pip install mss pynput pyautogui opencv-python numpy requests注意在macOS上使用pynput和pyautogui需要在“系统设置-隐私与安全性-辅助功能”里给终端或IDE授权否则模拟的输入事件会被系统拦截。Windows上一般不需要额外授权但如果开了UAC某些操作可能需要以管理员权限运行。4.2 感知模块的实现细节感知模块的核心函数是capture_screen()它返回一个经过预处理的图像和缩放比例。我贴一下关键代码import mss import cv2 import numpy as np def capture_screen(target_width1280): with mss.mss() as sct: monitor sct.monitors[1] # 主显示器 screenshot sct.grab(monitor) img np.array(screenshot) img cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) original_h, original_w img.shape[:2] scale target_width / original_w target_h int(original_h * scale) resized cv2.resize(img, (target_width, target_h), interpolationcv2.INTER_AREA) return resized, scale, (original_w, original_h)这里有几个细节值得说sct.monitors[1]取的是主显示器。如果你需要截取特定窗口需要先获取窗口位置然后用{top: y, left: x, width: w, height: h}的方式指定区域。获取窗口位置可以用pygetwindow库。cv2.INTER_AREA是缩小图像时的推荐插值方法比默认的INTER_LINEAR效果更好边缘更清晰。缩放比例的计算要保留足够精度不要四舍五入成整数否则坐标还原时会有累积误差。还有一个实用技巧保存最近几张截图到本地。调试的时候非常有用你可以回看模型当时看到的是什么判断是感知问题还是决策问题。我一般保留最近10张用时间戳命名。4.3 决策模块的提示词与解析决策模块我封装成了一个函数输入是当前截图、任务描述、历史动作输出是一个结构化的动作对象。核心代码如下import base64 import json import requests def decide_action(screenshot, task, history, api_endpoint, api_key): _, buffer cv2.imencode(.jpg, screenshot, [cv2.IMWRITE_JPEG_QUALITY, 85]) img_base64 base64.b64encode(buffer).decode(utf-8) history_text \n.join([ f步骤{i1}: {h[action]} - {h[result]} for i, h in enumerate(history[-5:]) ]) prompt f你是一个桌面操作助手。当前屏幕截图如下。 任务{task} 最近操作历史 {history_text} 请分析当前画面输出下一步动作。格式为JSON {{thinking: 你的分析, action: click|type|key|scroll|wait, params: {{...}}}} 动作说明 - click: {{x: int, y: int, button: left}} - type: {{text: 要输入的文本}} - key: {{keys: [ctrl, c]}} - scroll: {{x: int, y: int, direction: up|down, amount: int}} - wait: {{ms: int}} response requests.post(api_endpoint, json{ model: vision-model, messages: [{ role: user, content: [ {type: text, text: prompt}, {type: image_url, image_url: {url: fdata:image/jpeg;base64,{img_base64}}} ] }], temperature: 0.1 }, headers{Authorization: fBearer {api_key}}) content response.json()[choices][0][message][content] # 提取JSON部分 start content.find({) end content.rfind(}) 1 return json.loads(content[start:end])这段代码里有几个我踩过坑之后加上的东西temperature0.1温度调低让模型的输出更确定。桌面操作这种任务不需要创造力需要的是稳定性。JPEG质量85截图转base64的时候用JPEG格式并控制质量可以显著减小传输体积。PNG虽然无损但体积可能是JPEG的5到10倍对于远程API调用来说传输时间就是延迟。JSON提取的容错模型有时候会在JSON前后加一些解释性文字直接json.loads会报错。用find和rfind定位花括号范围能提高解析成功率。历史动作的截断只保留最近5步太多了会占用大量token而且早期的动作对当前决策的参考价值有限。4.4 执行模块的坐标映射与动作执行执行模块的关键是坐标还原和动作分发。坐标还原的代码很简单def restore_coordinate(x, y, scale): return int(x / scale), int(y / scale)但实际执行的时候还需要考虑窗口偏移。如果截屏区域不是从屏幕原点开始的还需要加上偏移量。我一般会在截屏时记录区域信息执行时统一做偏移。动作分发的核心逻辑import pyautogui from pynput.keyboard import Controller as KeyboardController, Key keyboard KeyboardController() def execute_action(action, scale, offset(0, 0)): act action[action] params action[params] if act click: x, y restore_coordinate(params[x], params[y], scale) x offset[0] y offset[1] pyautogui.click(x, y, buttonparams.get(button, left)) return f点击了({x}, {y}) elif act type: keyboard.type(params[text]) return f输入了文本 elif act key: keys params[keys] # 处理组合键 for k in keys[:-1]: keyboard.press(k) keyboard.press(keys[-1]) keyboard.release(keys[-1]) for k in reversed(keys[:-1]): keyboard.release(k) return f按下了{.join(keys)} elif act scroll: x, y restore_coordinate(params[x], params[y], scale) amount params.get(amount, 3) if params[direction] down: amount -amount pyautogui.scroll(amount, xx, yy) return f滚动了{params[direction]} elif act wait: time.sleep(params[ms] / 1000) return f等待了{params[ms]}毫秒注意pyautogui默认有一个故障安全机制——当鼠标移动到屏幕左上角时会抛出异常终止程序。这个机制在调试时很有用但在实际运行中可能误触发。可以通过pyautogui.FAILSAFE False关闭但关闭之后一定要确保有其他的中断机制比如监听某个快捷键。4.5 主循环的组装与运行把上面三个模块串起来主循环大概长这样def run_task(task, max_steps50): history [] for step in range(max_steps): # 感知 screenshot, scale, original_size capture_screen() # 决策 action decide_action(screenshot, task, history, API_ENDPOINT, API_KEY) print(f步骤{step1}: {action[thinking]}) # 执行 result execute_action(action, scale) history.append({action: action, result: result}) # 检查是否完成 if action[action] wait and action[params][ms] 5000: print(模型请求长时间等待可能任务已完成或遇到问题) time.sleep(0.5) # 给界面反应时间 return history这个循环看起来简单但实际跑起来你会发现很多问题。比如模型可能陷入死循环反复执行同一个动作或者任务已经完成了模型还在继续操作。我后来加了一个完成检测机制——让模型在每次决策时额外输出一个done字段如果它认为任务已完成就输出{done: true}主循环检测到这个字段就退出。5. 常见问题与排查技巧实录5.1 模型点不准怎么办这是最常见的问题。模型输出的坐标和实际想点的位置有偏差可能差几个像素也可能差几十个像素。排查思路如下第一步确认缩放比例是否正确。打印出缩放比例和原始尺寸手动算一下模型输出的坐标还原后是多少和实际目标位置对比。如果偏差是系统性的比如总是偏左偏上那大概率是缩放或偏移的问题。第二步检查截屏区域。如果你截取的是特定窗口确认窗口位置是否在截屏后发生了变化。窗口移动会导致坐标系错位。第三步看模型输出的“思考”字段。如果模型说“我要点击登录按钮”但坐标明显不在按钮上那是模型的空间定位能力问题。可以尝试在提示词里加入网格辅助线——在截图上画一些参考线或坐标标注帮助模型定位。第四步考虑图像质量。如果截图压缩得太厉害按钮边缘模糊模型可能无法准确判断中心位置。适当提高JPEG质量或者对关键区域做局部放大。我实测下来坐标偏差在5个像素以内是可以接受的因为大多数按钮的点击热区都比较大。如果偏差超过20个像素就需要认真排查了。5.2 输入文本乱码或丢失这个问题通常和输入法状态有关。排查步骤先确认当前输入法状态。可以在输入前先发送Shift键切换中英文或者用key动作发送ctrlspace切换输入法。检查是否有输入延迟。有些应用对输入事件的响应比较慢连续快速输入可能导致字符丢失。可以在每个字符之间加一个小延迟比如time.sleep(0.05)。对于特殊字符确认键盘布局。比如在某些欧洲键盘布局下需要按AltGrQ而不是Shift2。这种情况需要根据系统布局做映射表。5.3 任务执行到一半卡住卡住的原因可能有很多我整理了一个排查表现象可能原因排查方法解决方案反复点击同一位置模型陷入死循环查看历史动作在提示词中加入“如果上一步动作没有产生画面变化请尝试其他操作”等待时间过长模型误判需要等待查看wait动作的ms值设置最大等待时间上限超过则强制进入下一步画面无变化目标窗口未激活检查活动窗口在执行动作前先激活目标窗口坐标偏移缩放或偏移计算错误打印坐标还原过程重新校准缩放比例和偏移量输入无响应输入法或焦点问题检查输入法状态和焦点窗口切换输入法点击目标输入框后再输入5.4 性能优化的几个实用技巧减少截屏频率。不是每一步都需要重新截屏。如果上一步是wait动作等待期间画面不会变化可以直接复用上一张截图。局部截屏。如果任务只涉及屏幕的某个区域可以只截取那个区域减少图像尺寸和传输时间。缓存模型响应。对于完全相同的画面和任务状态可以缓存模型的决策结果避免重复调用。异步执行。截屏、决策、执行这三个阶段可以做成流水线截屏和决策可以并行——在等待模型响应的时候预先截取下一张图。提示性能优化要在功能稳定之后再做。我一开始就想着优化结果功能还没跑通优化了半天发现方向错了白费功夫。5.5 安全性与稳定性考量让一个程序自动操作你的电脑安全性是必须考虑的问题。我的做法是限制操作范围在代码里硬编码一个允许操作的窗口列表只对这些窗口执行动作避免误操作其他应用。设置紧急停止监听一个全局快捷键比如Esc连按三次触发后立即终止所有操作。操作日志记录每一步的动作、截图、模型输出方便事后审计和排查。沙箱环境如果可能在虚拟机或独立用户账户下运行避免影响主环境。稳定性方面最重要的是异常处理。截屏可能失败模型API可能超时输入模拟可能被拦截。每一个环节都要有try-except并且定义好失败后的重试或降级策略。6. 适用场景与边界分析什么时候该用cua什么时候不该用6.1 高价值场景cua框架最适合的场景有一个共同特征任务流程相对固定但界面细节经常变化。比如跨应用的重复性数据录入从A系统读数据填到B系统的表单里。传统脚本需要针对B系统的界面写死坐标一旦B系统改版就失效。cua框架通过视觉理解能自适应界面变化。软件安装与配置安装包的向导界面千差万别但操作逻辑大同小异——下一步、同意协议、选择路径、完成。cua框架可以处理这种“逻辑相同但界面不同”的任务。网页操作的补充有些网页操作无法通过DOM接口完成比如Canvas绘制的图表、Flash内容、或者有反自动化检测的页面。cua框架从视觉层面操作绕过了这些限制。测试与演示自动走一遍用户流程截图记录每一步的界面状态用于回归测试或产品演示。6.2 不适合的场景反过来以下场景我不建议用cua框架高频交易或实时控制延迟太高等模型决策完机会已经过去了。需要精确像素级操作的设计工作比如在Photoshop里画一条精确的曲线模型很难做到像素级精度。涉及敏感数据的操作把屏幕截图发给远程模型存在数据泄露风险。如果必须用一定要走本地模型。大规模批量处理cua框架的单次操作成本时间成本和token成本远高于传统脚本。如果任务量很大还是写脚本更划算。6.3 与传统RPA工具的关系很多人会把cua和传统RPA工具放在一起比较。我的看法是它们不是替代关系而是互补关系。传统RPA在稳定性和速度上有绝对优势适合流程固定、界面稳定的场景。cua在灵活性和适应性上有优势适合界面多变、难以用规则描述的场景。实际项目中我倾向于混合使用核心流程用传统RPA保证稳定性边缘情况用cua做兜底。比如一个表单填写任务大部分字段用脚本直接填遇到验证码或动态加载的字段再调用cua框架处理。7. 后续扩展方向与个人实践体会这个原型跑通之后我陆续做了一些扩展尝试。一个是多模态输入的融合——除了屏幕截图还加入了对剪贴板内容、当前窗口标题、系统通知的读取让模型有更多的上下文信息。另一个是动作序列的抽象——把常见的操作组合比如“打开文件并另存为”封装成高层动作减少模型决策的步数。还有一个我觉得很有潜力的方向是从演示中学习。让用户手动操作一遍任务记录下所有的截屏和输入事件然后用这些数据去微调模型让它学会这个特定任务的执行模式。这比每次都用通用提示词要高效得多。说实话cua这个方向目前还处于很早期的阶段。我踩过的坑远比上面写出来的多很多问题到现在也没有完美的解决方案。但它的潜力是实实在在的——当模型的操作精度和速度提升到某个临界点很多现在需要人工重复操作的桌面任务都可以交给它去完成。如果你也在折腾这块我的建议是从最简单的任务开始比如“打开记事本输入一段文字并保存”。把这个流程跑通你会对整个系统的能力和局限有一个直观的感受。然后再逐步增加复杂度每一步都做好日志和截图记录。不要一上来就挑战复杂任务那样很容易被各种问题淹没最后失去信心。另外不要追求100%的自动化率。在实际使用中80%的自动化加上20%的人工兜底往往比追求100%自动化要可靠得多。把cua框架当成一个“助手”而不是“替代品”心态会好很多系统设计也会更务实。