ARTICLE DETAIL

资讯详情

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

AI操作电脑实战:CUA系统从0到1搭建全记录

AI操作电脑实战:CUA系统从0到1搭建全记录 如果你跟我一样这两年一直在折腾各种 AI Agent大概率会撞到同一块玻璃天花板模型能聊天、能写代码、能调 API可真要它“在我的电脑上替我把活干了”——打开网页、登录后台、录入几十条数据、核对信息、再导出一份报表——绝大多数方案当场哑火。这个让 AI 像人一样操作电脑的方向行业里叫 CUAComputer Use Agent计算机操作智能体也是我这几个月一直在做的一个项目。CUA 的目标用一句话说清楚给定一个目标让它自己看屏幕、自己动鼠标、自己敲键盘遇到错误自己能绕过去最后把任务真正收尾。这篇文章不是概念科普而是我把一个可运行的 CUA 从零搭起来的过程记录感知层怎么选型、动作层怎么做得稳、决策层怎么防止模型瞎绕圈以及那些文档永远不会告诉你的高分屏坐标坑和重复点击风暴。想自己动手搭一套的可以直接照着抄只想了解原理的也能从里面摸到整个系统的骨架。1. CUA 到底是什么先和 RPA 把账算清楚在聊技术实现之前我建议先把“CUA 为什么值得搭”这件事聊透。因为很多人一听到“AI 操作电脑”第一反应是“这不就是自动化测试 / RPA 吗”——如果真的只是这样那直接用 UiPath、按键精灵就完了根本不用把大模型搬进来。这里面的本质区别决定了整个系统的设计思路。1.1 核心工作流一次完整任务的闭环CUA 的核心是让模型承担三层职责观察环境、理解任务、驱动动作。它不是一个预设脚本的执行器而是一个具有反馈闭环的决策体。完整的工作流大概是用户提出一个意图CUA 先把它拆成若干子任务然后通过截图或读取界面结构观察当前状态接着决定下一步动作点击、输入、滚动、等待、切换窗口等动作执行完毕后再次观察界面判断这一步是否生效、是否需要修正直到任务完成或触发终止条件。在这个闭环里最容易被忽视的是“观察反馈”这个环节。我早期做原型时把大部分精力都花在让模型生成正确动作上结果发现模型经常在点击后立刻进行下一步判断——而这时候页面还没刷新完它看到的还是旧界面于是整个决策链就建立在错误的状态上。后来我把“动作”和“观察”用状态机强制分开动作执行后必须完成一次新的观察并确认状态变化才能进入下一步决策。1.2 四层架构CUA 系统的骨架抛开概念回到工程一个可用的 CUA 系统可以拆成四层感知层、规划层、执行层、护栏层。感知层回答“当前屏幕上有什么”规划层回答“下一步该做什么”执行层回答“怎么做下去”护栏层回答“什么绝对不能做、什么时候必须停下”。这个分层不是纯粹为了代码整洁而是出于一个非常现实的理由四层的演进速度完全不同。感知层可能因为新增一个 OCR 模型就要替换执行层可能因为支持新平台要重写而规划层的提示词会频繁调整。如果把它们揉在一起任何一层的改动都会牵动全局拆开之后我可以对每一层单独做回归测试和性能评估。RPA 则是另一套哲学。RPA 讲究“录制 回放”核心是基于固定选择器XPath、控件 ID、坐标模板做确定性匹配。只要页面不升级、环境不变化它可以 7x24 小时稳定跑但一旦 UI 改版、弹窗出现、加载变慢脚本就碎成一地。维度RPACUA触发方式固定脚本、定时触发自然语言指令或目标描述界面定位依赖选择器/坐标模板视觉 结构混合理解面对UI改版脚本失效需要人工维护可自适应偏差后可重新规划错误处理内置少量重试规则模型反思 多步纠错适用场景流程稳定、量大、合规要求高长尾、多变、低频率高复杂度开发成本初期低维护成本随页面数量上升初期模型/基建成本高长期泛化性好我并不是说 RPA 会被 CUA 取代。实际项目中两者经常配合使用CUA 负责“找到入口、绕过变化”RPA 负责“进去之后高速重复执行”。比如我的项目里CUA 负责打开后台、跳转到录入页面、识别异常弹窗一旦进入表格区域就交给 RPA 风格的批处理逻辑快速录入。这种混合架构在稳定性和效率之间找到了一个很实用的平衡点。2. 感知层设计给 Agent 造一双稳定的“眼睛”感知层是 CUA 最容易被人低估的部分。模型再聪明如果看不到正确的信息一切都是空中楼阁。我从两条主流路线说起再讲最终采用的混合方案。2.1 路线一可访问性树 / DOM / 视图树所有操作系统都留了一扇“官方后门”用于辅助功能Windows 的 UI Automation、macOS 的 Accessibility API、浏览器里的 DOM 和 Accessibility Tree。通过这扇门程序可以拿到界面上的控件列表、控件类型、名称、位置矩形甚至直接触发控件的动作。这条路线最大的优点是精准你不会把“保存”按钮识别成“另存为”因为拿到的就是结构化的节点信息。但它有两个硬伤。第一很多应用没有做好辅助功能适配Electron 应用有时只能拿到一棵残缺的树某些自绘 UI游戏、设计软件、视频剪辑工具干脆完全不暴露任何节点。第二可访问性树描述的是“程序认为的界面”而不是“用户看到的界面”一旦遇到 Canvas 渲染的网页或者自绘控件树里的信息和屏幕上的实际视觉严重脱节。我在项目里给这条路线定的评价是有就用但绝不能只依赖它。CUA 要应对的真实环境里拿不到可访问性树是常态能拿到是惊喜。2.2 路线二纯视觉 OCR第二条路线是直接截图然后用视觉模型VLM或者 OCR 目标检测来理解画面。这更贴近人类操作电脑的方式我不需要知道按钮的控件 ID只要看到“登录”两个字就知道该点哪里。纯视觉路线的实现路径有两种区分度很大的做法。轻量做法是“OCR 模板匹配”截图后调用 PaddleOCR 或 Tesseract 提取文字块坐标再配合颜色/形状识别定位按钮。这个方案成本低、延迟小但对图标的识别无能为力遇到纯图形按钮比如一个只有齿轮图标的设置入口就失灵了。重做法是直接让多模态大模型看整张截图让它输出“我应该点击的坐标”或者“画面里有哪些可交互元素”。这个方案泛化性极强但成本高、延迟大而且模型偶尔会一本正经地输出一个不存在的坐标。我遇到过模型坚持认为屏幕右上角有一个“提交”按钮实际上那里是一片空白区域——这个现象在深色背景、复杂界面上尤其明显。2.3 路线三混合感知实测最稳最终我的 CUA 采用的是“截图 可访问性树 OCR VLM 兜底”的混合方案。优先级如下先拉可访问性树如果目标控件在里面直接用树节点的坐标。树里找不到就用 OCR 找目标文本找到文本块后取中心点作为点击坐标。如果目标是一个没有文本的图标就把截图裁剪下来交给 VLM 判断“图标是什么、在哪个区域”用返回的区域中心作为坐标。这个优先级设计的逻辑是“成本与可靠性的倒序排列”可访问性树零成本且最准OCR 成本低且适合文本场景VLM 成本高但能兜底。三条路径互相校验的时候还能顺带解决“模型幻觉坐标”的问题——只有 VLM 输出的坐标落在可访问性树或 OCR 给出的可交互区域内才放行动作。我为这个校验逻辑做过一次统计加上之后无效动作率从 22% 降到了 7% 左右效果非常明显。2.4 高分屏与多显示器坐标换算的血泪经验感知层最容易翻车的不是模型而是坐标系统。现代操作系统普遍开着 DPI 缩放Windows 的 125%、150%macOS 的 Retina 2x。这意味着逻辑坐标和物理像素坐标之间有一个缩放系数而且这个系数可以按显示器单独设置。我第一次跑通 Demo 的时候在 1080p 单显示器上一切正常换到一台 4K 笔记本外接 1080p 显示器后CUA 点击的位置全部偏到右上角。排查了半天才发现pyautogui 截图用的是物理像素而可访问性树返回的坐标是逻辑坐标每个显示器的缩放比例还不一样两者直接混用导致坐标漂移。解决这个问题建议做两件事第一在感知层定义一个统一的“逻辑坐标空间”所有来源的坐标全部先换算到这个空间第二换算系数不要写死通过 API 动态读取每个显示器的缩放比例Windows 下用 SetProcessDPIAware 枚举屏幕macOS 下用 NSScreen.backingScaleFactor。这个映射一旦建立后续动作层和执行层就永远只用同一套坐标问题一劳永逸。3. 动作执行层点击、输入、滚动背后的工程细节感知层负责“看”动作层负责“动”。动作层看起来只是把鼠标键盘 API 包了一层但真正做起来细节多到能写满一个小本子。3.1 先定义一套干净的动作空间为了让规划层不用关心操作系统差异我定义了一个最小但够用的动作空间move_mouse(pos)移动鼠标到逻辑坐标。click(pos, buttonleft, n1)在指定位置点击。type_text(text, interval0.02)输入一段文本。scroll(delta, posNone)滚动鼠标滚轮。hotkey(keys)执行快捷键。wait(ms)等待页面加载。finish(result)任务结束把结果传回上层。这个动作空间的粒度很有意思定得太粗模型没法表达复杂操作定得太细模型每步决策都很痛苦。我试过拆分出 drag 和 double_click也试过把 move 和 click 合并成 click最后发现最顺手的还是上面这个粒度。drag 在“文件拖拽”类操作中很必要但我会把它降级为 move press move release 的组合动作由执行层保证一气呵成而不是让模型去组织。3.2 执行器底层不同平台的输入注入跨平台的输入注入是执行层的第一个坑。Windows 上直接调用 SendInput 可以做到比较接近真实硬件的输入效果macOS 上由于辅助功能权限的严格限制需要使用 CGEvent 或 AppleScript并且应用需要获得“辅助功能”授权Linux 上则依赖 XTest 或 uinput。Python 生态里pyautogui 做了很好的封装但它有自己的问题有些平台实现依赖屏幕尺寸的百分比在特定分辨率下会出错并且 pyautogui 的截图和输入都相对偏慢。如果追求极致可控建议直接用平台 API 封装一个轻量执行器Windows 用 ctypes 调 SendInputmacOS 用 PyObjC 调 Quartz CGEvent。这听起来多写不少代码但换来的好处是动作延迟可控、支持真正的多键组合尤其是 Win/Linux 环境下 pyautogui 对某些组合键支持不佳、可以精确控制事件间隔。对 CUA 这种对时序敏感的系统来说这点差距往往就是“看起来像人操作的”和“看起来像机器乱点”的区别。3.3 动作的幂等、节奏与防抖动作层真正难的地方不是“发出一个点击”而是“保证这个点击是可撤回、可重复、可验证的”。先讲幂等。同一个动作用在不同的状态上结果可能完全不同。最典型的例子是“切换开关”理想的 agent 应该先判断开关当前状态再决定是点击状态反转还是不点击。但很多基于视觉的 CUA 只看到“这是一个开关”就直接点击于是任务跑第二遍时状态反了、结果全错。我在规划层加了一条硬规则对于 toggle 类控件必须先读取当前状态动作只表达“期望状态”由执行层判断是否需要真的点击。再讲节奏。操作系统对输入事件是异步处理的鼠标移动后立刻点击系统可能还没完成命中测试点击后立刻输入文本焦点可能还没切换过去。我在执行层维护了一个简单的动作队列和节奏控制器每个动作之间至少保留 50~100ms 的间隔输入类动作还要额外等待焦点确认。这个节奏在本地机器上不重要但在远程桌面、虚拟机里跑的时候延迟会明显放大建议节奏参数做成可配置的不要写死。最后是防抖。我在项目里踩过最严重的一次事故是 CUA 在弹窗出现后连续点击了十几次“确定”按钮导致生成了十几笔重复订单。根因是感知层和动作层之间没有做反馈同步动作发出后下一次观察还没执行上一轮的缓存状态被重复使用。修复方案是在感知层维护一个“屏幕状态指纹”——通过裁剪关键区域计算哈希如果两次观察之间指纹没变就认为动作未生效禁止重复执行同一个动作超过 N 次。3.4 中文输入与输入法切换的坑这个坑可能只有中文场景才特别明显直接调用 SendInput 注入字符时如果当前系统输入法处于中文模式type_text 注入的字符串可能被输入法吃掉或者变成拼音组合。我最终的做法是在输入之前先切换到英文输入法Windows 下切换键盘布局macOS 下切换输入源输入结束后再切回去。同时优先使用“剪贴板粘贴”而不是逐字模拟键盘——把文本放进剪贴板然后发送 CtrlV / CmdV。这个方式对长文本、特殊字符的稳定性远高于逐字输入速度也快得多。唯一的注意点是操作完要主动清空剪贴板避免后续步骤误粘贴到别的地方。4. 决策规划层让 CUA 学会“三思而后行”感知和执行解决了“怎么看”和“怎么做”真正决定 CUA 上限的是规划层面对一个模糊目标它如何拆解、决策、纠错。4.1 任务拆解把“大目标”翻译成“屏幕上的下一步”模型一开始拿到的往往是“帮我把某网站上的订单数据导成 Excel”这种高层目标。直接让模型从截图里决定第一步会非常不稳定。我的做法是先做一个离线任务拆解把高层目标拆成子步骤清单打开网站 → 登录 → 进入订单页 → 设置时间范围 → 点击导出 → 等待下载完成。这一步可以用一个较强的模型预先完成也可以把拆解逻辑沉淀成模板。这个拆解动作的价值有两层第一它把长程任务切成了短程任务每一步的决策空间变小模型更不容易迷路第二它给了规划层一个天然的进度检查点——每完成一个子步骤就把状态标记为“已完成”如果中途失败可以精确知道卡在哪一步。4.2 观察-思考-行动反馈循环的实现CUA 的规划层和感知、执行层之间是一个循环观察状态 → 模型思考下一步动作 → 执行 → 再观察。用代码表达就是def run_agent(task, steps, max_steps15): state observe() # 返回截图描述、UI树摘要、可用控件列表 for step in range(max_steps): action planner.decide(task, state, steps) if action[type] finish: return action[result] result executor.execute(action) new_state observe() if not state_changed(state, new_state): no_progress_counter 1 else: no_progress_counter 0 state new_state if no_progress_counter 3: state rethink(task, steps, state) # 反思修正 return {status: failed, reason: max_steps_exceeded}这里的核心是把“观察”对象设计成什么。我一开始只喂截图模型经常被截图里的无用信息干扰后来改成“缩略截图 结构化控件摘要”的组合缩略截图保留空间信息控件摘要ListView 里列出的按钮、输入框、文本及其坐标给模型明确的候选操作对象。实测这个组合把无效动作率降低了将近一半。4.3 卡死检测与反思纠错模型驱动的 agent 最烦人的行为是“不断重复一个无效动作”明明按钮没生效它还是隔两步就再点一次。我的经验是两层防御。第一层是上文提到的“状态指纹”检测连续 3 次观察不到状态变化就进入反思模式第二层是反思模式本身把最近 5 轮的动作历史和截图变化打包给模型问它“你当前策略是否有效如果无效给出新的计划”。这比简单地限制步数有用得多因为很多任务确实需要大量重复动作简单地限制步数会误杀正常任务。卡死检测的另一个场景是“等待”页面加载、文件下载、后台任务处理都是耗时的模型如果不会等待就会在加载完成前反复点击。我在动作空间里显式加入了 wait 动作并把“系统是否处于忙碌状态”鼠标指针形状、网络请求状态、页面 loading 指示器作为观察项喂给模型。这一步对真实场景至关重要因为测试网站都有 SPA 的异步渲染真实后台的响应时间可以长达几十秒。4.4 模型选型与提示词框架规划层的“大脑”选择我踩过很长的弯路。纯视觉模型可以一次完成“看 想 动”但成本高、延迟高、容易在长任务中失忆纯文本模型虽然便宜但需要感知层先把截图转成文本描述信息损耗大。现阶段我比较推荐“感知弱模型 决策强模型”的分层感知层用专用的小模型OCR、目标检测、可访问性树解析把屏幕内容结构化然后把这些结构化信息以文本 JSON 形式喂给一个较强的文本推理模型去决策。这样既保留了大模型的泛化推理能力又避免了每步都调用昂贵的多模态接口。提示词框架上我的 System Prompt 里固定包含了几块当前任务目标、已拆解的子步骤清单、当前步骤、动作空间说明、输出 JSON 格式要求、安全边界。每次决策的 User Message 则包含当前屏幕状态的文本摘要、最近 5 步动作历史。明确要求模型“如果你认为任务已完成必须给出证据”这个细节能显著减少模型提前宣布完成的情况。5. 实战案例让 CUA 自动完成网页表单录入理论讲完了我挑一个最经典的任务走一遍完整实现在某个后台系统里把用户名单逐条录入到网页表单并提交。这个任务非常适合 CUA因为流程固定但页面有异步加载传统脚本容易崩让模型来做节奏控制则非常轻松。5.1 任务设定与技术选型任务目标把一批“姓名 手机号”从 Excel 中读出自动填入网页表单点击提交然后处理下一条。中间可能遇到页面卡片式弹窗、输入框异步校验、加载过慢等。技术选型上我的组合是截图用 mss比 pyautogui 截屏快很多OCR 用 PaddleOCR控件信息从浏览器 Accessibility Tree 获取决策模型用支持结构化输出的通用文本模型动作执行用自己封装的层。选型理由很简单mss 截屏快、PaddleOCR 中文识别稳、可访问性树作为第一优先定位来源保证准确度文本模型负责兜底决策整套链路延迟可以控制在 2~4 秒一个交互。5.2 代码实现感知、执行、决策三模块感知模块的核心是“目标控件在哪里”class Perception: def __init__(self): self.ocr PaddleOCR(use_angle_clsTrue, langch) def locate_text(self, target: str) - tuple[int, int] | None: img mss.mss().grab(mss.mss().monitors[1]) result self.ocr.ocr(np.array(img)) for line in result: if not line: continue text line[1][0] box line[0] if target in text: cx int((box[0][0] box[2][0]) / 2) cy int((box[0][1] box[2][1]) / 2) return cx, cy return None def get_ac_tree(self, xpath_hint: str): # 通过 Chrome DevTools Protocol 获取可访问性树 # 返回控件矩形和角色信息 ...执行模块负责把动作发出去class Executor: def __init__(self, scale1.0): self.scale scale # 逻辑坐标到物理坐标换算系数 def click(self, pos): x, y int(pos[0] * self.scale), int(pos[1] * self.scale) self._move(x, y) self._click(x, y) def type_text(self, text): pyperclip.copy(text) self.hotkey(ctrl, v) time.sleep(0.05) def hotkey(self, *keys): # 底层调用 SendInput / CGEvent 实现 ...决策循环把两者连起来def run_form_filling(pairs, target_form_url): agent CUA(Perception(), Executor()) agent.goto(target_form_url) for name, phone in pairs: agent.fill_by_text(姓名, name) agent.fill_by_text(电话, phone) if agent.verify_text(提交): agent.click_text(提交) agent.wait_until(lambda: agent.has_text(录入成功), timeout15) return agent.summary()这个例子里最有价值的设计是 fill_by_text 和 wait_until 两个抽象前者会先按“控件名称 → 可访问性树定位 → OCR 文本定位 → 区域临近定位”的顺序找输入框不关心输入框在页面上的具体位置后者会在提交后轮询判断是否出现成功提示如果超时会触发一次截图归档并标记失败点。5.3 运行效果与实际效率最终这套流程跑完 200 条数据录入耗时约 18 分钟无人值守成功率在 95% 以上。剩余 5% 的失败主要是两类一类是网络超时导致页面一直转圈另一类是遇到验证码滑块——这两类我都直接放入人工接管队列不浪费模型步数去硬试。有个特别值得说的细节录入过程中偶尔会出现“手机号格式错误”的提示但页面并不会跳转也不清空表单。早期版本里 CUA 会因为“没看到成功提示”而反复点击提交触发多次重复请求。后来我在 wait_until 里加了“未成功且表单字段被清空”才判定失败的逻辑并在这种情况下自动读取页面提示文本把提示信息反馈给规划模型让它决定是修正数据还是放弃本条。加上这个闭环之后成功率从 87% 直接跳到了 95% 以上。6. 实测踩坑清单那些文档里没有的教训如果这篇文章只留一节我会留这一节。以下每一个坑都让我至少搭进去一个完整的晚上。6.1 坐标漂移DPI 缩放、多显示器与远程桌面我在第 2.4 节已经讲了坐标换算的基本方案这里补充一个更隐蔽的场景远程桌面。通过 RDP 或 VNC 操作远程机器时远程会话的屏幕分辨率、缩放比例可能和物理机完全不一样。CUA 在远程桌面上拿到的截图和控件坐标跟“看起来的界面”很可能存在系统性偏移。我的解决办法是在会话开始时主动读取远程桌面的分辨率和缩放并在执行层动态重建坐标映射表如果发现映射表变化就冻结任务、等待人工确认。千万别在远程任务中依赖写死的坐标。6.2 暗黑模式与主题切换视觉感知的“雪盲症”暗黑模式是视觉型 CUA 的天敌。模型在浅色背景上训练得很充分一旦界面变成深色背景OCR 的识别率会明显下降VLM 对按钮位置的判断也更容易出错。更麻烦的是很多系统会跟随操作系统自动切换明暗主题导致同一套任务的感知结果时好时坏。我的应对方式有两个第一在感知层把截图统一做一次“背景反转预处理”深色背景转浅色减少对模型的干扰第二把“当前主题是否为暗色”作为一个显式状态喂给决策模型提醒它不要依赖颜色判断交互动效。6.3 异步加载与“等待”语义没有 sleep 的自动化是不完整的很多教程代码喜欢用固定的 time.sleep(2) 等页面加载这在 CUA 里是最不可靠的写法。真实网络环境千差万别固定等待要么太短导致误判要么太长拖慢任务。我把等待做成了三态检测等待元素出现、等待元素消失、等待状态稳定连续两次截图指纹一致。每个等待操作都要设置超时上限超时后不是直接放弃而是进入“诊断模式”把最近截图和 UI 树快照存档然后尝试按下 Esc 取消可能存在的弹窗再重试一次。这个小小的纠错逻辑让我绕过了一大批“幽灵弹窗”问题。6.4 重复点击风暴状态未变的动作防重前面已经提到过重复点击事故这里再补充一个数据在我早期版本中因为缺少“状态指纹”防护模型在遇到卡顿时平均会重复同一个动作 4.7 次才切换策略。加上防重机制后这个数字降到 1.2 次。具体做法是感知层维护最近 3 次观察的截图哈希和 UI 树摘要动作执行前先检查“目标区域状态是否已改变”。如果连续 3 次观察状态没变就触发反思而不是继续无脑重复。6.5 安全护栏CUA 必须知道什么不能做最后也是最重要的一点CUA 是在真实操作系统上执行动作的它绝不能变成一匹脱缰的野马。我的护栏层实现了四条规则动作白名单只有定义好的动作空间会被执行、敏感操作确认删除、提交订单、发送消息、格式化等动作从目标中匹配命中则暂停等待确认、步数上限默认 15 步长任务另行评估、紧急停止热键系统级监听 CtrlShiftQ任何时候按下就中止任务并回滚到最近检查点。有人在社区问我“CUA 什么时候才能真正可靠”我的回答比较保守在受控环境、高价值场景里它已经能稳定完成 90% 以上的常规 UI 操作任务但正因为它能真实操作系统我们更需要把护栏设计放在功能之前。如果你也准备在自己的项目里引入 CUA我建议先跑三天的“影子模式”——只让它输出计划和预期动作不真正执行积累足够的决策样本后再逐步放开动作权限。这套路子我从踩坑到跑通大概用了三周希望你能比我更快。
返回列表