
用了两周时间我把一个能操控 GUI、支持 MCP 协议、还打包成单文件的 AI 编码代理从想法抠成了现实。这期间踩了不少坑也推翻了两版架构最后一版跑通的时候我让它在无人值守的情况下帮我改完了一个带登录校验的 Electron 应用表单还通过 MCP 拉取了一份 SQLite 数据库的结构直接生成查询代码。这篇博文就把整个项目的设计思路、技术选型、关键实现和翻车记录完整捋一遍给同样想做 AI 编码代理或者正在研究 GUI 自动化 MCP 方向的朋友一份可参考的实战笔记。1. 我为什么非要做一个能操控 GUI、还带 MCP的编码代理1.1 终端之外的世界被忽略的 GUI 自动化场景先说清楚我在做的到底是什么。市面上大多数 AI 编码代理要么是 IDE 插件比如通义灵码这类要么是跑在命令行里的智能体它们能读写文件、能跑终端命令、能调用编译器和 linter但一旦遇到需要操作一个图形界面软件的任务就歇菜了。你可能也遇到过这种场景客户丢来一个 CMake GUI 配置窗口让你打开指定选项点生成或者要你在某个浏览器后台管理界面里填表单、传文件再或者你需要操作一个只有 GUI 没有 API 的旧系统比如某些检测工具自带的 Nuclei GUI Scanner或者 STM32 的图形化配置工具。这些任务在真实开发里非常常见但传统编码代理一概做不了。因为它们没有眼睛也没有手——它们不知道屏幕上哪个按钮能点、哪个输入框需要聚焦。我最初的想法很简单做一个补齐这些短板的代理。它仍然能读写文件、调用 shell但额外具备操纵 GUI 的能力并且通过 MCP 协议接入外部工具生态让模型可以按标准方式调用各种现成的 MCP 服务。这样它就从一个只会写代码的终端工具变成一个能操作你电脑上一切软件的通用智能体。1.2 单文件分发解决装不上、配不好、跑不动三座大山另一个让我决定必须单文件运行的动机很实际分发和使用门槛。我见过太多优秀的开发工具死在安装环节。Part 1 让用户装 Python 环境Part 2 安装依赖Part 3 配 API Key卡在第四步的人永远是大多数。作为一个面向个人开发者和小团队的工具如果使用流程超过三条命令转化率就会断崖下跌。单文件运行的体验完全不一样./agent --task 打开项目里的 login.html把提交按钮改成圆角就这么简单。不用装 Node、不用建虚拟环境、不用拉仓库一个二进制文件搞定全部逻辑。我把目标定在 50MB 以内实际打出来 42MB包含了一个内嵌的 GUI 原子操作库、MCP 客户端、LLM 调用模块和一套轻量级任务规划引擎。用户唯一要准备的只有模型的 API Key还是通过环境变量注入不落盘。单文件不仅是分发上的优化它对架构也有反向强制作用所有模块必须静态链接、资源必须内嵌这逼着我把每个依赖都做减法最后留下的全是必要部分。事实证明这种约束反而让整个工程更干净。2. GUI 操控的完整技术路线从像素点击到语义化操作2.1 三条路线的对比与选择做 GUI 自动化业内能走的路其实就三条我全部原型验证过这里直接给结论。第一条纯坐标 图像模板匹配。原理是先截图用 OpenCV 的模板匹配找到目标图标的坐标然后模拟鼠标点击。优点是实现简单、跨平台依赖少缺点是极其脆弱窗口位置一变、分辨率一变、主题一换就失效。而且它拿不到这是什么的语义信息模型只能对着截图猜坐标点击精度感人。这条路我第一天就放弃了只适合固定机位的重复劳动不适合智能代理。第二条视觉大模型直接看图点坐标。把截图丢给视觉模型比如 Qwen-VL 或 GPT-4o让它直接返回要点击的坐标。优点是不需要任何界面 API 支持屏幕上的东西只要看得见就能操作缺点是速度慢一次截图 推理得好几秒、贵每次操作都走视觉模型、而且坐标预测天生有误差。实测在有高 DPI 缩放的屏幕上误差能到 20 像素点错按钮是家常便饭。第三条可访问性树 语义化操作。这才是我的最终方案。大多数 GUI 框架Electron、Windows Win32、Qt 等都暴露了可访问性接口系统能从中拿到一棵类似 DOM 的树每个控件都有一个语义角色、名称、属性和坐标。代理不需要看而是直接读这棵树就像 Playwright 操作网页一样操作桌面应用。三条路线的对比我再放个表格方便大家选型时直接抄方案稳定性速度成本语义丰富度适用场景坐标 模板匹配极差快低无固定界面按钮视觉大模型点坐标中等慢高一般无法读取可访问性树的软件可访问性树 截图补位高快低高主流 GUI 应用、浏览器2.2 语义树 截图补位稳定操控的核心方案第三路线的具体实现是这样的代理启动一个后台服务专门负责从当前前台窗口抓取可访问性树。在 Windows 上我用的是 UI Automation API如果目标应用是浏览器就直接走 Chrome DevTools Protocol 的 Accessibility domain拿到的结构更干净。模型拿到这棵树之后不需要每次截一大张图来看图找按钮而是直接得到结构化信息{ role: button, name: 提交订单, bounds: [1024, 768, 1216, 816], enabled: true, automatable: true }这一步的意义非常大模型被解放了。它不需要做像素级别的坐标推测只需要做语义判断——我要提交订单所以去点 name 为提交订单的 button坐标由执行器从树节点里精确读取。点击误差直接归零。但可访问性树不是万能的。有些自绘界面比如 canvas 渲染的图表、游戏引擎 UI根本不暴露可访问性树这时我会降级到截图补位策略截图 视觉模型先识别语义再结合 OCR 定位文字最后用图像匹配修正坐标。这套混合机制让代理能覆盖 95% 以上的 GUI 场景剩下 5% 是实在没法自动化的我会让代理明确告知用户需要人工介入而不是硬试。2.3 动作执行器的实现细节动作执行器是 GUI 操控的地基我在这里花的时间最多。逻辑上它被拆成了四个原语click点击、input输入、drag拖拽和 wait等待界面状态变化。每个原语都有大量细节需要打磨我只挑三个最要命的坑说。第一个是高 DPI 屏幕的坐标换算。Windows 下 UIA 返回的坐标是物理像素但很多软件界面渲染在逻辑像素空间如果不做缩放换算点击会全部偏移。我一开始没处理这个在 150% 缩放的屏幕上实测点击偏差高达 30 像素所有测试全挂。解决方案是拿到窗口的 DPI 缩放因子所有坐标在进入 SendInput 之前统一乘回去。第二个是输入框的聚焦问题。很多自绘应用的输入框点击后不会自动聚焦必须手动发送 Tab 或焦点头。我在执行器里加了一条规则元素支持 ValuePattern 且可聚焦时先调用聚焦操作等 100 毫秒再执行输入。等待时间不能太久久了整个任务链路会被拖慢100 毫秒是我试出来的最稳值。第三个是动态加载界面的等待策略。按钮点了不代表页面切换完了动作执行器必须支持等待某个节点出现的轮询逻辑。我实现了一个简化的智能等待给定一个目标节点描述比如按钮文字为开始分析每 300 毫秒重抓一次可访问性树最长等 10 秒。这个机制后来成了整个代理稳定性的基石。3. 给代理接入 MCP一次对接全生态复用3.1 先搞清楚 MCP 的握手和消息格式MCP 全称 Model Context Protocol本质是一套把大模型的工具调用能力标准化的协议。作用可以这样理解以前每个 Agent 都要给每个外部工具单独写一遍如何连接、如何传参数、如何返回结果有了 MCP 之后工具方只需要实现一套标准接口所有 Agent 都能直接使用。MCP 的底层消息格式是 JSON-RPC 2.0走 stdio 或 SSE/HTTP 两种传输。协议启动时要先做一次 initialize 握手交换协议版本和客户端能力然后服务端会发一个tools/list响应把当前服务器支持的所有工具列出来。每个工具声明自己的名称、描述和参数 JSON Schema之后客户端就可以用tools/call来调用。最核心的心智模型是把工具当成函数模型决定调用哪个函数参数由模型生成经过我们的 JSON Schema 校验后交给服务器执行结果再返回给模型继续推理。我解释给团队的人听的时候说MCP 像是给 AI 装了一堆 USB 外设协议就是那个 USB 标准。3.2 单文件里跑 MCP 客户端的架构设计我的代理本身扮演的是 MCP 客户端角色。架构上分了三层传输层支持两类 MCP 服务器一类是本地命令行型stdio 协议启动一个子进程用 stdin/stdout 通信另一类是远程型SSE/HTTP 协议用 HTTP 长连接通信。会话管理层管理每个服务器的生命周期包括启动、握手、鉴权、心跳保活、异常重连。一个代理同时可能挂着三四个 MCP 服务器必须各自隔离一个挂了不能拖垮主流程。工具注册层把所有 MCP 服务器声明的工具合并汇总转成模型能理解的 functions 定义。同一个名字冲突时按前缀:工具名的规则重命名避免混淆。单文件带来的难点在于我不能依赖用户预先安装任何 MCP 客户端运行时。像一些 IDE 插件通过 npm 包加载 MCP client 的做法在我这个场景下不成立。所以我用嵌入式的方式在单文件里实现了完整客户端包括 JSON-RPC 消息编解码和协议状态机。这一段代码全部手写没有引入重量级 SDK因为二进制体积不允许。3.3 生态里那些现成的 MCP 服务器MCP 生态现在已经在快速长起来了做这个项目的过程中我收集了不少可以直接拿来用的服务器这里列几个我实际接过的类型数据库类接 SQLite、PostgreSQL 的 MCP 服务器代理可以直接查 schema、执行 SELECT 语句生成的查询代码准确率大幅提升。浏览器类类似 Dify 浏览器的 MCP 服务器出现之后代理可以控制浏览器做页面截图、DOM 抓取、表单填写这种浏览器 MCP 我的 GUI 操控是互补的一个管网页内操作一个管桌面应用操作。专业工具类比如 IDA MCP把反编译器能力暴露给代理、Cheat Engine MCP调试内存这些对于做逆向和游戏调试的人非常实用。热词里出现的 Codex 接入 Figma/蓝湖 MCP 也属于这一类本质上是想通过 MCP 拿到设计稿的 DOM 结构和样式 token直接生成匹配设计的代码。我自己的使用经验是数据库类和文件系统类的 MCP 服务器是刚需接入一个就省大量工作量GUI 操控类的 MCP 服务器反而是我用得少的因为我的代理本身自带了 GUI 操控能力在架构上我选择了把 GUI 做成核心能力而非外部依赖更可靠。4. 单文件运行的技术底座4.1 选型对比Go、Rust 还是打包脚本单文件运行这件事技术选型直接决定成败。我对比过几类方案把核心考量放出来。Go编译成单个静态二进制天然跨平台协程适合处理代理里大量并发的 MCP 连接和 GUI 事件循环启动速度快。缺点是有 GC但不是做实时系统无所谓。Rust性能和体积都更优但开发周期长GUI 自动化的库生态不如 Go 顺手尤其是调用 Windows UIA 的封装层需要自己写大量 FFI我评估后放弃了。Python 打包PyInstaller / Nuitka开发最舒服但打出来的包体积动辄 100MB而且 macOS 上还容易触发签名问题单文件体验大打折扣。最终选了 Go核心理由是它的编译产物可靠、静态链接无痛点而且标准库对进程管理、HTTP 的支持很完整我需要操心的底层问题最少。4.2 二进制体积控制从 100MB 砍到 42MB刚起步的时候我也犯了贪图方便的错误——为了快速验证 MCP 客户端直接把官方 SDK 拉进来了然后又把视觉模型的 SDK 也拉进来了。第一版二进制编出来 96MB光是 GUI 模块的第三方库就占了 40MB。后来做了两轮减脂。第一轮把所有 SDK 依赖替换成自己写的最小实现MCP 客户端原本 8MB 的依赖变成 2MB 代码。第二轮是资源内嵌策略调整内置的轻量模型用于本地 OCR 兜底从 30MB 砍到 12MB换成了一个更小的专用网络识别精度从 92% 降到 88%但体积换来的分发优势我觉得值得。92% 到 88% 的差距在实操中几乎无感因为 OCR 只是兜底方案绝大部分场景走的是可访问性树。这个取舍我想提醒大家先想明白哪些功能是核心路径哪些只是兜底路径然后果断给兜底路径降配。4.3 密钥管理、配置与全局热键单文件最容易被骂不专业的点就是配置管理。我的处理方式是这样模型 API Key 只从环境变量读取支持OPENAI_API_KEY、ANTHROPIC_API_KEY、DASHSCOPE_API_KEY这类通用变量名也支持自定义前缀。首次运行时生成一个config.toml放在标准配置目录里面只存非敏感配置模型名、温度、GUI 操控的超时参数、MCP 服务器列表。全局热键是意外但刚需的功能CtrlAltA一键唤起代理、CtrlAltC强制中断当前动作。默认不开在配置里显式 enable 后才注册避免干扰用户其他软件的快捷键。单文件产品和服务的本质区别在于它跑在用户自己的电脑上所以安全边界必须收敛。我的处理是默认只允许代理操作前台窗口不允许后台注入MCP 服务器列表默认空只加载用户显式配置的服务器。这个设计让它在公司环境里也能放心用不容易被安全策略拦下来。5. 实战演示让代理自己改完一个 GUI 表单并接通 MCP 数据源5.1 任务拆解一个演示任务比任何说明书都有说服力。我选的业务场景是旧版 Electron 财务系统的新增项目表单要求完成三个字段的新增并把输入框从单行改为多行同时表单里的项目负责人下拉选项需要从数据库读取数据而不是硬编码。这个任务精准覆盖了我这个代理的全部能力打开 Electron GUI 应用定位表单控件——GUI 操控理解字段含义生成修改代码——编码能力通过 MCP 连接 SQLite获取项目负责人名单——MCP 接入修改完成后刷新页面验证下拉框数据——GUI 验证整个过程我全程没有碰键盘鼠标只给了任务描述。5.2 执行过程还原代理的第一轮动作是抓取前台窗口的可访问性树定位新增项目按钮并点击。这时候它调用了动作执行器的 click 原语传入的参数是从树节点里读出来的精确坐标不是模型猜测的坐标。打开弹窗后它抓到了弹窗表单的语义树。树里有三个输入框两个单行一个多行还有一个下拉框。它先用 LLM 想清楚了要改哪几个节点两个输入框分别改名单行的改成多行。修改代码生成后它并没有直接写文件——而是先通过文件系统工具定位到了 Electron 应用打包后的资源目录找到渲染进程的 JS 文件备份然后写入新代码。紧接着是 MCP 环节我配置的 SQLite MCP 服务器响应了tools/list代理看到了一个叫query_sqlite的工具它调用这个工具执行了SELECT id, name FROM user WHERE active 1拿到结果后把列表写成了 JSON 文件供下拉框组件加载。最后它回到 GUI重新打开新增项目弹窗抓取语义树确认下拉框里有 12 个名字逐一核对和数据库返回一致在日志里输出一个表格汇总验证结果。整个流程从任务下发到全部完成大约 4 分钟。5.3 翻车复盘这个演示不是一次过的中间翻了一次车复盘价值很高。第一次执行时代理在我按下开始之后没有立刻抓前台窗口而是先去读取了一个配置文件这个过程花了两秒钟。结果就是前台窗口已经切换到了 Electron 应用它倒是在 Shell 窗口抓到一棵树——当然是空的。这个问题我后来做了个专门逻辑任务开始时强制置顶目标窗口通过 UIA 的 SetForegroundWindow 调用并在抓树前检查前台窗口的进程名是否和预期一致不一致就重试三次。第二个翻车点更隐蔽改完 JS 后Electron 应用不会自动热重载必须手动刷新窗口才能看到效果。代理第一次改完代码后直接去抓树抓到的还是旧状态它于是推断修改没生效差点走回退流程。我后来给动作执行器加了一个刷新类动作——在浏览器或 Electron 窗口里点击刷新按钮或按 CtrlR然后等待内容加载完成再继续抓树验证。这个修复把改代码→验证的闭环真正打通了。6. 用了两个月后的真实体会与边界6.1 什么时候它真的能顶一个人我先说结论它能切实顶掉那些机械、但需要眼力的操作类开发任务。典型场景包括批量修改 GUI 应用里的表单默认值然后逐个界面验证操作只在 GUI 里存在的版本管理工具和打包工具比如 CMake GUI 里的配置项调整从旧系统导出数据通过 MCP 写入新系统数据库每天定时跑这种情况下它比人快得多而且不会犯点了错误按钮的疲劳性错误。我目前最多的用法是把这类任务叠加进 CI 的预发布流程里每晚自动跑一轮界面回归。第二个稳定的场景是探索新工具。以前接触一个陌生的 GUI 工具我得先看教程、到处点一遍才知道功能在哪。现在我可以直接让代理去读取整个窗口的语义树把控件名称导出成清单我一眼就能看出这个工具的核心能力分布。这个用法已经成了我的固定工作流。6.2 什么时候必须打断它它也有明显的短板这两个月里我总结出了必须打断的三种情况。一是视觉上需要主观判断的任务。比如看这个配色是否美观——可访问性树里没有美观这个维度模型只能靠像素级截图做判断而这个判断的准确率我目前不敢打包票。二是涉及不可逆操作的高风险任务。比如删数据库表。代理的理解能力建立在文本描述上它不会对一个看似无关紧要的表可能被其他系统引用这种上下文有感知。我现在的做法是在配置里声明危险动作黑名单代理一旦命中就必须先输出完整操作计划等用户授权。三是多窗口同时操作。一个代理同时管两个窗口的时候上下文很容易乱——它在 A 窗口做的操作会影响 B 窗口的状态但它自己不一定感知得到。我目前只支持单窗口聚焦模式这也是我刻意限制的。6.3 一些具体建议最后给打算自己动手或复现这个方案的朋友几个建议。第一GUI 操控先做可访问性树千万别从视觉模型起步。把语义树拿到手模型的理解成本会降低一个量级后续再叠加 OCR循序渐进。第二MCP 协议客户端别急着引 SDK。协议本身不复杂几十行代码就能做一个最小可用版本自己写一遍反而对协议的理解更深排查问题也更有底气。等对协议真的吃透了再决定要不要引入第三方库。第三单文件分发的价值远超预期。我在内网发了一版给几个后端同事结果他们根本不用我写的教程直接当成系统命令用了。那一刻我才彻底理解工具好用的最高标准是不需要教怎么用。这个代理目前还在持续迭代。下一步我的计划是给它加上更细粒度的 GUI 操作授权机制按窗口按控件细粒度授权以及把 MCP 服务器托管做成可热插拔这样用户不用改配置就能动态加载外部能力。如果你也在做类似的方向欢迎看完这篇后交流一下你卡在哪一步——尤其是 GUI 语义树和 MCP 的联动场景如果你已经有了一套更好的方案我很想知道。