做GUI自动化的朋友这几年应该都有同感底层的活越来越难干。以前写个脚本录个坐标跑个点击能应付一阵子现在应用界面动辄几百个控件布局动态变化主题深浅切换分辨率忽高忽低再碰上要操作的系统压根不开放接口传统那套脚本录制和固定流程的方法论基本走到头了。我一直在琢磨另一条路如果把工具本身该有的能力比如识别对话框、填表单、翻下一页、拖文件像人的技能一样拆开交给Agent去编排组合效果会怎样。这就是CUA-Skill框架的由来。CUAComputer Use Agent简单说就是让AI直接操作电脑图形界面像人一样看屏幕、点按钮、打字。而Skill就是把这种操作能力沉淀成可复用、可组合的基本动作单元。这篇是CUA-Skill框架的完整实战复盘从设计思路到落地代码再到我踩过的坑一次性写清楚。适合做RPA、做自动化测试、研究GUI Agent的开发者参考也能给准备入行的朋友一个清晰的技术路线。1. 为什么GUI自动化需要“技能”这件事1.1 传统GUI自动化的三个死穴老玩家都知道传统GUI自动化大致分两派一是录制回放记录鼠标键盘操作序列然后回放二是基于控件树编程通过UI Automation这类接口拿控件属性然后写死流程。这两派都有一个共同的底层假设界面是稳定的。问题是现在的软件界面从来不按这个假设来。我实测过一个数据录入系统同一台机器隔一天打开一个下拉框的位置因为顶部提示条的出现整体下移了40像素录好的坐标脚本全部失效基于控件属性的脚本也会因为控件标题从“确认”变成“确定”直接崩溃。这是第一个死穴界面一变脚本就要重写。第二个死穴是跨应用能力。传统方案想打通两个应用通常靠剪贴板、窗口切换、键盘模拟脆得像纸糊的。一旦目标窗口失去焦点或者中间弹出一个系统级弹窗整条流程就断了。第三个死穴是维护成本。功能逻辑越复杂脚本状态就越多分支、重试、异常处理、超时判断全都要人肉维护。到我实际维护一个中等规模的流程时光处理环境差异和偶发弹窗的代码就占了总代码量一半还多。1.2 CUA视角让Agent像人一样看屏幕、按步骤操作CUA派的核心思路是完全换一个角度不再假设界面不变而是让Agent实时观察界面理解当前状态再决定下一步做什么。人是怎么操作软件的人看到弹窗知道它提示什么然后点击“确定”。这个过程中人依赖的是语义理解而不是某个坐标值。CUA-Skill框架就是把这种语义理解能力正式引入自动化流程。具体来说Agent的每一步操作都包含四个环节感知当前界面状态截图或读取控件树、理解状态含义这是什么窗口、有哪些可操作元素、决策下一步动作符合当前技能定义且状态允许、执行并验证模拟输入然后确认预期结果出现。这套流程听起来不复杂难点在于感知和理解环节如果每次都从头做成本和延迟都太高Agent就会变得又慢又笨。这就需要把操作过程拆成技能。1.3 技能化到底解决什么问题把“点击保存按钮”封装成一个技能而不是一段写死的脚本区别在哪区别在于技能带上了三样东西前置条件、执行策略、后置校验。比如“保存当前文档”这个技能前置条件是“存在一个可编辑文档且修改未保存”执行策略是“定位保存按钮并点击如果按钮不可见则尝试快捷键或菜单项”后置校验是“确认保存成功提示出现且文档标题栏的未保存标记消失”。技能的本质是把不确定性封装起来。传统脚本面对界面变化会直接崩掉技能则是根据界面实时状态动态调整策略换来的是稳定性和可复用的效率。一个技能可以在不同的应用、不同的流程里反复调用就像人的能力一样可以迁移。2. CUA-Skill框架的核心设计2.1 一个Skill由哪几部分组成我设计的Skill定义包含五个核心字段元信息、输入参数、触发条件、执行体、验证规则。下面是一份实际在用的Skill定义示例用JSON Schema描述方便框架做校验和检索{ skill_name: app.save_document, skill_version: 1.2.0, description: 保存当前活动文档支持快捷键、菜单和按钮三种路径, inputs: [ { name: save_strategy, type: enum, values: [auto, keyboard, menu, button], default: auto } ], trigger: { element: window.active, conditions: [ {if: content_modified, then: proceed}, {else: skip_and_report_success} ] }, actions: [ { step: 1, method: perceive, target: get_active_window_screenshot, then: [detect_save_button, detect_menu_bar, detect_modified_flag] }, { step: 2, method: dispatch, strategy_from_input: save_strategy, fallback_order: [keyboard, menu, button] }, { step: 3, method: verify, check: [save_success_toast, modified_flag_cleared], timeout_ms: 5000 } ], verification: { pass: modified_flag false OR success_toast.visible true, fail: timeout OR error_dialog.visible true }, recovery: [ {on: timeout, try: press_escape, then: retry_actions}, {on: error_dialog, try: read_message, then: log_and_abort} ] }触发条件说明这个技能在什么状态下生效执行体描述具体动作序列验证规则判断操作是否真的成功。这个设计里最重要的是最后那个recovery字段它决定了技能在遇到异常时是自动恢复还是安全退出。技能必须有明确的自救能力而不是像传统脚本一样一旦出错就停在原地这种设计在实践中能解决大部分偶发弹窗问题。2.2 技能注册与编排从意图到动作序列单看一个技能都是简单的复杂的是把这些技能串起来解决一个完整任务。我在这套框架里做了一个技能注册表所有技能统一登记注册表会维护技能之间的依赖关系我不允许技能之间存在隐性依赖一切都得显式声明。整体流程是用LLM作为编排核心。Agent先接收用户意图拆解为一个任务图然后逐个匹配技能。比如“把这份报表导出成PDF然后通过邮件发送”这个任务会被拆成打开报表应用、定位报表、导出PDF、打开邮箱客户端、新建邮件、添加附件、输入收件人、发送。每个节点都能在注册表里找到对应的技能节点之间的数据传递也靠技能的输出定义来衔接。这套编排设计的好处是新任务不需要写新代码只需要重新组合已有技能。我维护这个项目一段时间后技能库从最初的十几个增长到四十多个但新任务的平均开发时间反而越来越短因为绝大多数流程都可以复用需要新增的只是一些特定于业务场景的细节技能。2.3 界面感知的三种核心技术路线技能执行第一步是“感知界面”这一步的技术选型直接决定整个框架的上限。我实测下来有三条路线各有取舍路线一系统辅助功能接口。这是最精准的路线读的是应用暴露出来的控件树能拿到控件类型、名称、状态、位置等结构化数据。它的致命弱点是很多应用的自定义控件没法通过标准接口读取尤其是那些用Canvas自绘界面的应用控件树往往是一片空白。路线二视觉识别与OCR。截图之后用目标检测模型识别按钮位置用OCR读取文本内容。这条路线的优势是和应用具体实现无关不管什么界面框架都能处理缺点是精确度受画质影响文字识别偶尔会出错并且对视觉模型的推理资源要求较高。路线三混合感知。这是CUA-Skill框架实际采用的方案。先尝试读控件树拿到可靠的结构化信息读不到时再降级到视觉识别。两条路都走不通的时候技能会主动报告“当前界面状态未知”而不是盲目尝试操作。2.4 为什么选择语义化定位而不是纯坐标传统GUI自动化对坐标的依赖太重这是我觉得最需要摆脱的一件事。坐标天然脆弱分辨率一变、窗口一移、分栏一拖坐标就全错了。CUA-Skill框架里定位全部基于语义描述比如“当前窗口右上角的关闭按钮”“列表第一行第三个单元格”。坐标这个维度只在动作执行的最底层才出现它由“感知模块”实时计算出来整个Skill定义里不存任何硬编码坐标。我实际验证过把窗口从一个显示器拖到另一个显示器或者分辨率从1080P切换到4K技能依然能正常执行因为动作层是在感知之后即时生成坐标而不是从历史数据里读坐标。3. 实操从零构建一个可用的GUI操作技能3.1 环境准备与权限配置动手之前先说环境这套框架主要用Python搭建。感知层用到了系统辅助功能APIWindows平台通过pywinauto或UIAutomation库macOS通过pyobjc访问AXUIElement接口视觉识别层用了一套轻量级目标检测模型做按钮定位OCR用通用文本识别引擎。最麻烦的是权限配置。macOS上要开启终端或主程序所在应用的“辅助功能”权限否则任何AX调用都拿不到数据Windows上虽然权限限制相对宽松但以管理员身份运行的系统和普通用户态运行的系统UIA的权限边界也不一致目标应用以管理员运行时非管理员态的Agent去读取UIA数据会直接失败。注意权限问题是最容易让人误判为代码Bug的地方。如果你发现某个技能在自己的机器上跑得正常换一台机器却无法感知任何控件优先检查辅助功能权限有没有被系统重置别一上来就查代码逻辑。3.2 Skill定义的完整示例我拿一个真实场景来演示自动翻页读取网页内容。假设在一个不支持键盘操作的自定义浏览器内核中翻页我们需要定义技能webpage.next_pageskill_name: webpage.next_page version: 1.0.0 description: 翻到下一页并等待内容加载完成 inputs: - name: max_retry type: int default: 3 trigger: element: window.active conditions: - if: next_button.visible true OR scroll_container.visible true then: proceed - else: report_no_next_page actions: - step: 1 method: perceive target: page_container wait_for: content_loaded timeout_ms: 8000 - step: 2 method: locate strategy: hybrid candidates: - //button[contains(aria-label, 下一页)] - //*[data-testidnext-page] - visual_find: bottom_right_region - step: 3 method: click element: located_element click_mode: human_like post_wait: 500 - step: 4 method: verify check: page_content_changed OR scroll_position_increased timeout_ms: 5000 - step: 5 method: extract target: new_page_content save_as: content_snapshot recovery: - on: element_not_found try: visual_find_retry max_retry: 3 - on: content_not_changed try: scroll_container_to_bottom then: re_verify这个示例里最值得关注的是candidates字段它体现了语义化定位的精髓。定位按钮时优先按可访问性标签找不行就按DOM测试标识找再不行才降级到视觉识别。三条候选路径是串行尝试的每一条失败后都会记录失败原因最终即使技能无法完成目标也能留下“尝试了哪些路径、在哪个环节失败”的排查日志而不是一个笼统的“出错”。3.3 技能调用的核心逻辑光有Skill定义还不算跑通的框架。Agent侧需要一套执行引擎来驱动整个流程。核心循环可以抽象为下面的逻辑def execute_skill(skill_definition, context): # 阶段一触发判断 if not check_trigger(skill_definition[trigger], context): return SkillResult(skippedTrue, reasontrigger_not_met) # 阶段二步骤执行 for step in skill_definition[actions]: for attempt in range(1, max_retry 1): try: step_result run_step(step, context) if step_result.success: break recovery get_recovery_for(step, skill_definition) if recovery: apply_recovery(recovery, context) continue return SkillResult(failedTrue, stepstep, errorstep_result.error) except PermissionError: return SkillResult(failedTrue, errorpermission_denied) # 阶段三最终校验 if verify_skill(skill_definition[verification], context): return SkillResult(successTrue) else: return SkillResult(failedTrue, errorverification_failed)执行引擎除了顺序执行步骤之外还要维护一个全局的界面状态缓存。这个缓存很关键它保存的是上一次感知到的窗口信息、控件位置和内容摘要并行执行的多个技能会共享这份缓存。我实测过有了缓存一个流程里的连续多个技能感知次数能减少约40%每次感知都涉及截图或控件树读取省下来的是立竿见影的速度提升。还要补充一个调度逻辑技能之间是顺序还是并发由编排层根据任务图决定。并发执行时必须隔离各自的界面状态上下文一旦两个技能同时去操作同一个窗口轻则操作互相覆盖重则一个技能等另一个技能的窗口出现直接死锁。所以我在框架里做了窗口锁窗口级互斥宁可牺牲一点并发度也不能让界面状态错乱。3.4 正确率与效率的平衡技巧技能设计里有一个很现实的矛盾验证步骤越严格越不容易误判成功但耗时也越长。我在实际项目中总结了一套分级策略。轻量验证只检查操作是否执行成功比如按钮点击后焦点是否变化、是否出现预期的小组件状态变化耗时可以压到1秒以内。标准验证是在轻量验证基础上再检查关键结果比如保存后检查未保存标记是否消失耗时2到3秒。完整验证则是做内容层的比对比如导出的文件和源数据是否一致耗时可能长达10秒只在关键节点使用。技能定义里配置成可调的Agent在编排时根据任务的复杂程度自动选择合适的验证级别这个折中真的很重要一直跑完整验证整个流程会慢到让人想放弃。4. 常见问题与排查实录4.1 界面轻微变化导致定位失败我实际跑的时间久了之后最常见的失败场景就是目标元素的属性值发生轻微偏移。比如一个按钮的标题从“查询”变成了“查 询”中间多了个空格基于精确文本匹配的选择器就会失效。解决办法是给选择器加上模糊匹配机制。文本匹配改用空格归一化后再比对控件类型匹配允许泛化比如Button和CustomButton认为是同一类。视觉识别兜底策略在这时也很有用控件树里的属性对不上就切到视觉识别按形状和相对位置找按钮。加了这些机制之后技能面对界面微调的容错能力明显提升。4.2 动态加载与异步渲染的等待问题动态内容是最容易把自己坑了的场景。传统脚本喜欢固定等待几秒我见过有人因为网速波动把整个流程的等待时间拉到20秒效率极低。CUA-Skill框架里采用的方案是轮询式状态检测循环检查某个目标状态是否出现而不是固定等待。def wait_for(condition_fn, timeout_ms5000, interval_ms200): deadline time.time() timeout_ms / 1000 last_state None while time.time() deadline: state condition_fn() if state.is_achieved: return state last_state state time.sleep(interval_ms / 1000) raise TimeoutError(fcondition not met. last state: {last_state})这套轮询逻辑的核心是先判断目标状态是否已经满足如果已满足就直接继续不再傻等能省下不少等待时间。对动态渲染的内容我都是先等主框架元素出现再等内部数据元素出现分阶段等待避免一上来就等一个靠后的元素导致前端一直不渲染就超时。4.3 权限与系统级拦截macOS升级某个大版本后辅助功能权限经常被重置这是系统级行为不是代码能决定的。遇到这种情况没有捷径只能加一个启动自检模块启动时做一个最小权限探活测试比如尝试读取当前前台的窗口标题拿不到就直接提示权限异常并给出打开系统设置的具体路径而不是等到流程跑到一半才报一堆莫名其妙的错。Windows上还会遇到另一种权限场景目标应用高权限运行Agent低权限访问。这种情况下UIA的查询接口经常返回空数据。我的建议是尽量让Agent和目标应用保持同一权限级别如果实在做不到就主动降级走视觉识别路线别在控件树数据上死磕。4.4 技能组合执行时的状态依赖技能之间的状态传递是另一个高频出问题的点。比如“导出Excel”这个技能结束后“发送邮件”这个技能需要读取到导出文件的路径。如果这两个技能之间没有显式约定运行时会经常出现文件还没落盘就触发邮件附件的错误。解决这个问题的方案是技能的输出协议。每个技能的输出参数必须明确声明并且写清楚有效条件的定义。导出技能的输出文档路径必带一个file_ready标记这个标记由“文件大小稳定且文件可打开”来确定。下游技能接收到这个参数后必须先检查标记再继续只要输出协议清晰技能之间的组合就能做到高可靠。4.5 性能与资源占用控制视觉识别模型跑在CPU上时单次识别的延迟可能在几百毫秒到几秒不等整个流程里如果有大量视觉识别步骤性能会很难看。我的经验是能不截图就不截图优先走控件树读取必须走视觉识别时缩小检测区域。不要对全屏做识别先根据窗口位置估算一个包含目标元素的小区域再在小区域内检测识别耗时会大幅下降实测能从一两秒降到两三百毫秒。缓存也值得一提。目标应用短时间内界面不会变化缓存上一次的控件树数据和截图结果直接命中缓存就能跳过整个感知环节。5. 经验心得与可扩展的方向这套框架我前后迭代了几个月最大的体会是不要把GUI自动化当脚本写要当系统来设计。脚本是给特定界面量身定做的系统是为一类界面场景提供通用能力。Skill就是中间那层“通用能力”的载体它把感知、决策、执行、验证四个环节固化成模板让Agent能站在语义层面思考而不用陷在坐标和控件ID里。我还在摸索的方向有三个。第一个是技能之间的动态组合目前编排主要靠LLM根据任务图选择技能接下来想尝试让Agent在技能执行中途根据实际界面反馈临时切换其他技能这更像人的操作方式。第二个是跨设备联动同一个技能定义能否直接从桌面端迁移到移动端比如“点击登录按钮”这个技能在PC上靠鼠标坐标在手机上靠触摸坐标语义是一样的。第三个是把技能定义本身用自然语言生成我做过一个实验描述一个操作流程让LLM生成对应的Skill定义正确率大概在七成左右继续优化后这个方向应该能大幅降低技能开发成本。最后分享一个小技巧给每个技能加上一个“成功率自评”字段每次执行完由验证模块自动评估定期统计那些成功率低于阈值的技能重点去排查它们的定义或者感知策略。这个做法能帮你持续发现框架的薄弱环节很多人忽略了这个数据驱动的迭代方式实际用起来效果相当好。GUI自动化这个领域正在从“跑一遍能用就行”走向“面对未知界面也能靠谱干活”。CUA-Skill框架的探索还远谈不上终点但这条技术路线值得更多实践者加入进来。如果你也在做类似的方向欢迎把你在技能拆解和界面感知上的经验沉淀下来这条路一本书都写不完。
阅读完成 · 觉得有帮助?