ARTICLE DETAIL

资讯详情

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

UFO框架实战:用GPT-Vision+pywinauto实现自然语言操控Windows

UFO框架实战:用GPT-Vision+pywinauto实现自然语言操控Windows 1. 从点鼠标到说句话UFO到底想解决什么问题第一次看到UFO这个名字很多人会以为是某个不明飞行物观测项目但在Windows自动化和AI代理这个圈子里它指的是UI Focused Operator——一个把自然语言指令直接翻译成Windows界面操作的智能代理框架。简单说你对着它说一句帮我把这份Excel里的销售数据按地区汇总然后导出成图表它就能自己打开Excel、定位菜单、点按钮、填公式、生成图表全程不需要你碰鼠标键盘。这件事听起来像是科幻但它背后依赖的技术栈其实并不神秘GPT-Vision负责看屏幕把当前界面截图理解成结构化的元素描述pywinauto负责动手通过Windows的UI Automation接口精确操控控件中间再加一层任务规划与状态跟踪把一句模糊的人类指令拆解成打开应用→定位窗口→点击某个按钮→输入内容→验证结果这样的原子动作序列。我接触Windows自动化大概有七八年时间从最早的按键精灵脚本到AutoHotkey再到pywinauto、Playwright这些现代工具一路踩坑过来。传统方案最大的痛点是脆弱你写死一个坐标或者控件ID软件一升级、分辨率一变化、主题一换脚本立刻报废。UFO这类方案的价值就在于它不再依赖硬编码的定位方式而是让模型看着屏幕实时判断该点哪里容错率一下子高了一个量级。这篇文章适合三类人看一是做RPA机器人流程自动化的工程师想了解AI代理怎么和传统自动化结合二是测试岗同学想用自然语言驱动UI自动化测试三是对AI Agent感兴趣、想动手做一个能操作电脑的助手的开发者。我会把UFO的核心思路、关键技术点、实操步骤、以及我自己踩过的坑都摊开讲尽量让你看完就能上手复现一个最小可用版本。2. 整体设计思路为什么是视觉控件双通道2.1 纯视觉方案和纯控件方案的各自死穴要理解UFO的设计得先搞清楚Windows自动化的两条主流路线。纯视觉路线比如基于坐标点击、图像识别找按钮的优点是通用——不管什么软件只要能截图就能操作。但缺点也很致命分辨率一变坐标就废按钮换个颜色就找不到遇到动态内容比如滚动列表、弹窗基本抓瞎。而且纯靠像素判断模型很容易看走眼把取消点成确定。纯控件路线pywinauto、UI Automation的优点是精确——每个按钮、输入框、菜单项都有唯一的控件树路径和属性点击命中率接近100%。但缺点是覆盖率不全很多现代应用尤其是Electron、Flutter、游戏引擎渲染的界面根本不暴露标准控件树pywinauto去抓只能抓到一坨空白或者一个大的渲染容器里面啥都识别不到。UFO的聪明之处在于两条腿走路优先用控件树UIA拿到结构化的元素列表把每个可交互控件的名称、类型、位置、可用状态都提取出来同时截一张屏幕图让GPT-Vision做视觉理解补充那些控件树抓不到的视觉元素。最后把两路信息融合成一个可操作元素集合模型在这个集合上做决策——是点控件A还是点视觉坐标(x, y)。提示这个双通道融合是UFO区别于早期纯视觉Agent的核心。我在实测中发现对于Office、记事本、资源管理器这类原生应用控件通道能覆盖90%以上的操作对于VS Code、Discord这类Electron应用视觉通道就成了主力。2.2 任务规划层把一句话拆成可执行的动作序列光有看和动手还不够中间得有个大脑把模糊指令拆解。UFO的规划层大致做三件事第一意图解析。把帮我整理桌面文件这种模糊指令结合当前屏幕状态翻译成打开文件资源管理器→选中所有.txt文件→移动到文档文件夹这样的具体步骤。这一步靠的是大语言模型的推理能力配合当前屏幕的控件列表作为上下文。第二动作选择。每一步具体执行时模型要在候选元素里选一个并决定动作类型点击、输入、滚动、拖拽、快捷键。UFO通常会把候选元素编号让模型输出点击元素3这样的结构化指令而不是让它直接输出坐标——这样既精确又可解释。第三状态验证与回退。执行完一个动作后重新截图抓控件树判断任务是否推进了。如果发现点错了比如弹出了意料之外的对话框要能识别并回退。这一步是很多Demo级Agent的短板也是UFO工程化程度比较高的地方。2.3 为什么选pywinauto而不是别的Windows上做控件自动化可选的有pywinauto、pyautogui、WinAppDriver、FlaUI.NET等。UFO选pywinauto我的理解是几个原因纯Python和GPT调用、图像处理在同一套技术栈里不用跨语言通信同时支持Win32和UIA两种后端老应用用Win32新应用用UIA覆盖面广API相对友好Application().connect()、window.child_window()这类写法上手快社区活跃遇到问题能搜到答案。pyautogui虽然也能点坐标但它本质是盲点没有控件信息和UFO的融合思路不搭。WinAppDriver更偏向测试场景需要单独起服务重量级。所以pywinauto算是这个场景下的甜点区选择。3. 核心细节拆解控件树、视觉理解与动作执行3.1 用pywinauto把界面读成结构化数据pywinauto的核心能力是把一个窗口的控件树遍历出来。每个控件节点通常包含这些属性control_type按钮、编辑框、列表项等、name显示文本、automation_id开发者设的ID、rectangle屏幕坐标矩形、is_enabled、is_visible。我一般会写一个递归函数把整棵树拍平成一个列表只保留可交互的控件按钮、输入框、菜单项、列表项、超链接等过滤掉纯容器和装饰性元素。这样得到的列表可能几十到几百项正好适合塞进模型的上下文。from pywinauto import Desktop def dump_controls(window, max_depth8): results [] def walk(ctrl, depth): if depth max_depth: return try: info { type: ctrl.element_info.control_type, name: ctrl.element_info.name, auto_id: ctrl.element_info.automation_id, rect: ctrl.rectangle(), enabled: ctrl.is_enabled(), visible: ctrl.is_visible(), } if info[visible] and info[enabled]: results.append(info) for child in ctrl.children(): walk(child, depth 1) except Exception: pass walk(window, 0) return results这段代码有几个实操要点。max_depth别设太大否则遇到复杂应用比如IDE会遍历出上千个节点既慢又污染上下文。我一般设6到8层。另外try/except是必须的因为有些控件在遍历过程中会失效窗口关闭、动态刷新不加保护直接崩。注意ctrl.rectangle()返回的是屏幕绝对坐标如果你有多显示器或者缩放了DPI坐标会偏。建议在程序开头调用ctypes.windll.shcore.SetProcessDpiAwareness(2)把进程设为DPI感知否则截图和控件坐标会对不上。3.2 GPT-Vision怎么看屏幕视觉这一路UFO的做法是把截图连同控件列表一起发给多模态模型让模型做两件事一是识别截图里有哪些控件列表没覆盖到的元素比如Canvas绘制的按钮二是给出这些元素的近似坐标。这里有个关键技巧给截图打网格或者标注。直接把原图发过去模型对坐标的估计误差可能几十上百像素。我实测下来在截图上叠加一层半透明的坐标网格比如每100像素一条线标上刻度模型输出的坐标精度能提升不少。另一种做法是把控件列表里已有的元素在截图上画框并编号让模型直接引用编号这样它就不用猜坐标了。from PIL import Image, ImageDraw def annotate_screenshot(img_path, controls, out_path): img Image.open(img_path).convert(RGB) draw ImageDraw.Draw(img) for idx, c in enumerate(controls): r c[rect] draw.rectangle([r.left, r.top, r.right, r.bottom], outlinered, width2) draw.text((r.left, r.top), str(idx), fillred) img.save(out_path)把编号标上去之后模型返回点击编号5这种指令你就能直接映射回对应的控件对象精确调用ctrl.click_input()。这比让它返回坐标靠谱得多。3.3 动作执行click_input和type_keys的坑pywinauto执行动作主要靠click_input()和type_keys()。这两个方法底层是模拟真实鼠标键盘事件比直接调UIA的invoke()更像人兼容性也更好——有些应用对UIA的invoke不响应但对真实点击响应正常。但坑也不少。click_input()默认点控件中心如果控件被遮挡或者中心点落在子元素上可能点不中。我一般会加个coords参数微调或者先set_focus()再点。type_keys()更麻烦它对特殊字符的处理依赖当前键盘布局输入中文、emoji、或者{}、、^这些特殊符号时经常出问题。# 输入普通文本 ctrl.type_keys(hello world, with_spacesTrue) # 输入特殊字符要转义 ctrl.type_keys(a{}b, with_spacesTrue) # 输入 ab # 输入中文建议用剪贴板 import pyperclip pyperclip.copy(中文内容) ctrl.type_keys(^v) # CtrlV中文输入我强烈建议走剪贴板type_keys直接打中文在不少应用里会丢字或者乱码。剪贴板方案稳定得多代价是要处理剪贴板被占用的情况。4. 实操过程从零搭一个最小可用的UFO原型4.1 环境准备与依赖安装先把环境搭起来。Python建议3.10以上太老的版本有些库不兼容。pip install pywinauto pillow pyperclip openaipywinauto在Windows上装完就能用不需要额外驱动。如果你要用UIA后端确保系统装了.NET FrameworkWin10/11默认都有。openai库用来调多模态模型你也可以换成任何兼容OpenAI接口的服务。装完之后先跑个冒烟测试确认能连上记事本from pywinauto import Application app Application(backenduia).start(notepad.exe) dlg app.window(title_re.*记事本.*) dlg.type_keys(测试输入, with_spacesTrue)能打出字就说明基础环境没问题。4.2 主循环感知-决策-执行-验证UFO的核心是一个循环我把它简化成四步感知截图 抓控件树融合成当前状态决策把状态和任务目标发给模型拿到下一步动作执行用pywinauto执行动作验证重新感知判断任务是否完成或需要调整。def run_agent(task, max_steps20): history [] for step in range(max_steps): screenshot capture_screen() controls dump_controls(get_active_window()) annotated annotate_screenshot(screenshot, controls, tmp.png) action ask_model(task, annotated, controls, history) if action[type] done: break execute_action(action, controls) history.append(action) time.sleep(0.5) # 等界面稳定time.sleep(0.5)这个等待很关键。界面操作后往往有动画、加载、刷新立刻截图会抓到中间状态导致模型误判。我一般等0.5到1秒复杂操作比如打开大文件要等更久或者轮询检测某个元素出现。4.3 提示词设计让模型输出结构化动作模型能不能稳定输出可执行的动作全看提示词。我的经验是把候选元素列表、动作类型枚举、输出格式约束都写清楚并且给一两个示例。你是一个Windows操作代理。当前屏幕上有以下可交互元素 {controls_json} 用户任务{task} 请输出下一步动作格式为JSON {type: click|type|scroll|hotkey|done, target: 元素编号, value: 输入内容或按键, reason: 简短理由} 只输出JSON不要其他内容。reason字段看起来多余但它对调试帮助极大。出问题时你能看到模型当时在想什么快速定位是感知错了还是推理错了。4.4 一个完整案例自动整理下载文件夹我拿一个真实场景跑一遍——把下载文件夹里的图片按日期归类到子文件夹。任务描述打开文件资源管理器进入下载文件夹把所有.jpg文件移动到图片归档文件夹。执行过程大致是模型先决策点击任务栏文件资源管理器图标感知到窗口打开后决策在地址栏输入下载文件夹路径然后全选jpg文件这里用CtrlA配合筛选或者逐个Ctrl点击最后剪切并粘贴到目标文件夹。实测下来这个任务在20步以内能完成成功率大概七成。失败的情况主要是地址栏输入后没等加载完就截图导致模型看到的是旧界面或者文件太多需要滚动模型没意识到要滚动。这两个问题都能通过加等待和加滚动动作类型缓解。5. 常见问题与排查技巧实录5.1 控件抓不到怎么办最常见的报错是控件树为空或者只有顶层窗口。原因通常是应用用了非标准UI框架。排查步骤换backend试试backendwin32和backenduia都试一遍用微软自带的Accessibility Insights或者Inspect.exe看看这个应用到底暴露了什么如果确实没有控件树就退化成纯视觉方案靠截图坐标点击。5.2 点击没反应或者点错位置先确认坐标对不对。把控件矩形画到截图上肉眼看一下框是不是在按钮上。如果框对了但点击没反应可能是控件被遮挡或者需要先获得焦点。试试ctrl.set_focus()再click_input()或者用click_input(coords(x, y))指定相对坐标。DPI缩放是重灾区。如果你的屏幕是150%缩放而进程没设DPI感知截图是放大的但控件坐标是逻辑坐标两者对不上。开头那句SetProcessDpiAwareness一定要加。5.3 模型决策不稳定同一个界面模型这次点对了下次点错了这种随机性很头疼。几个缓解手段降低温度调API时把temperature设成0或接近0给足上下文把历史动作也发给模型让它知道已经做过什么加约束明确告诉模型如果上一步动作没有改变界面尝试其他元素重试机制同一个动作失败两次就换策略别死磕。5.4 常见问题速查表现象可能原因排查方向控件树为空非标准UI框架换backend用Inspect.exe检查点击无反应控件被遮挡/未聚焦set_focus后重试检查坐标坐标偏移DPI缩放未处理设置进程DPI感知中文输入乱码type_keys编码问题改用剪贴板粘贴模型决策随机温度过高/上下文不足降温度补充历史动作截图是旧界面未等待界面刷新增加sleep或轮询检测5.5 几个我踩过的坑第一个坑是多显示器。截图默认抓主屏但目标窗口可能在副屏导致截图和控件坐标完全对不上。解决办法是抓取目标窗口所在显示器的区域或者干脆把窗口先移到主屏。第二个坑是弹窗拦截。操作过程中突然弹出权限确认、更新提示、广告弹窗模型如果没识别到就会继续按原计划操作结果全乱。我的做法是在每步感知时检测是否有新窗口出现如果有就优先处理弹窗通常是点取消或关闭。第三个坑是无限循环。模型卡在某个状态反复点同一个按钮。加个计数器同一个动作重复超过3次就强制中断并报错避免烧token。6. 这套方案还能怎么扩展UFO这个思路的延展性其实很强。往小了说你可以把它做成一个个人助手帮你在本地软件里批量处理重复操作往大了说它是AI操作电脑这个方向的一个工程化样本。我最近在尝试的几个扩展方向一是接入更多感知通道比如OCR识别截图里的文字补充控件树里name为空的元素二是动作录制与回放把一次成功的操作序列存下来下次遇到类似任务直接复用省去模型推理三是多应用协同比如从网页抓数据、在Excel里处理、再发到邮件客户端这种跨应用流程正是Agent的用武之地。不过要提醒一句这类方案目前还远没到开箱即用的程度。界面千变万化模型的视觉理解也有上限实际落地时一定要有人工兜底和失败重试机制。我个人的经验是把Agent当成一个能处理80%常规情况的实习生剩下20%的边界情况留给人来处理这样整体效率提升最明显也不会因为Agent偶尔抽风造成严重后果。最后分享一个小技巧调试阶段把每一步的截图、控件列表、模型输入输出都存到日志文件里。出问题时回看日志比盯着屏幕猜快十倍。这个习惯帮我省了无数时间。
返回列表