ARTICLE DETAIL

资讯详情

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

从零构建AI编码代理:GUI操控、MCP接入与单文件打包实践

从零构建AI编码代理:GUI操控、MCP接入与单文件打包实践 前阵子我把自己的自动化脚本整体重写了一遍做了一个叫得上“AI编码代理”的东西。它可以接收一句任务描述然后自己去看屏幕、操作GUI软件也能通过MCP标准协议调用外部工具最后所有代码和依赖被压进一个可执行文件放到哪台电脑上都能双击运行。这篇文章把这个项目从设计到落地的完整过程写下来包括循环框架、GUI操控、MCP接入、单文件打包还有一堆跑出来之后才发现的坑。如果你也在折腾AI Agent、桌面自动化或者想给测试工作找点提效手段这篇应该能用得上。1. 为什么我会想做一个“会动手”的AI编码代理1.1 AI编码代理到底解决什么问题现在的AI模型聊天、写代码确实很厉害但大多数时候它只是停在一个对话框里等我把内容贴过去。真正干活的时候我还是要在终端、IDE、浏览器和一堆桌面工具之间来回切换手动安装依赖、点按钮、改配置。换句话说模型负责“想”我负责“跑腿”这显然不合理。所谓AI编码代理做的事情就是连接这两段让模型不仅能“想”还能自己操作电脑去“做”。它会自己打开终端执行命令会在没有API的软件界面上点击输入也会通过MCP调用外部工具拿数据。我把它理解成给大模型配了一双眼睛和两只手。眼睛负责截图、读界面、看命令执行结果手负责移动鼠标、敲键盘、发JSON-RPC请求。这样原本需要人盯着做的机械操作就能交给代理去跑。这个思路不是新概念市面上也已经有不少相关项目但我的需求比较具体要免费、要能处理老旧的桌面GUI软件、还要能按标准协议扩展工具集。找了一圈很多工具要么只做命令行要么依赖一套复杂环境要么收费。所以我就决定自己做一版目标很明确免费、能操作GUI、支持MCP、单文件运行。最终这个目标也全部实现了虽然过程没有一开始想的那么顺利。1.2 为什么同时支持GUI和MCP第一次听到我同时做GUI操控和MCP接入的人都会问一句这俩不是重复了吗其实它们的定位完全不同。GUI操控是用来对付“没有开放接口”的软件的。很多桌面工具、企业内部系统只提供可视化界面没有命令行更谈不上SDK。想自动化除了模拟人去点鼠标没有第二条路。比如某个老旧的配置工具导出数据必须手动点菜单这种场景就只能依靠屏幕识别和键鼠模拟。MCP则是用来对付“有API但接口千奇百怪”的工具。MCP的全称是Model Context Protocol模型上下文协议它把外部工具的能力封装成统一格式AI可以通过同一个客户端去“发现工具、查看工具参数、调用工具”。我只需要给某个MCP server写好配置代理就能自动知道它能做什么不需要为每个工具写一套单独的调用代码。GUI解决的是“没有接口”的问题MCP解决的是“接口太多不统一”的问题。把两者放在一起后这个代理的能力边界就宽了很多它既能操作完全封闭的桌面软件也能稳定调用各种在线工具和数据源。2. 总体设计观察、思考、行动的循环2.1 核心循环怎么转起来整个代理没有复杂的调度逻辑核心就是一个循环观察、思考、行动然后再观察。和人的工作方式一样。对着屏幕看几秒想想要点什么动手操作一下再看看屏幕上发生了什么。在这个项目里我把它具体化为一个Agent Loop。循环每次会做这几件事先把屏幕截图或把当前环境状态收集回来然后把截图和任务历史一起发给大模型让模型输出下一步动作接着执行动作最后把执行后的截图或返回结果放回上下文再继续下一轮。整个循环有一个最大步数上限比如50步防止模型绕圈子出不来自动卡死。这个模式说起来简单但实际工程化的时候有不少细节。最核心的是不能让模型“失明”。每次动作之后必须把新的截图喂回去模型才能判断是不是点成功了、页面是不是变了。如果只看一次截图就开始规划后面的操作基本都是瞎猜。我第一版就犯了这个错误结果模型老是点同一个按钮因为它根本没看到点击后的变化。2.2 模块划分和各司其职我把代码按职责分成几个模块相互之间只通过接口通信。这样做最大的好处是以后换掉任何一块其他部分都不用动。核心模块一共五个。入口模块负责接收命令行参数读取配置文件规划器负责维护任务目标、对话历史和步数统计它决定接下来是哪一类动作GUI执行器负责所有屏幕相关的操作包括截图、定位、点击、输入MCP执行器负责通过stdin/stdout或SSE方式和外部MCP server通信安全模块负责检查动作白名单、敏感操作拦截、日志记录。这里我想重点说一下规划器和执行器分离的原因。如果让模型直接生成可执行Python代码运行起来很自由但也很危险。一个错误的缩进或一个恶意的库就可能把系统搞得一塌糊涂而且完全没有审计记录。反过来让模型只能输出固定的JSON动作灵活性下降一些但每个动作都是可解析、可检查、可拦截的。我宁可在统一动作集上多花点时间也不愿意让模型满世界乱跑。2.3 行动指令格式到底长什么样实际让模型输出的不是自然语言而是下面这种JSON格式的动作序列。每轮思考后模型可以返回一个动作也可以返回多个动作的数组。[ {action: screenshot, comment: 先看看当前界面}, {action: click, target: {text: Start, type: button}}, {action: type, text: pip install -r requirements.txt}, {action: key, keys: [enter]} ]MCP调用也走同一个格式。比如需要调用一个外部工具时动作会是这样{action: mcp_call, server: file_tools, tool: read_file, arguments: {path: ./config.json}}选择JSON而不是直接生成代码有个很直接的实践原因JSON可以被安全模块逐字段审查。比如key动作里的按键列表我可以设置白名单不允许发送winr这类组合避免代理误开系统功能。type动作里的文本也可以做敏感词检查。如果直接让模型写代码这些控制几乎都做不到。另外JSON解析失败时错误提示可以完整回传给模型下一轮修正这是普通代码生成很难做到的。我试下来这个格式虽然约束性比较强但出错率明显低排查也直观截图加日志一眼就能看出哪一步出了问题。3. GUI操控屏幕就是接口3.1 给代理一双能看清的“眼睛”GUI操控的第一个难点不是动手而是看清屏幕。我的方案是三层识别同时配合截屏后用传统图像模板匹配找固定图标用OCR提取屏幕上的文字信息再把截图交给视觉模型做自然语言定位。模板匹配适合那些位置基本不变的图标和Logo。把一张小图片作为模板在整块屏幕截图里算相似度找到坐标。这种方式速度快但怕界面换皮肤、改尺寸。OCR适合找文字按钮、输入框的标签比如界面上写着“下一步”“确认”我就能通过OCR找到这个文字的中心点。比较麻烦的是中文和字体小的界面OCR准确率会下降这时候我会先把目标区域放大两三倍再识别实测准确率能高一大截。视觉模型是兜底方案。我直接把截图缩小后发给支持视觉理解的大模型让它描述界面元素并返回归一化坐标。这里有个关键技巧不要让模型直接返回物理像素坐标而是返回0到1之间的比例坐标。因为不同电脑的屏幕分辨率、缩放比例完全不同物理坐标到了别的机器上就废了但比例坐标配合运行时获取的真实分辨率可以随时换算。我在代码里先拿到当前屏幕的宽高再用比例坐标乘以宽高得到实际点击点这个办法跨机器适配性非常好。3.2 让代理有一双能操作的“手”看得清之后还得点得准、敲得到位。操作层我用的是轻量级底层接口通过鼠标事件和键盘事件直接和系统交互而不是调用那些重量级测试框架。点击之前我会先做一件事情把鼠标移动到目标坐标停顿一下再按下弹起。移动这一步看起来多余但对很多软件很重要。有些桌面程序会做悬停态变化按钮在高亮之后才真正响应点击如果没有移动直接按下控件可能还处于未激活状态。输入文字也有讲究。对于输入框我会先点击一下确保焦点落在控件里再按ctrla全选清空旧内容最后才输入新文本。这样能避免原先输入的残留字符混进去。键盘事件我封装成列表形式每个按键用虚拟键码而不是直接发字符串。实测下来在Windows下用虚拟键码的兼容性比直接发送Unicode字符更稳定尤其是在某些中文输入法开启的状态下直接发字符可能导致文字被输入法吃掉。GUI操作还有一个避不开的问题等待。点击之后界面不是立刻刷新完的如果紧接着就去读下一个状态看到的可能还是旧界面。我的做法是引入一个简单但有效的等待机制执行完动作后等待0.5到1.5秒然后重新截图检查关键变化。如果没变化再重试一次而不是简单靠sleep。比如点击“下一步”后我们会循环检查目标按钮是否消失、新按钮是否出现最多等10秒。这比固定等待更聪明也不会因为网络慢而频繁失败。3.3 实操中GUI操控的几个稳定诀窍GUI自动化很多时候不是一次就能成功的不要指望模型输出一次坐标就永远正确。我做了一个比较实用的统计算法同一个动作执行后对比前后屏幕截图的变化区域。如果模型说已经点击了“保存”但截图上根本没有弹窗或者按钮没有任何变化就自动判定这个动作无效并把“操作后界面无变化”这个信息回传给模型让它换一种方式。这个反馈循环是GUI代理能真正跑起来的关键否则一步错后面全错。另一个诀窍是窗口锁定。在自动化开始前先找到目标窗口的句柄和位置之后所有点击都只在该窗口的坐标系内计算。不然其他弹窗、通知把焦点抢走了代理的所有操作就会打到错误的地方。我吃过一次大亏代理正在填表单结果右下角弹了一个系统通知后续所有点击全部偏掉花了十几分钟才排查出来。后来我干脆在启动前弹一个提示要求手动确认窗口已经前置并校验窗口位置确认无误再开始操作。4. MCP把工具能力统一成标准接口4.1 MCP为什么值得接入MCP的全称是Model Context Protocol一个用于AI系统与外部工具通信的开放协议。它做的事情很像给所有设备装一个通用接口工具提供方只需要实现一套MCP规范AI客户端通过同一套方式发现和调用所有工具。对使用者来说就是一次接入能被所有遵守这个协议的客户端复用。如果你以前给AI接工具可能需要给每个工具写单独的HTTP调用、认证、参数解析。而MCP把这一切标准化了。一个MCP server启动后会提供一份工具清单每个工具包含名称、描述、参数Schema。AI拿到这份清单后能自行决定什么场景下用哪个工具不需要预先把每个工具的API写死在代码里。这对我这类个人项目来说节省了大量适配工作。我在这个代理里选择了MCP还有一个原因生态增量太快。热词里那些MCP server从代码托管到浏览器自动化几乎每天都有新的。如果我自己为每个工具写对接代码精力完全不现实。但通过MCP我只要维护一份mcp_servers配置把server跑起来代理就能立刻获得新的能力。这也是“免费”背后的另一个含义你可以用社区已有的MCP server不必重复造轮子。4.2 自己实现一个轻量MCP客户端我这里没有用很重的MCP框架而是手工实现了一个轻量客户端只依赖标准库。因为对个人工具来说核心就是三件事启动MCP server子进程、发现工具列表、调用工具。MCP通过stdio传输时本质上就是读写子进程的标准输入输出消息用JSON-RPC 2.0格式每条消息用换行分隔。我封装了一个MCPServer类启动时用subprocess.Popen拉起server进程然后发送初始化请求。初始化完成后调用tools/list拿到工具列表之后就可以根据模型需求随时发起tools/call。下面是客户端最核心的请求逻辑简化版import subprocess import json import threading class MCPServer: def __init__(self, name, command, args): self.name name self.proc subprocess.Popen( [command] args, stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue, ) self.buffer self.initialized False def send(self, payload): line json.dumps(payload, ensure_asciiFalse) self.proc.stdin.write(line \n) self.proc.stdin.flush() def read_line(self): while True: if \n in self.buffer: line, self.buffer self.buffer.split(\n, 1) return json.loads(line) chunk self.proc.stdout.readline() if not chunk: raise RuntimeError(fMCP server {self.name} closed) self.buffer chunk def initialize(self): self.send({ jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: my_agent, version: 0.1.0} } }) self.read_line() self.initialized True def list_tools(self): self.send({jsonrpc: 2.0, id: 2, method: tools/list, params: {}}) return self.read_line() def call_tool(self, name, arguments): self.send({ jsonrpc: 2.0, id: 3, method: tools/call, params: {name: name, arguments: arguments} }) return self.read_line()这段代码看起来很短但已经能跑通完整的工具发现和调用链路。实际使用中我加了一层超时保护线上代码会设置5秒超时。因为有些MCP server启动特别慢尤其是基于Node生态的server首次初始化可能要好几秒。超时超过后我会主动把错误信息返回给模型让模型换一个工具而不是卡死在那里。4.3 MCP的配置和安全边界MCP server的配置我放在一个JSON文件里代理启动时读取。一个配置项包含server名称、启动命令和参数。比如我常用的文件工具可以直接配置成指向一个基于Node的MCP server。不过我不想把具体server名写死在例子里大家根据自己的需要填即可。{ mcp_servers: [ { name: fs_tools, command: npx, args: [-y, 你的文件工具server包], allowed: [read_file, list_directory, write_file] } ] }关于MCP我最想强调的一点是工具能力越强越需要限制。我的代理不是直接暴露所有工具给模型而是在配置里维护了一个allowed白名单。模型可以发现全部工具但执行时只能调用白名单以内的。开发阶段我甚至把“删除文件”“执行任意命令”这两类工具完全禁掉宁可手动去操作也不给代理这个权限。5. 单文件运行配置、依赖和密钥怎么处理5.1 用PyInstaller把整个代理压成一个文件单文件目标听上去很酷做起来主要靠打包工具。我用的是PyInstaller的--onefile模式。打包命令大概是这样的pyinstaller --onefile --noconsole --name my_agent agent.py--onefile把所有模块、依赖库都塞进单个可执行文件里--noconsole则让Windows下启动时不弹黑框。不过这句命令只是起点真正麻烦的是处理动态导入。PyInstaller靠静态分析来收集依赖但我在代码里用了不少动态导入的模块还有通过字符串引用的包打包后很容易缺文件。我最终的解决方案是写了一个hook文件显式告诉PyInstaller把哪些额外模块打包进去同时把配置模板、OCR语言包、模板图片等资源全部放进资源目录运行时从sys._MEIPASS读取。这一步解决之后单文件才真正能在干净机器上跑起来。onefile模式有一个特点程序启动时会把整个包解压到临时目录所以首次启动会慢一些有时要等一两秒。这对我来说可以接受。真正让人头疼的是杀毒软件误报。代理需要模拟键盘鼠标这种操作特征很容易被安全软件判断成恶意行为。我的处理办法是发布时做了代码签名虽然个人开发者拿代码签名证书不便宜但至少把自己常用电脑设为白名单开发阶段足够了。如果你只是自己用建议直接把编译目录加入杀毒白名单能省很多事。5.2 运行配置和模型密钥独立于代码为了不把密钥写进代码我把所有可变配置统一放在一个config.json里启动时读取。模型API的Key不直接写在这个配置文件里而是通过环境变量传入配置文件只记录环境变量名。这样即使我分享配置文件出来也不会泄露密钥。{ model: { api_base: 你的兼容接口地址, api_key_env: AGENT_API_KEY, model_name: 你的模型标识 }, gui: { screen_index: 0, dpi_aware: true, max_steps: 50 }, mcp_servers: [ { name: fs_tools, command: npx, args: [-y, 你的文件工具server包], allowed: [read_file, list_directory] } ] }单文件里没有“代码目录”的概念所以配置文件放在可执行文件同级的config目录下。启动时程序先检查配置是否存在不存在就生成一份模板并提示用户编辑。这个设计花了不少精力但实用性很高。我拿这个单文件拷贝到同事的电脑上只要填好配置和自己的API Key立刻就能跑不需要装Python也不需要安装任何依赖。这就是单文件运行最大的价值分发成本几乎为零。6. 实测中遇到的问题和排查记录6.1 高频问题速查表任何自动化工具都会遇到环境差异我这里把跑的过程中踩得最深的几个坑整理成一个速查表方便直接对照。现象原因解决办法点击位置总是偏几个像素或整块偏移系统DPI缩放导致逻辑坐标和物理坐标不一致启动时设置SetProcessDPIAware让进程按物理像素工作OCR识别不到中文小字截图分辨率不够文字太密集裁剪目标区域并放大2到3倍再做OCR识别率能明显提升MCP调用长时间无响应server初始化慢或网络超时给每个MCP server设置5秒超时超时后发送error给模型重试模型输出JSON格式频繁出错模型的输出概率天生不稳定解析失败后把异常信息和原始输出一起回传给模型要求重出一次PyInstaller打包后被安全软件误报键鼠模拟特征明显个人使用加白名单公开发布做代码签名代理反复执行同一个动作跳不出来缺少“状态未变化”反馈动作后对比截图如果界面没变化则视为失败并把状态回传给模型第一条DPI缩放我说一下具体表现。在Windows下如果你的显示器缩放是150%系统API返回的屏幕尺寸和实际截图像素是不同的。我早期踩坑时模型提供的坐标对应的是截图里的物理像素但鼠标点击用的是逻辑坐标结果每个位置都差一截。加了DPI感知声明之后整个进程按真实像素跑坐标才对上。6.2 安全防误操作的设计和实际体会如果问我在整个项目里最担心什么不是功能做不完而是代理在无人监督的情况下点了不该点的按钮。所以我给安全模块定了三条铁律。第一只读操作直接放行。比如截图、查文件、读进程信息这些不影响系统状态模型可以随时调用。第二写操作必须经过确认。比如修改配置文件、删除文件、执行有副作用的命令代理会把完整动作展示出来等我在终端里按一下y才继续。第三危险操作默认禁止。比如格式化磁盘、修改系统注册表、静默安装软件这类动作我在动作定义层就直接不做模型想输出都找不到对应的动作类型。我还在日志里完整记录了每一步的截图、模型输出和实际操作。代理跑完一个任务后我可以像回放录像一样回看整个过程每一步做了什么、屏幕变成了什么样一目了然。这个功能平时用不上但只要出了问题就是最有价值的调试工具。6.3 一个完整的实操场景复盘这里放一个我反复测试过的典型场景让代理帮忙安装一个Python项目的依赖并跑一次测试。任务下发后代理先截图看当前工作目录通过MCP工具读取requirements.txt确认项目文件存在。然后它打开终端窗口键入pip install -r requirements.txt按回车。接着循环截图观察终端输出。什么时候算装完代理不会等命令自己结束它会盯着屏幕上的提示符是否重新出现同时记录日志最后几行。一旦提示符回来了再调用MCP工具读取项目里的测试文件然后执行pytest把测试结果截图保存。第一次跑的时候问题出在它装完依赖后没有等待终端提示符结果下一个命令被输入到还没结束的进程里。后来我在GUI执行器里加了状态回读执行完命令后先截图OCR识别到命令行提示符特征才判定完成了。加了这一步之后同一套流程的成功率从60%以下提到了90%以上。这个提升并不是模型变聪明了而是工程上把“确认动作是否生效”这个环节补上了。最后再分享一点个人体会整个项目做下来我最大的体会是让AI编码代理“能跑通”并不难真正难的是让它“敢自主跑”。能跑通只需要把观察、思考、行动的循环搭起来敢自主跑则需要把反馈确认、安全限制、异常重试这些繁琐的细节全部补上。我现在的使用原则是先让它在虚拟机或者一个不重要的目录里跑几轮确认行为正常之后再让它接触正式项目。如果你也想做一个类似的东西我建议不要一开始就追求大而全。先从一个固定的简单场景入手比如只让它操作某一个软件的导出流程只接一个MCP服务器验证全套链路没问题后再慢慢放宽限制。每一步操作都保留日志每次失败都让模型看到错误信息这比堆更多功能有价值得多。这个代理现在帮我完成最多的恰恰是那些最枯燥的部分填表单、装依赖、点下一步。工具本身不一定复杂但把它打磨到我能放心让它自主操作这个过程本身就值回票价了。
返回列表