做个东西的冲动通常来自一次卡住的时刻。去年我用一款挺主流的 AI 编码助手改项目配置模型很快就分析出问题、给出了修改方案但改完后死活没法继续——因为它需要操作的下一步是一个图形弹窗里的按钮而模型只能改文件碰不到界面。那一瞬间我意识到编码代理的真正瓶颈往往不是模型推理而是它有没有“手”。这也是我后来坚持做这个免费 AI 编码代理的直接原因它不只写代码还能直接操控 GUI同时支持 MCPModel Context Protocol接入外部工具整个项目收敛成一个单文件拿到任何一台装了 Python 的机器上都能跑。这个代理本质上是一个把“文件操作 桌面操作 外部工具调用”统一起来的自治循环模型看屏幕、看文件、看终端输出然后决定下一步动作动完再观察结果。你可以让它“帮我把这个 Markdown 文档转成 PDF 并放到桌面”也可以让它“打开某个 GUI 软件完成一项重复设置”。免费、单文件、能摸界面、能接 MCP这四个特性凑在一起市面上的现成方案里我还没找到一个完全对应的所以只能自己写了。这篇文章就把我的设计思路、实现细节、踩过的坑以及一套真实演示完整摊开讲希望能给也在琢磨 AI Agent 落地的人一点参考。1. 从“只写代码”到“会摸软件”需求的起点与产品边界1.1 编码代理的痛点模型的回答不能替代操作如果用一句话说清我这个代理和普通 AI 编程助手的差别那就是普通助手负责“出主意”我这个代理还负责“动手”。典型 AI 编程助手的闭环是“问题 - 代码 diff”它产出的是一个补丁最终改文件、跑测试、看效果的环节还得人来。这没毛病但现实中软件开发流程有一大堆操作根本不属于“代码修改”范畴装依赖可能要点安装包界面调数据库可能要操作图形客户端看 CI 产物可能要打开一个 GUI 工具验证。这些环节一旦出现自动化的链条就会断最终还是要人回到电脑前手动处理。所以我把产品定位从“编码补全”挪到了“编码代理”。它拥有对代码库的读写能力也能执行终端命令但最大的变化是给它加上了视觉和手持能力截屏、识别界面、移动鼠标、点击、键入。这意味着它可以把过去断掉的环节重新接上例如下载一个安装包、通过 GUI 向导完成安装、打开编辑器写入配置、再跑一遍命令行验证结果。1.2 为什么把“GUI 操控”和“MCP”做进同一个代理更早的时候GUI 自动化如 RPA和 MCP 是两条独立的技术脉络RPA 强调模拟人的桌面操作MCP 强调用标准协议把工具能力暴露给大模型。按理说分开用也行但实际做代理时你会发现二者根本是互补关系。GUI 操作适合“没有代码接口、只能人肉点”的场景但它每一步都慢、容易受界面变动影响MCP 适合“有明确定义的接口”的场景效率高、可靠但前提是对方提供了 MCP server。同一个任务往往两头都要走从官网下载一个开源编译器官网可能没有 MCP 接口得用浏览器 GUI 操作下完以后要处理的压缩包、安装脚本、环境变量这些又完全可以走 MCP 的文件系统和命令工具。一个成熟的编码代理理应在两种模式之间自由切换所以我在设计的第一天就把 MCP 客户端和支持鼠标键盘的 GUI 执行器并排放进了同一个循环里。1.3 “免费 单文件”作为产品底线的现实原因为什么不让用户装一个大型框架因为代理类工具的使用场景太碎片化你可能在公司电脑上用也可能在家里的旧笔记本上跑环境可能是 Windows也可能是 macOS 或者 Linux你可能只想临时跑一个小任务不想为此引入一整套运行时和依赖树。单文件方案能解决这类碎片化问题。单个.py文件意味着分发简单、审查简单、迁移简单更重要的是它倒逼我把架构做得清爽——如果一万多行代码连自己都理不清模块边界那用户更不可能信任它。至于免费一方面是因为它借用了各家 LLM API 的能力工具本身没有理由收费另一方面我觉得这种“代理骨架”类项目本来就应该是开放的让大家能检查它在本地到底做了什么这对一个能操作你桌面的程序来说非常关键。2. “单文件”的工程取舍入口、依赖与内部组织方式2.1 单文件不是把一万行代码堆在一起很多第一次看到这个项目的人会问单文件那代码是不是一大坨没法维护其实不是。我在文件内部按“区段”组织用显眼的注释块把不同子系统分隔开结构大致如下# 1. 基础工具日志、配置、并发 # 2. 感知层截屏、OCR、图像压缩 # 3. 执行层键盘鼠标、进程、文件 # 4. 模型层LLM 调用、提示词编排 # 5. MCP 客户端JSON-RPC 通信 # 6. 核心循环感知-决策-执行-观察 # 7. 交互入口CLI / Web UI 这就像一个人家里没有单独建储藏间但墙面做了足够的收纳分区找东西依然很快。实际开发中我还会通过类型注解和 docstring 保证任意一个区段都能独立读懂。单文件在 Python 里还有一个隐藏好处不会有代码包导入路径的问题也不会有多个版本同时存在于 site-packages 的混乱。2.2 依赖策略轻量引入 启动时自检全部依赖如果都塞进一个文件肯定不现实所以我选了“最小依赖集合 启动时自检”的策略。核心依赖只有四类多模态模型 SDKOpenAI/Anthropic 二选一、截图库mss、桌面控制库pyautogui、图像处理库Pillow。这些加起来不过百来兆但能力覆盖面已经够了。启动时我会做一次依赖扫描缺哪个就打印一条明确的安装指令而不是抛一个晦涩的 ModuleNotFoundError。比如python agent.py --task 打开记事本输入一段文字 # 自动检查依赖缺失时提示 # 缺少 pyautogui请执行pip install pyautogui这种方式虽然不如“真正的单文件打包”干净但比动不动就要用户配一个虚拟环境、装一堆 SDK 再跑 demo 要友好得多。等哪天有空了我打算用 PyInstaller 把它再收成一个原生可执行文件那就是彻彻底底的单文件了。2.3 三种运行模式对话、任务、服务单文件并不意味着只有一种入口。我根据实际使用场景把它分成三种运行模式第一种是对话模式。直接python agent.py进入 REPL你可以跟它连续对话它会反复调用工具、汇报中间结果。适合调试代理行为、逐步尝试新任务。第二种是任务模式。python agent.py --task xxx一条命令结束走人适合脚本化调用和批处理。任务结束后它会输出一份简短的执行报告包括“我做了什么、每步调用了什么工具、最终状态如何”。第三种是服务模式。python agent.py --serve会起一个本地 HTTP 服务前端是个小页面可以在浏览器里观察代理的实时截屏和动作串。这个模式对我自己调试 GUI 自动化特别有用因为我能直接看到它“看见”了什么、下一步打算点哪里。3. 看着屏幕干活GUI 操控链路的实现与可靠性设计3.1 感知层截图、压缩、视觉模型出结构化动作GUI 操控的第一步是让模型“看见”屏幕。这里最直接的做法是截全屏把截图直接送给多模态模型让它基于图像推理出目标坐标和动作。全屏截图虽然信息完整但 token 消耗巨大、推理速度很慢。所以我做了三层简化第一层按窗口区域截图。模型需要操作某个应用时我先通过窗口管理 API 拿到目标窗口的边界框只截那一块把无关区域全部剔除。第二层JPEG 压缩与缩略化。默认把截图缩放到宽度 1280JPEG 质量压到 60单张图体积能控制在 200KB 左右一次推理的视觉 token 成本大约只有原始 PNG 的三分之一到二分之一。第三层必要时叠加 OCR 文本信息。模型对不规则中文界面元素偶尔会认不准确我接入了一个本地 OCR 步骤把界面里的关键文本提取出来和截图一起送进上下文让模型“看字 看图”双重校验。模型输出不是自由文本而是严格 JSON{ reasoning: 窗口标题栏有“下载”按钮位于屏幕右下角区域, action: click, target: {x: 1543, y: 862}, parameters: {double_click: false} }我在提示词里规定不是所有坐标都需要精确到像素模型可以先给一个粗略目标执行层会做“按钮级校正”比如 OCR 识别出按钮中心点模型只需说“点这个按钮”。这样既保留了模型的泛化能力又减轻了它对坐标的精确记忆负担。3.2 执行层从坐标到鼠标键盘的落地方式执行层相对粗暴我用 pyautogui 作为主后端因为它跨平台、API 简单、能覆盖鼠标移动、点击、拖拽、滚轮和键盘输入。但“简单”是有代价的比如 GUI 缩放DPI问题就折腾了我一整天Windows 下如果系统缩放是 150%截图里的坐标是物理像素而 pyautogui 操作的是逻辑坐标两者存在 1.5 倍的比例偏差。解决办法是启动时读取系统缩放参数对所有坐标做一次统一映射import tkinter as tk root tk.Tk() scale root.winfo_fpixels(1i) / 96.0 # Windows DPI 缩放因子 logical_x physical_x / scale做完映射之后点击精度明显提升误点率降了一大截。不过这同时也说明了一个问题GUI 自动化上任何一个“想当然”的细节都会在现场给你上一课。3.3 可靠性设计状态对比与二次确认机制任何做过 GUI 自动化的人都知道界面自动化的失败从来不是“偶尔失败”而是“经常莫名失败”。最典型的场景是模型想点“确定”按钮但界面上有两个长得差不多的按钮它认错位置了。单纯的“点一下”并没有自纠能力。我采取的方案是动作前预判 动作后对比。执行完一个动作之后系统会立刻重新截屏与动作前画面做对比。如果发现画面完全没变化说明点击没有命中任何交互元素或者出现了未预期的弹窗代理会自动把差异提取出来再交给模型分析如果画面没变化它知道“刚才那一步可能没点上”会微调坐标再试一次如果出现了意外弹窗它会先分析弹窗内容决定是关闭还是继续连续失败超过两次它就停下向你请求指令而不是自作主张乱试。另外我还设计了一个“高代价操作确认”开关当某个动作的代价较高例如删除文件、提交代码、安装软件代理默认不会直接执行而是先把打算做的事用一句话告诉你等你确认。这种谨慎对普通文本自动编辑可能多余但一旦它开始摸你的鼠标和键盘“确认”就不是可选项而是必需品。4. 接入 MCP把外部工具变成代理的“外挂手脚”4.1 边写边想MCP 究竟解决了什么MCP 全称 Model Context Protocol最初由 Anthropic 提出并开源本质上是一套让大模型应用动态发现和调用外部工具的协议。你可以把 MCP 想象成“USB-C 接口”之于外设以前每个 AI 助手要接一个新工具都得写一套私有集成有了 MCP只要工具提供方按同一个协议提供接口AI 就能即插即用。在实际使用中MCP 给我带来了两个立竿见影的改变。一是打通了工作流过去“让代理读一个 SQLite 数据库”“让代理搜索本地文件”这类能力都需要我为它单独开发现在只要配置好对应的 MCP server它就能自动列出可用工具并发起调用。二是和 GUI 操作形成互补GUI 负责“打开安装包并勾选选项”MCP 负责“读配置文件并修改参数”二者叠加自动化链条才完整。4.2 我实现的 MCP 客户端核心逻辑附代码MCP 协议描述起来很高大上落地其实就是一个基于 JSON-RPC 2.0、通过标准输入输出stdio或者 HTTP 传输的通信过程。我没用官方 SDK直接写了一个轻量客户端核心只有几十行import json, asyncio class MCPClient: def __init__(self): self._id 0 self.proc None async def connect(self, command: list[str]): self.proc await asyncio.create_subprocess_exec( *command, stdinasyncio.subprocess.PIPE, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE, ) await self._request(initialize, { protocolVersion: 2025-03-26, capabilities: {} }) async def list_tools(self): return await self._request(tools/list, {}) async def call_tool(self, name: str, arguments: dict): return await self._request(tools/call, { name: name, arguments: arguments }) async def _request(self, method: str, params: dict): self._id 1 payload { jsonrpc: 2.0, id: self._id, method: method, params: params } self.proc.stdin.write(json.dumps(payload).encode() b\n) await self.proc.stdin.drain() line await asyncio.wait_for(self.proc.stdout.readline(), timeout30) return json.loads(line)核心方法是四个connect负责拉起子进程并完成初始化握手list_tools让模型知道这个 server 能做什么call_tool真正调起工具_request是公共的 JSON-RPC 收发层。整个过程只依赖 Python 标准库的asyncio没有引入任何第三方依赖这在单文件项目里非常关键——少一个依赖用户部署时就会少一次出错机会。4.3 接入后的真实效果与选型建议我在项目里默认配置了三个实用的 MCP server文件系统服务器能对指定目录做读写比 agent 自己拼路径安全、命令执行服务器负责跑命令行并回传输出、以及一个浏览器自动化服务器负责打开 URL、抓取页面内容。这套组合的实际效果是代理拿到一个“把远端某个配置仓库拉下来并按说明修改”的任务时可以先通过浏览器 MCP 读取说明页面再通过命令 MCP 执行 git clone然后通过文件系统 MCP 精确改动文件最后还能跑 git diff 给我展示改动差异。全程 GUI 只需要在“确认执行命令”时露面其余都在文本层面高速完成效率和稳定性都比纯 GUI 操作高出一个量级。对想接入 MCP 的开发者我的建议是先盘点自己的项目里“有哪些确定性接口”和“有哪些只能靠肉眼识别”的操作尽量把前者交给 MCP、后者留给 GUI而不是什么事情都想让模型用鼠标去试。混合调度才是生产力最大化的关键。5. 实战演示让代理自己完成一次 GUI 安装流程5.1 任务的拆解与前置条件为了展示真实效果我在一台干净的 Windows 虚拟机上跑了一次完整流程让代理“下载并安装一个开源 Markdown 编辑器然后用它打开一个示例文档”。任务难点在于下载页面、安装向导、界面菜单全都是 GUI没有任何标准 API 可用全部依赖视觉识别和鼠标键盘操作。前置条件如下已准备好 Python 3.11 agent.py 必要的依赖配置了一个多模态视觉模型 API Key要求代理每一步操作后都汇报当前界面状态。这个任务被代理自动拆成了五个子目标打开浏览器、找到下载链接、触发下载、运行安装向导、启动应用并打开文档。每个子目标内部又会有若干次“截屏 - 推理 - 动作 - 再截屏”的循环。5.2 逐步推进从下载到安装完成第一阶段是打开浏览器并定位下载页。我给了它项目的官网地址它直接通过“浏览器自动化 MCP”打开了页面。但页面上的下载按钮并不醒目第一次截屏后模型误把“文档”按钮当作下载入口点了下去随后通过“动作后画面对比”发现跳到了文档页。它立刻回退重新截屏并 OCR 了页面上的按钮文字这次识别到了“Download for Windows”顺利进入下载页。第二阶段是等待下载并定位安装包。下载完成之后代理通过窗口标题变化判断“浏览器下载栏已出现”然后用文件系统 MCP 扫描Downloads目录找到新出现的.exe文件。到这里它执行了一个 GUI 动作双击该文件。第三阶段是安装向导流程。安装程序弹出了安全提示窗口显示“是否允许此应用更改设备”代理的视觉模型识别到了“是”按钮并点击。随后安装向导出现界面有“下一步”“选项”“安装”等多层页面。代理的策略是每屏截一张图找到最醒目的“下一步”按钮点击遇到“安装路径”页面它没有修改默认路径直接点“继续”最后一屏出现“安装”按钮点击后等待进度条走完。第四阶段是验收。安装完成后代理通过“开始菜单搜索”找到刚装的编辑器并启动之后用“新建文件”选项创建了一个demo.md写入一段测试文本并保存到桌面。到这里整个无人值守流程顺利结束耗时大约 20 分钟。5.3 这次演示暴露的三个问题一次完美 Demo 是不可能的这次的教训很典型第一安装向导的“界面语言”影响了识别准确率。中文安装界面里“为了在计算机上安装请选择下一步”这类说明文字很长模型有时会把中间某个链接误认为按钮。后来我在视觉提示词中增加了“只选择窗口最底部通常位于右下方的主操作按钮”的约束误点率才有了明显下降。第二初始截图全屏时模型会迷失重点。第一次它盯着整个桌面看找下载按钮时把任务栏上的通知图标都分析了一遍浪费了很多推理时间。后来我强制要求它“先根据 OCR 文本定位窗口区域再局部截取”感知效率提升了差不多两倍。第三安装过程不可回滚。安装程序一旦点错可能会装错目录或者多装组件。虽然代理最后能完成但过程里它确实有一次多点了“安装可选组件”导致安装耗时变长。这提醒我GUI 自动化的“验证”必须前置到动作之前而不是等到出错了才补救。6. 真实的边界免费方案能做的事与踩过的坑6.1 高频踩坑对照表我连续迭代了三个月把遇到的高频问题整理了一张表希望对打算做类似项目的朋友有点帮助问题表现根因我的解决办法DPI 缩放导致点击偏移明明看到按钮在这里却点到旁边截图坐标与鼠标逻辑坐标映射不对启动时读取系统缩放因子统一换算截图 token 消耗过大跑一个任务要花几块钱每轮都送全屏高清图窗口区域截图 JPEG 压缩 缩略图模型把相似按钮认错点击后弹出意料之外的页面视觉模型对文字和按钮位置不敏感叠加 OCR 文本信息到上下文GUI 点击无响应动作后画面没变化点到的是背景/无效区域动作后重截图对比失败则微调坐标重试MCP server 崩溃任务执行到一半工具断开子进程异常退出对 MCP 调用做超时 自动重连长任务上下文溢出跑十分钟之后模型开始发呆每轮截图和文本累计过多定期对历史观察做摘要压缩6.2 防误操作机制GUI 代理最需要的一层保险能做 GUI 自动化的代理天然拥有比“改文件”高得多的操作权限因此也必须有更严格的防误操作机制。我的设计里有一套三角色策略第一默认只读。代理刚启动时处于只读模式可以截屏、可以读文件、可以列目录但不能修改任何东西。只有当用户显式下发“修改类任务”时它才获得写权限。第二高危险操作二次确认。删除文件、执行系统级命令、安装软件、修改关键配置文件这类操作默认要求用户确认即使任务已经下发了也必须弹出一个确认窗口。第三操作日志可审计。每一次 GUI 点击、每一次 MCP 调用、每一次文件写入都会记录到本地日志。出了任何意外用户可以立刻回溯到“它当时到底做了什么”。这套机制一开始被我觉得“太啰嗦”但实际用了才发现自动化的价值在于替你干活而不是替你惹祸多一层确认反而让整个流程更容易被信任。6.3 关于后续的一些个人打算这个项目现在还在持续迭代。我下一步的计划是把 GUI 操作部分做成更原子化的“技能包”例如“点击黄色高亮按钮”“在输入框键入文本”“滚动到底部”让模型每次只调用一个原子技能而不是直接输出原始坐标这样技能的内部实现可以不断优化而模型不需要跟着学习细节。最后再分享一个我实际使用中的体会AI 代理这种工具最让人上头的时刻不是它一次跑通的时候而是它第一次犯错然后自己纠正过来的时候。那种感觉不像是在用一个自动化脚本倒像是在带一个上手很快的新同事。前提是你要给它足够的边界和护栏并且始终保留“你点头它才动手”的最终控制权。