
我做了个免费 AI 编码代理支持操控 GUI 和 MCP单文件运行先说这个项目是干嘛的一个完全免费、本地跑的 AI 编码代理能通过 MCP 调用外部工具也能直接操控 GUI最关键的是——编译完只有一个可执行文件丢到哪台电脑都能跑不需要装 Python 环境不需要配 Node更不需要折腾虚拟环境。我花了两周多时间从零折腾出来中间踩了不少坑今天把整个设计和实现过程完整拆开讲一遍包括 MCP 协议的选择、GUI 自动化方案的取舍、单文件打包的细节以及一箩筐实战中才碰得到的鬼问题。适合想自己搭一套 AI Agent 工具的开发者也适合对 MCP、GUI 自动化感兴趣的玩家哪怕你是刚接触这些概念的新手这篇文章也能帮你少走一大段弯路。先交代一下背景。我一直想要一个“能自己动手干活”的编码助手不只是帮我补全代码而是真的能打开界面、点按钮、输入文本、读取内容、执行命令然后把结果反馈给我。市面上类似工具不少但要么收费要么重度依赖云端要么装起来要配一堆环境变量和依赖包。我就想能不能用最朴素的方式自己做一套代理出来核心需求就三条一是能通过 MCP 让 AI 调用本地工具二是能操控 GUI 应用三是必须支持单文件运行拷贝即用。于是就有了这个项目。1. 整体设计与思路拆解1.1 为什么选择 MCP 协议作为工具调用的骨架MCPModel Context Protocol简单说就是一套标准化的“AI 调用外部工具”的协议。它把工具注册、参数校验、结果返回都规范了起来AI 模型不再需要自己写死每种工具的调用方式而是通过统一的接口去发现工具、传入参数、拿到结构化结果。我最初也想过自己写一套 JSON-RPC 工具调用协议后来发现全是重复造轮子MCP 生态里已经有不少现成客户端和服务端实现直接用反而省事。我选 MCP 还有个重要原因它能跨模型使用。今天接 OpenAI 兼容接口明天换本地模型后天接国内大模型只要 MCP 服务端不变工具层完全不用重新适配。这种“模型无关、工具标准”的设计思路正好解决了 AI 编码代理最头疼的兼容性问题。实际用下来MCP 的工具注册机制能自动生成 JSON Schema模型侧对参数的推断准确率高很多不像自己写的协议那样容易出现参数格式对不上、模型瞎猜字段的尴尬场面。1.2 GUI 操控的三种备选方案我为什么选这条GUI 操控是这个项目里最难啃的骨头我前后对比过三种方案第一种是图像识别加坐标点击类似按键精灵的思路。优点是通用性强什么软件都能点缺点是慢、脆弱窗口一变位置或分辨率一变之前的坐标全废了。第二种是辅助功能接口直接读取系统级 UI 树能找到窗口、按钮、文本控件的准确信息。这个方法稳定很多但跨平台适配工作量不小而且很多小众应用不暴露完整的辅助功能信息。第三种是专业 UI 自动化框架比如 Windows 上的 UI Automate 方案能查询控件名、点击按钮、读取列表内容API 设计也贴近人的操作直觉。我最后选了第三种作为主力原因很现实它能返回“结构化”的界面信息。AI 模型通过 MCP 调用工具读取窗口控件树返回的是带层级关系的 JSON模型可以读懂哪个按钮对应什么操作而不是猜像素坐标。对于常见的桌面软件、浏览器窗口这种方案的成功率明显高得多。当然我也留了一手遇到一些特殊自绘 UI 窗口读不到控件树时再退回图像识别方案进行兜底。这种“辅助功能为主、图像识别兜底”的组合实测下来覆盖了九成以上的 GUI 自动化场景。1.3 单文件运行把“环境依赖”这个最大的坑用打包碾平很多 AI Agent 项目安装过程特别劝退要装 Python 3.11、配虚拟环境、拉一堆依赖换台电脑就得重新来一遍对于非重度开发者来说门槛太高。我做的第一个重大决定就是必须单文件运行。目标用户拿着代理可执行文件双击就能起服务、开界面Windows 和 Linux 都需要支持macOS 暂缓。这里说的“单文件”不是指源码只有一个 .py而是指构建产物只有一个可执行文件把解释器、第三方库、静态资源全部打进去。这样做的代价是文件体积变大启动时解压有点开销但换来的部署体验提升是碾压级的。在我实测的机器上可执行文件大约 80MB启动时间 2 到 3 秒完全在可接受范围内。最终我在不同机器上反复验证了“拷贝即用”这才算真正达到了设计目标。2. 核心功能拆解与实操细节2.1 MCP 工具注册与调用的完整链路这部分的架构其实不复杂但细节非常多。我拆成三层来看模型层、代理层、工具层。模型层负责意图理解和参数生成代理层负责维护会话上下文并调度工具调用工具层就是一个个具体的 MCP 工具比如“打开应用”“读取窗口”“执行命令”“写文件”。整体调用链路我简化一下大概是这样的async def run_agent(task: str): messages [{role: user, content: task}] for step in range(10): response await llm_client.chat(messages) tool_calls response.get(tool_calls, []) if not tool_calls: return response[content] for call in tool_calls: tool_name call[function][name] args json.loads(call[function][arguments]) result await mcp_registry.execute(tool_name, args) messages.append({ role: tool, tool_call_id: call[id], content: json.dumps(result, ensure_asciiFalse) })这段代码的巧妙之处在于完全基于通用聊天接口模型返回什么工具调用我就执行什么再把结果追加回消息列表让模型基于最新结果继续推理。我后来测试了很多任务大部分都能在 10 轮工具调用内收敛。这里有个关键经验MCP 的工具描述必须写得非常详细包括参数格式和返回值示例模型才能准确理解意图。比如“打开应用”这个工具我就在描述里写了支持哪些常见路径、找不到程序时该去哪搜索模型少犯很多错误。2.2 GUI 自动化工具的设计细节GUI 自动化工具是我自己封装的核心模块MCP 只是外壳。我暴露给模型的工具并不需要模型了解 Windows API 的复杂性。我把操作简化成几类对应暴露 6 个 MCP 工具gui_list_windows列出所有顶层窗口返回句柄、标题、进程名。gui_inspect读取指定窗口的完整控件树。gui_click按控件名或坐标点击。gui_input在目标输入框输入文本。gui_scroll滚动窗口内容。gui_screenshot截取窗口或屏幕图像。注意gui_inspect返回的控件树必须剪枝。未经过处理的控件树可能包含上千个节点像钉钉这类界面复杂的软件动辄两层三层嵌套序列化成 JSON 后 tokens 会爆炸。我在封装时做了深度控制只保留可见控件、合并冗余按钮文本、压缩坐标信息让每次模型看到的实际数据能控制在几百行以内。关于坐标点击有个非常实用的经验模型理解坐标比理解控件路径更费劲所以最好优先按控件名搜索找到控件后获取中心坐标再点击。这样模型只需要描述“点击确定按钮”代理自动把按钮名称解析成坐标。我在不少场景实测下来这种方式既稳定又省 tokens。2.3 流式输出让每一步操作都能实时看到用户等待 AI 干活时最怕的就是长时间没反馈。我曾经用过一个很蠢的版本AI 执行完整个任务后才一次性回复中间如果 GUI 卡住或者工具调用失败用户完全不知道发生了什么。后来我就把代理改造成流式输出模式每次工具调用、每一步操作、每一条日志都作为事件实时推送到 Web 界面上。实现的原理很简单代理端把事件放到一个异步队列里WebSocket 服务把队列内容推送前端前端按角色渲染成不同气泡。模型思考时显示“思考中”状态调用工具时显示“正在打开应用 xxx”执行 GUI 点击时显示“点击了控件确认按钮”。这种透明化设计不仅好看也特别利于排查问题——一旦哪一步卡住你能立刻看出是模型决策出了问题还是工具执行失败了。3. 实操过程从源码到单文件可执行程序3.1 环境准备与依赖选择搭建环境这块我建议直接照抄我的配置避坑效果明显。开发语言我选 Python主要原因有二一是 MCP 的 Python SDK 非常成熟二是 UI 自动化生态在 Python 这边选择最多。依赖方面只需要少量核心库pip install mcp fastapi uvicorn websockets pyinstaller pip install pywinauto pillow numpy如果读者只是做一个类似的项目底层的pywinauto用来做 Windows GUI 操控pillow和numpy用来做图像识别兜底fastapi和websockets用来搭建本地 Web 服务。所有这些库体积都不小等打包阶段就明白了单文件体积膨胀基本来自这里。关于 Python 版本我强烈建议用 3.11 以上。原因很简单3.11 对异步编程的支持更强内置tomllib能少装一个依赖而且 PyInstaller 对高版本支持也好了很多。我最初在 3.9 上开发后来因为一个语法兼容问题被迫统一到 3.11回头看这个迁移决定帮我省了很多后续麻烦。3.2 MCP 服务端封装流程MCP 服务端封装是整个系统的技术核心之一。SDK 提供了FastMCP类可以直接把普通 Python 函数注册成工具我封装好 GUI 工具后用 Python 装饰器暴露出来就行。这一步极其简单一段代码就能说清楚from mcp.server.fastmcp import FastMCP mcp FastMCP(agent-gui-control) mcp.tool() async def open_application(app_path: str) - dict: 在系统上启动一个应用程序。 Args: app_path: 应用程序的完整路径或可执行文件名。 return await gui_controller.launch(app_path) mcp.tool() async def click_by_name(window_title: str, control_name: str) - dict: 点击指定窗口中指定名称的控件。 return await gui_controller.click(window_title, control_name) # 启动 MCP 服务 if __name__ __main__: mcp.run(transportstdio)MCP 服务端跑在 stdio 上意思是代理进程直接通过标准输入输出与工具进程通信调用工具时发送 JSON 消息给子进程子进程执行完再返回 JSON 结果。这种传输方式特别适合单文件分发因为不需要额外起一个网络服务也没有端口冲突的烦恼。我强烈建议把 GUI 工具放在独立进程中执行而不是在 AI 代理进程里直接调用。好处有两点第一是隔离崩溃GUI 自动化偶尔会卡死或抛异常不会拖垮主代理第二是便于重启某个 GUI 工具进入异常状态时直接重启子进程就能恢复主进程完全不受影响。3.3 Web 操作界面的实现单文件程序也要给用户一个操作入口我选的是“本地 Web 界面”。代理启动后自动拉起默认浏览器访问 127.0.0.1 的固定端口。开发时用 FastAPI 做的后端前端就是一个简单的 HTML 页面支持输入任务描述、展示流式日志、展示控件树结果。为什么不用桌面 GUI因为我不想再引入一套窗口框架到头来又要为打包大小和跨平台兼容头疼。Web 界面有个额外好处AI 代理在服务器上运行时用户在浏览器里也能随时随地访问天然支持远程控制。实际测试中我用手机浏览器绑定了代理端口虽然界面布局有点挤但核心功能一个不落。3.4 单文件打包的关键配置与体积优化PyInstaller 能把所有依赖打包进一个可执行文件但直接用默认配置会有很多坑一个是第三方库的动态依赖检测不全另一个是打包出来体积巨大。我的打包命令长这样pyinstaller --onefile --name ai-agent-gui \ --add-data webui:webui \ --hidden-import mcp.server.fastmcp \ --hidden-import pywinauto \ --collect-all mcp \ --exclude-module matplotlib \ entry.py--add-data用来带上 Web 界面静态文件--hidden-import是为了确保动态加载的模块被正确包含--collect-all mcp会把 MCP SDK 的所有子模块都收进来避免遗漏。--exclude-module matplotlib是我做过的最大体积优化它没被真正用到但 PyInstaller 的依赖分析有时会把它误检进去。实际测试中单文件体积从 120MB 直接降到 80MB。打包后每次启动PyInstaller 会把依赖解压到临时目录所以首次启动有 1 到 2 秒延迟后续会走系统缓存稍微快点。这里有个无法绕过的问题自带 GUI 控制能力的可执行文件很可能被杀毒软件当成可疑行为。因为它的行为模式操作其他窗口、注入控件、发送按键和自动化脚本有相似性。我在 Windows Defender 上就遇到过一次误报处理方法是给程序目录加白名单或者对可执行文件做数字签名后者更保险但也更麻烦。这个属于发布阶段必须要接受的“单文件代价”。4. 实战演示让 AI 把一次 GUI 操作完整跑通4.1 案例目标与使用流程实践出真知我用一个具体任务走一遍完整流程让代理打开记事本在里面输入一段文字然后另存为桌面上的文件。读者可以在自己的电脑上模拟这个任务只要把路径换成实际存在的应用即可。操作流程分四步启动代理、输入任务描述、观察各步骤执行、查看结果。启动代理后界面会自动打开浏览器输入一个中文提示请打开记事本输入“MCP GUI 实战测试”然后保存到桌面的 mcp_test.txt。我预期代理会调用三个工具打开记事本、输入文本、保存文件。但实际跑起来模型还加了“等待窗口出现”和“读取控件树确认状态”两步这一下就完美展示了 AI 编码代理的价值——它知道自己在做什么甚至会在无信息时主动探测。4.2 各工具调用过程的日志解读第一轮模型调用的是open_application。它传的参数是notepad.exe代理执行后返回“成功启动进程主窗口句柄 2097158”。第二轮模型调用gui_wait_and_confirm等待窗口出现并读取了控件树。第三轮模型才定位到文本编辑区域第五轮调用save_file。我截取了核心日志[1] toolgui_wait_window args{title:记事本} result: OK [2] toolgui_inspect args{title:记事本} result: windows1, controls28 [3] toolgui_input args{title:记事本, control:Edit, text:MCP GUI 实战测试} result: OK [4] toolgui_click args{title:记事本, control:文件} result: OK [5] toolgui_click args{title:记事本, control:另存为} result: OK [6] toolgui_input args{title:另存为, control:文件名, text:mcp_test.txt} result: OK [7] toolgui_click args{title:另存为, control:保存} result: OK模型一共用了 7 次工具调用。最值得关注的是第 6 步它对“另存为”窗口输入文件名时没有直接回车而是选择了点击“保存”按钮。这说明模型理解了 Windows 对话框的行为模式文本输入后必须点击按钮触发最终保存而不是简单地确认。这个小细节让我确定基于控件的结构化路径比坐标点击更符合模型的理解能力。4.3 失败回退与重试设计这个案例在一次演示中出现过惊险场面保存对话框弹出后控件树读出来只是部分内容没有“保存”按钮。原因是文件名的输入框还没聚焦完整UI 树刷新不及时。我原本的设计里gui_click失败会直接报错但后来加了一个容错机制工具执行失败后代理会把错误信息发回模型模型可以根据错误自动决定重试还是换方案。那一轮模型回了一句“控件未找到先输入文件名再重新读取窗口控件树”然后主动执行了第二步顺利完成保存。由此可见流式反馈与重试机制合在一起就是 AI 编码代理真正“智能”的核心所在。模型不需要一次部署好所有路径而是靠工具结果不断感知环境自我修正。这个设计让我后来在处理更复杂的任务时省了大量调试时间。5. 常见问题与排查技巧实录5.1 MCP 工具调用报错JSON Schema 与模型参数格式不匹配这是 MCP 路径上最多的问题。现象是模型传的参数类型不对比如tool_call里把window_title传成了数字或 null服务端反序列化时报错。我排查发现根源有二一是模型本身对参数类型理解不稳定尤其面对复杂的嵌套参数二是工具 Schema 标注不够严格导致模型自由发挥空间太大。后来我立下一条规矩所有 MCP 工具的参数必须只保留简单类型如字符串和布尔值避免使用嵌套对象和数组。复杂的结构先包装成 JSON 字符串由函数自己json.loads()解析。这样做虽然看起来笨拙但实测模型生成正确参数的比例大幅提升几乎接近百分百。如果你的模型对复杂类型有很强的能力可以考虑不强求简化但保守方案永远更稳。5.2 GUI 窗口读取不全或控件树为空80% 的 GUI 自动化问题都出在控制权不足、窗口未激活或者程序用了自绘界面。pywinauto读取控件树依赖系统的辅助功能接口很多抓窗口工具对后台窗口读取是不完整的。一个非常实用的技巧是先让窗口获得焦点等待 0.5 秒再重新读取控件树往往能多出很多控件。这就是我gui_inspect工具内部默认做了一次“激活窗口 睡眠 重读”的原因。特殊自绘窗口如某些游戏界面、带硬件加速的渲染窗口辅助功能接口根本读不到。只能走截图与图像识别兜底但识别速度明显变慢调试也更困难。遇到这种窗口我一般建议让 AI 换个思路比如直接用快捷键或命令行完成操作而不是硬刚 GUI。5.3 单文件在别的电脑上无法运行明明是单文件拷到别的机器却起不来的情况也很常见。原因通常是缺少 Visual C 运行库。我最初打包时没有包含运行库导致在精简版 Windows 上直接报“DLL 加载失败”。PyInstaller 有--add-data和--runtime-hook等机制可以帮忙但最省心的做法是让目标机器装一下微软常用运行库。我在项目文档里特别标注了这句话算是“单文件并非真正零依赖”的诚实提醒。另一类更坑的问题出现在执行环境上如果你的代理需要操作某个窗口但没有管理员权限pywinauto对某些高权限进程调用SendMessage时会直接失败或静默失效。权限不够时窗口标题能读到但点击、输入都没有效果。排查这类问题最快的方式就是打开 UAC 以管理员身份运行代理实测成功率提升显著。5.4 常见问题速查表问题现象可能原因快速解决方式MCP 工具参数解析失败类型不匹配、参数含复杂对象保持参数为简单类型复杂结构 JSON 字符串化窗口控件树为空辅助功能接口未暴露、窗口不可见先激活窗口再读取对自绘 UI 换截图方案点击按钮但 UI 无响应权限不足、后台窗口锁死以管理员身份运行等待窗口重新绘制单文件启动慢PyInstaller 解压到临时目录首次启动后使用系统缓存或改用目录打包模式杀毒软件拦截行为特征与自动化工具相似加白名单或做代码签名6. 扩展可能性与个人实操体会这个项目做完后我第一时间想的不是怎么商业化和如何完美而是如何利用这套框架持续加新功能。最让我激动的一点是MCP 让工具层彻底“即插即用”了不用动代理主逻辑我只要不断添加 MCP 工具AI 就能学会调用它们。我现在已经接了文件操作工具、命令行工具、浏览器控制工具后面还计划加数据库查询和 HTTP 抓取工具。如果你也想做一个类似的东西有一个很值得研究的方向试试让代理通过 MCP 工具回写自己的配置文件实现自我修正和升级这会是一个很带感的体验。在整理这套架构和代码的同时我自己留下了几个重要体会。第一模型上下文管理比工具实现更值得花时间。GUI 控件树动不动就是几千行如果全部塞给模型一个任务 20 秒内就烧光了上下文胜率反而低。我的选择是每次把结果剪枝压缩到 100 到 200 行只保留关键控件和状态信息反而让模型决策更精准。第二工具返回值必须结构化不要半路返回“成功”这种闷话要带上关键状态和简短信息比如“窗口已打开标题是 X位置在 Y”模型后续操作出错时更容易自我纠正。最后分享一个小技巧在 MCP 工具描述里有意提示失败场景。比如“打开应用时如果找不到提示检查是否已在运行”模型真遇到这个问题时就不会傻乎乎的反复重试而是直接走你的后备路径。类似的提示越多代理就越像一位经验老到的操作员而不是只会复读的机器人。说到底AI 编码代理的天花板不取决于用了多大参数的模型而取决于工具层的设计质量、错误恢复的细腻程度和用户对整个过程的理解深度。我现在仍陆续把这个项目的各种边角打磨完善如果你也正想动手做同类的事情这套拆解应该已经帮你把大方向放清了。