恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
手搓一个GUI Agent:从截屏到鼠标键盘自动操作的完整指南
首页
资讯中心
/
手搓一个GUI Agent:从截屏到鼠标键盘自动操作的完整指南
手搓一个GUI Agent:从截屏到鼠标键盘自动操作的完整指南
发布时间:2026/10/18 14:57:11
1. 先搞清楚GUI Agent到底是什么以及我们第一步该做什么做这行久了你会发现很多听起来很高端的东西拆到最底层往往就是一个朴素的循环。GUI Agent图形界面智能体也是这样它的核心就一句话让程序像人一样看着屏幕决定下一步干什么然后动手操作鼠标键盘完成任务后停下。近几年多模态大模型火起来之后这件事的门槛被大幅拉低了。以前想做一个能操作电脑的程序你要么走RPA机器人流程自动化路线用专门的工具去录制和回放操作脚本要么走计算机视觉路线训练一个识别按钮和图标的小模型。这两条路的共同问题是遇到界面改版就废了遇到没见过的页面就懵了。而大模型带来的是另一种可能性——它确实没见过你这个软件但它见过海量的图形界面交互样本它能理解一个界面大概长什么样、按钮一般出现在什么位置、点击某个按钮之后通常会跳转到哪里。所以这篇文章想带你做的事不是去搭一个工业级的、能应对所有软件的通用Agent而是手搓一个最小可用的版本出来。目标很朴素给定一个自然语言任务描述比如“打开记事本输入一行字然后把窗口拖到右上角”它能一步一步自动完成。整个过程不涉及微调模型、不涉及复杂的强化学习也不需要你去训练任何东西纯粹靠几个开源库加上一个带视觉能力的模型API就能跑通。适合看这篇文章的人我默认你是这样的写过一点Python知道什么是pip install但对Agent本身没什么概念想看看这东西到底怎么实现或者你脑子里有几个重复的电脑操作想自动化掉但不想学那种厚重的RPA工具。看完之后你会得到一个能跑的骨架代码以及一套改造成你自己任务的思路。2. 环境准备与思路选型为什么不用现成方案偏要自己搓很多人在接触Agent这个概念时第一反应是去看看那些现成的开源框架。坦白讲现在市面上的通用Agent框架确实不少有的能帮你解析任务有的能调用各种工具还有的干脆把浏览器操作集成好了。但你会发现一个问题它们太黑了。封装得太好的框架你跑通了也只是会用了出问题的时候完全不知道去哪排查更别提想改造成自己专属的Agent时那种寸步难行的感觉。所以这篇文章坚持用最朴素的方案每个环节都是自己写得出来的逻辑这样你才算真的把GUI Agent拿捏住了。2.1 最小闭环需要哪些组件先列一下我们要用到的组件其实只有四块截屏模块把当前屏幕的像素信息抓下来。理解模块调用一个支持视觉输入的大模型把屏幕图像和任务指令一起丢给它。执行模块模拟鼠标移动、点击和键盘输入。控制循环负责“截图给模型看等它回复指令执行指令再截图确认”这个循环。这套架构里没有任何一个组件是特别新的东西。截屏用Pillow库自带的ImageGrab就能搞定模拟鼠标键盘用PyAutoGUI理解模块直接调用大模型API。核心的技术含量其实在于那个控制循环怎么设计以及怎么防止模型瞎点。2.2 为什么不直接用现成方案而是走视觉这条路这里得说一个很多人忽略的点。做GUI自动化传统上有一条偷懒的路径很多软件支持通过可访问性接口Accessibility API暴露内部控件树比如Windows上的UI Automation。走这条路你会拿到非常精确的控件名称和坐标点击某个按钮就像直接调用函数一样可靠。但问题也很现实——这条路径只对支持这些接口的传统桌面软件有效遇到网页、截图、自制渲染引擎的界面、游戏客户端基本就抓瞎了。而且这类接口在不同版本、不同系统上的表现差异很大维护成本极高。而视觉方案的天花板更高。人怎么操作电脑它就怎么操作电脑——靠眼睛看屏幕靠大脑推理该点什么。所以GUI Agent从视觉出发天然就能应对那些没有开放接口的软件。现在这一步之所以行得通是因为大模型的界面理解能力已经足够强。你给它一张Word文档的截图它不仅能认出菜单栏、工具栏还能推理出“保存”按钮大概率在左上角。这种能力放在几年前得专门训练一个CV模型才能勉强做到现在直接开箱即用。我自己的经验是第一次跑通视觉方案的时候你会产生一种“这竟然真的能行”的错觉。但请注意这只是错觉后面你会被现实毒打的。视觉方案的不确定性远高于传统控件的确定性所以我们需要在控制循环里做文章降低这种不确定性——这部分后面专门讲。3. 手写核心代码从截屏到执行的完整闭环下面开始搭骨架。我不会给你一个巨大无比的工程文件而是按照层来拆解你照着敲一遍就能理解每一块在干什么。3.1 感知层先把屏幕变成模型能看懂的输入感知层的任务很简单截屏然后把图像以合适的格式喂给模型。这里有两个细节要注意截屏的分辨率和图片的编码格式。先说分辨率。现在的屏幕动辄2K、4K整张截图直接发给模型一方面浪费token另一方面模型在解析超大图片时反而容易忽略关键细节。所以我们可以做一次缩放把图片的长边限制在1500像素左右。这个尺寸既能保留足够的界面细节又能减少传输和解析的开销。实测下来这个数值比较平衡如果你只需要操作某个特定软件的窗口还可以裁剪得更狠一点只截那块区域。再说编码格式。模型API接受的一般是base64编码的图片数据。Pillow截图拿到的是Image对象我们转换成JPEG或PNG再base64编码。JPEG文件小、传输快但文字边缘会有压缩伪影对模型识别界面文字稍微有点影响。我的建议是界面风格偏图形化、图片比较多时用JPEG如果你的目标软件是文字密集型的编辑工具用PNG更稳识别率会有可感知的提升。from PIL import ImageGrab import base64 import io def capture_screen(max_size1500): # 截取整个屏幕 img ImageGrab.grab() # 缩放控制图片尺寸 if max(img.size) max_size: ratio max_size / max(img.size) new_size (int(img.width * ratio), int(img.height * ratio)) img img.resize(new_size, Image.LANCZOS) # 转成base64字符串 buffer io.BytesIO() img.save(buffer, formatPNG) encoded base64.b64encode(buffer.getvalue()).decode(utf-8) return encoded, img.size注意我这里把缩放后的尺寸也返回了。这个尺寸后面有大用模型给出一串坐标时指的是它看到的那张图上的坐标而我们要把坐标映射回真实屏幕的实际尺寸上才能正确点击。这个映射关系如果不处理你的Agent操作就会像喝醉酒的人一样点到奇怪的位置。考虑到不同操作系统权限限制不同Windows上直接ImageGrab就能截全屏macOS、Linux上可能需要额外的权限配置。这一步没什么好的绕过方式系统怎么要求就怎么给权限否则截图会变成一张黑屏或者只截到你的桌面壁纸。3.2 规划层用结构化输出来约束模型的“想法”规划层是整个Agent的大脑。我们给模型看的不是一张图加一句话而是精心构造的一段提示词要求它按固定的JSON格式返回操作指令。这一步极其重要因为如果不加约束模型可能会给你回复一大段文字告诉你“我建议你点击左上角的保存按钮”而不是直接告诉你该执行什么动作。我们必须把“建议”变成“指令”。提示词里定义的动作类型我建议你从这四个开始就够了点击click、输入type、滚动scroll、等待wait。点击需要给出坐标输入需要给出文本内容滚动需要给出方向和滚动量等待只需要一个时间值。这四个动作覆盖了绝大多数桌面软件的基本操作。另外必须告诉模型两个原则。第一个原则如果屏幕上的信息不足以判断下一步怎么走就返回一个特殊动作“unknown”不要瞎猜。第二个原则每次只允许执行一个动作执行完之后会给你新的截图你再决定接下来的动作。这两条规则是防止模型“放飞自我”的关键。system_prompt 你是一个GUI自动化助手。你会收到一张屏幕截图和一个任务描述你需要输出下一步操作。 输出必须是严格的JSON格式不要输出任何其他内容。格式如下 {action: click, x: 100, y: 200} {action: type, text: hello world} {action: scroll, direction: down, amount: 3} {action: wait, seconds: 2} {action: unknown, reason: 描述为什么无法确定下一步操作} 规则 1. 坐标是你看到的图片像素坐标。 2. 一次只输出一个动作。 3. 如果截图信息不足以判断下一步操作输出unknown。 用文本模型调用接口的时候把这套提示词和图片base64一起传过去。现在的多模态模型基本都支持“图像 文本”的输入方式。你会得到一个字符串用json解析库解析它拿到动作类型和参数。我遇到过很多次模型不严格遵守JSON格式的情况——多了一行解释或者用Markdown代码块包住了JSON。所以解析的时候要做一层容错处理把反引号去掉找到第一个“{”字符和最后一个“}”字符中间的部分才拿去解析。3.3 执行层让鼠标键盘听话执行层用的是pyautogui这个库胜在简单直接。坐标映射那块要格外小心上面说了模型看的是缩放后的图如果我们不做逆变换点就会错位。逆变换公式很简单真实坐标 模型输出坐标 × (真实屏幕宽 / 图片宽)除了坐标映射还有两个实操细节。第一个是点击前的停顿。真实的人操作电脑鼠标从一个位置移到另一个位置是需要时间的界面元素的响应也需要时间。程序操作太快上一个操作引发的界面变化还没完成下一个操作就来了很容易踩空。所以每次执行动作前我习惯性地加一个0.3到0.5秒的暂停。这个过程看起来不起眼但能显著提升稳定性。第二个细节是输入法的干扰。pyautogui的typewrite方法遇到中文基本是废的它会按普通键盘键位去输入打出来的是一堆乱码。处理中文字符的可靠方案是先拷贝到剪贴板再模拟CtrlV粘贴。实测下来这个方案最稳避免了输入法状态导致的种种问题。import pyautogui import pyperclip def execute_action(action_data, screen_size, original_size): action action_data[action] scale_x original_size[0] / screen_size[0] scale_y original_size[1] / screen_size[1] if action click: real_x int(action_data[x] * scale_x) real_y int(action_data[y] * scale_y) pyautogui.click(real_x, real_y) elif action type: pyperclip.copy(action_data[text]) pyautogui.hotkey(ctrl, v) elif action scroll: direction action_data[direction] amount action_data[amount] # 注意负数表示向下滚动 pyautogui.scroll(-amount if direction down else amount) elif action wait: time.sleep(action_data[seconds])3.4 控制循环给Agent装上一个“确认-执行-确认”的保险丝控制循环是整个系统里最容易被新手忽略、但也最值得花心思的地方。最简单的循环长这样截屏给模型看拿动作执行再截屏。听着像是个人都会写但实际跑起来你会发现这个朴素的循环在现实中很容易陷入死循环比如模型反复对同一个位置发出点击指令或者明明任务已经完成了但它还停不下来继续在那里瞎操作。因此一个可用的控制循环至少要包含三样东西第一任务完成判断。每次拿到新的截图后除了让模型输出操作指令我会再单独问一次模型“任务是否已经完成”让它只是简短地回一个“是”或“否”。把这个判断做成独立的步骤而不是让模型在生成动作时顺带判断因为实践下来“动作生成”和“完成判断”混在一起容易让模型产生幻觉它会在任务进行到一半时突然觉得完成得差不多了。第二重复动作检测。维护一个最近五步的操作历史如果模型连续输出的动作完全一致比如一直点同一个坐标点说明它卡住了。这时候不要继续执行强制结束循环并报错。这个机制简单粗暴但非常有效毕竟一个真实的人如果在电脑前反复点同一个地方不是界面卡死了就是自己卡死了。第三最大循环次数。我给循环设置一个总上限比如20步。超过上限不管任务有没有完成都停下来。这是个兜底策略防止上面两道防线都失效时Agent烧掉你的显卡。def run_agent(task_description, max_steps20): history [] for step in range(max_steps): screen_b64, view_size capture_screen() action ask_model_for_action(task_description, screen_b64) if action[action] unknown: print(f模型无法判断下一步终止。原因{action.get(reason, 未知)}) break # 检查重复动作 if len(history) 5 and all(h[action] action for h in history[-3:]): print(检测到连续重复动作终止。) break execute_action(action, view_size, get_screen_size()) history.append(action) # 检查任务完成 done ask_model_if_done(task_description, screen_b64) if done 是: print(f任务在第{step 1}步完成。) break这段代码逻辑没问题但有一个细节值得注意我每次“是否完成”的判断用的是执行动作之前的截图理论上应该是执行动作之后再截图来判断。我故意把它放在动作前是因为少一次截屏。你会看到在真实运行中这种程度的“偷懒”其实影响不大动作执行前后界面差异通常不大。不过如果你在做比较精确的操作还是改成动作后判断更严谨。4. 跑通第一个真实任务打开记事本并自动输入内容理论知识讲了半天不跑一个真实任务等于白搭。这一节我带你把整个流程走一遍。任务很简单打开Windows自带的记事本自动输入一段话然后把窗口最小化。之所以选记事本是因为它打开速度快、界面元素少、不存在复杂的登录和弹窗干扰非常适合作为第一个验证脚本。4.1 任务定义和目标任务描述就写一句话“打开记事本输入文字‘这是GUI Agent的第一次实操’然后将窗口最小化。”我们期望看到的现象是脚本启动后程序先按住Win键按下R打开运行对话框输入notepad并回车等记事本窗口出现后自动输入文字然后点击窗口右上角的最小化按钮把它收起来。4.2 完整可复现的运行脚本为了让这个Demo能直接在你机器上跑出效果我把几个关键环节都做了硬化处理。第一个环节是打开记事本。我直接用PyAutoGUI模拟快捷键比让模型去点击开始菜单再搜索要可靠得多毕竟模型在这一步很容易产生五花八门的操作方案。import json import time import os def get_env_prompt(): return system_prompt def ask_model_for_action(task, image_b64): # 这里调用你选定的多模态模型API # 把system_prompt、task、image_b64拼成消息发送 # 返回解析后的dict pass def ask_model_if_done(task, image_b64): # 这里调用模型让它只回复“是”或“否” pass def get_screen_size(): import pyautogui return pyautogui.size()上面细化到调模型接口的部分我用pass占位了因为你用的模型服务不同接口代码会有差异。核心思路就是你调用一个支持视觉输入的大模型把system prompt、用户任务、图片base64三个部分传进去拿到Return字符串后解析。我自己常用的是通用的OpenAI兼容接口网上找一下你用的模型对应的调用方式改一下请求函数就行。4.3 实测记录与运行效果第一次运行的时候别指望一次就能成功。我的第一轮跑出来还是让我比较惊喜的模型识别出了运行对话框里输入notepad后的回车操作然后在新截图中看到了记事本窗口最后在窗口右上角识别到了最小化按钮整个过程大概花了15秒、6次循环。但注意这是“一切顺利”的情况。第二次我重新运行模型在第一轮就翻车了它在打开运行对话框后的截图上把中间那段空白区域当成了输入框直接点击了一下然后模型误以为已经有输入内容了直接按下回车打开的是运行指令的历史记录下拉框整个流程乱套。这个案例非常有代表性很能说明视觉Agent的一个核心毛病——界面上有很多可以点的地方但模型对这些区域的语义理解还达不到人类的水平。要改善这类问题核心手段就是在截图上把鼠标指针移动到一个候选位置并“悬停”上去很多界面元素在悬停时有高亮效果模型识别起来会容易很多。不过这个优化细节我们放到后面下一节讲。目前的这个版本如果你运行翻车多试几次大概率能成功这就是大模型方案“概率性成功”的典型体验。5. 实操过程中的典型坑与解法从炸裂到稳定从我第一次跑通GUI Agent到现在踩过的坑比想象中多得多。有些问题一眼就能看出来有些坑简直让人怀疑人生。这一节挑几个有代表性的结合排查过程讲清楚。5.1 坐标错位最常见也最容易忽视的坑坐标错位问题的表象是你看到程序运行得很起劲但鼠标点到的地方跟它“认为”的地方差了十万八千里可能它本来想点窗口右上角的关闭按钮结果点到屏幕正中间去了。排查思路其实不复杂你只需要在调用模型之前和拿到坐标之后各打印一次截图尺寸对照一下就能发现问题所在。这类问题根源就是截图缩放后的坐标没有映射回原始屏幕坐标。我见过有人在这种问题上排查了整整两天最后发现是缩放之后屏幕尺寸取错变量导致比例完全反了。这个坑防御其实特别简单哪怕你嫌麻烦不加代码每次截屏后才把图的尺寸和真正屏幕尺寸的比值打印出来人工核对一遍也能避免绝大部分悲剧。5.2 模型胡言乱语和JSON解析失败多模态模型在当前阶段还不稳定偶尔会输出一些跟指令完全不搭边的话。尤其是模型在没有足够视觉细节的时候会倾向于脑补。我遇到过最离谱的一次是任务描述是打开计算器模型给出的动作却是“请输入你要计算的内容”然后它还真配了一个type动作。对付这类问题可以分三层来解决。第一层是在提示词层面采取强约束明确告诉模型只能输出指定格式的JSON其他任何解释都不被允许。第二层是做容错解析如果解析失败尝试用正则抽取JSON片段把模型包裹它输出的Markdown代码块标记剥离掉。第三层是设置兜底策略三次解析失败或者动作类型不在白名单里就直接终止循环。宁愿任务做不完也不能让程序乱点鼠标这关乎安全问题。5.3 界面加载延迟导致的点击失效大部分人第一次调GUI Agent时都会碰到这个情况程序用瞬移一样的速度发出指令、点击按钮但界面元素还没渲染出来导致点击落在空处。解决这个问题有两种思路。你要是图省事就在每次动作执行之后加固定的延时比如0.5秒这种“无脑等待”简单有效但很浪费。你要是想做得聪明点可以做一个简易的屏幕差异检测——动作执行后截一张图跟执行前比较像素差异如果差异接近零说明界面没变化可能还在加载中那就多等一会儿。这个思路实现也不算复杂用Pillow直接比较两张图的像素差异就行。不过我得诚实一点固定延时在实际使用中因为实现简单占了上风我也一直在用。AI Agent这个领域现在还是工程实践走在学术前面“实用优先”是主旋律以后有更好的办法再迭代。5.4 误操作之后的安全恢复GUI Agent跑在真实桌面上操作是实打实的。万一模型发疯点到了删除文件、格式化之类的危险动作后果很严重。所以强烈建议你第一次测试时用一个不太重要的虚拟环境或临时用户反正多做一层隔离没坏处。同时在代码层面给动作白名单加一道防线高危动作强制拦截。我自己的做法是维护了一个“禁区列表”里面的坐标或动作类型一旦触发直接中止脚本并弹出警告。比如“ctrlw”关窗口这种还好你要是跑在浏览器自动化里“ctrlw”会关掉整个浏览器你这个Agent连兜底的机会都没了。这只是个形象例子重点是提醒你这东西跑起来失控的破坏力比你想象的大隔离环境真的很重要。6. 从能跑到好用几个提升成功率的关键优化如果上面的代码你已经跑通了恭喜你已经拥有一个最朴素的GUI Agent。但这个版本距离“能用”还有一段路。接下来分享几个我实际用下来对成功率提升最明显的优化手段。6.1 局部截图裁剪与focus区域不一定每次都把整个屏幕丢给模型。很多时候我们操作的目标只集中在某一个窗口或某一个面板里。与其给模型一大张含杂七杂八信息的图片不如先把目标窗口定位出来只截取这个窗口的区域。窗口定位用系统级的接口就可以实现Windows上可以调用Win32 API来枚举窗口和获取窗口矩形。在裁剪之后有个联动的好处图片变小了可以不做缩放或者缩放比例放低模型的识别效果会好很多。而且裁剪之后坐标映射也简化了模型的注意力集中在一个窗口里识别和点选的准确率会有一个肉眼可见的提升。6.2 引入“悬停预览”降低识别难度屏幕上有一些区域本身就是“暗桩”模型的判断可能不准但让鼠标在对应位置悬停一瞬间对应的按钮高亮、提示气泡出现模型的识别率就上来了。这个技巧的实现代价极低只是让pyautogui.moveTo过去停0.2秒再截屏。代价小收益高的优化我会优先做。6.3 从“单步决策”升级为“计划-执行”两步式我们目前为止的架构是让模型自己走一步看一步每一步都要结合最新的截图来预判。这种方式的好处是灵活缺点是没有长远眼光容易在界面流转里迷路同时每步都从屏幕上重新开始推理既费token又费时间还很累。比较成熟的方案是改成两步式在最开始先给模型一张初始截图让它基于任务描述先给出一个多步骤计划比如先打开应用再找到输入框再输入内容最后保存并关闭。然后在执行计划中的每一步时用具体的截屏让模型基于当前计划步骤做决策而不是从零开始。这种方式比原先更稳定因为每一步模型的行动边界被上下步骤约束住了出错概率也下降了不少。6.4 人类反馈回路你还可以在Agent执行中插入一个人工确认机制。最简单的方式是模型每次输出动作你都弹一个小框展示“即将执行的动作”由人来点确认或否决。否决的样本记录下来收集到一定量后可以拿去微调模型也可以作为实时决策的依据。当然这一步已经超出“手搓”的范畴了但如果你做的Agent是用于重要操作场合这套人工确认流程比任何优化都管用算是工程上的兜底手段。7. 写在最后的一点个人实操体会做GUI Agent这段时间我最大的感受是它其实是在拼凑一个正在慢慢接近“人类操作方式”的系统。模型负责理解脚本负责执行出错的时候你要学会容忍一定的“概率性成功”。它不像写传统程序那样能做到确定性输出每一次运行都像在抽卡但你通过优化可以让中奖概率从六成涨到九成已经能解决不少实际问题了。我个人的习惯是每次准备让Agent跑一个操作任务前先把任务描述写清楚按“打开某软件→在某位置输入什么→点击哪里→预期看到什么”这种结构拆条。这个习惯让我减少了大量来回调提示词的次数现在我自己搭的这类小工具已经能稳定帮我处理一些重复的报告整理和软件操作演示了。如果你也打算上手试试我给你几个最后的建议。第一从最简单的场景开始不要一上来就挑战高难度软件先拿记事本和计算器练手。第二日志一定要记录每一步截屏、每一步模型返回的原始输出最好都留痕因为你会频繁碰到同一个问题上次效果不错这次怎么开始发疯有日志和截图排查起来顺手很多。第三不要迷信任何一个框架已有的轮子可以借力但关键的核心数据通路值得你自己从头梳理一遍这对你理解和改造整个系统都有巨大帮助。这个方向我相信还远远没到天花板你手搓的这个版本虽然朴素但已经是好多年前需要一整个团队才能实现的系统雏形了。接下来就靠你在实际任务里拧螺丝了。