
做这个 AI 编码代理起因是我在几个编程助手之间折腾了好一阵发现它们有一个共同的死穴只能处理代码文件和终端命令一旦任务里出现“打开系统设置、点几下界面、拖一个滑块”这种操作就直接罢工。我当时想与其等那些商业工具慢慢补上 GUI 自动化能力不如自己动手写一个免费开源的小工具让大模型拥有真正的“眼睛”和“手”能够操作图形界面同时通过 MCP 把外部工具链也串起来最后用单文件方式发布拿到哪台机器都能直接跑。项目本身不大但涉及的点不少GUI 操控怎么做得稳、MCP 怎么当个 host 接入现成生态、打包成单文件又怎么处理依赖地狱。这篇文章把设计思路、核心实现、实操过程和我踩过的坑全部整理出来希望能给想入坑 AI 代理开发的朋友一些参考。1. 这个项目到底解决了什么问题1.1 现有 AI 编码工具的痛点在哪里如果你用过市面上的 AI 编程助手你会发现它们的能力边界其实非常清楚改代码、查文档、跑命令行、生成 diff这些是强项但只要任务牵涉到图形界面操作基本就是空白。举个例子你想让 AI“打开项目的编译设置把优化等级从 O0 改成 O2然后点保存”普通编码助手顶多帮你改改配置文件里的参数至于打开设置面板、在 GUI 里找到那个下拉框、点击确认按钮这些它全都做不了。更深层的问题是现在很多开发工具本身就重度依赖图形界面比如 Xcode 的签名配置、Visual Studio 的 NuGet 包管理器、各种数据库客户端里的可视化查询界面。如果你希望一个编码代理能端到端地完成“打开项目 → 调整界面配置 → 跑构建 → 查看结果”这条完整链路它就是绕不开 GUI 这个环节的。我之所以决定自己写正是因为意识到这个缺口在一段时间内不会有现成方案补上——商业产品更愿意在纯代码领域堆能力GUI 自动化的坑太多、收益又不够立竿见影。这个项目的目标很简单做一个免费、开源、可在本地跑的 AI 编码代理它既能跟其他助手一样写代码、跑命令也能像人一样盯住屏幕、操作界面再通过 MCP 挂上数据库、文件系统这些外部能力把“想法”变成“真实操作”。它能覆盖的场景恰好是传统编码助手够不到的那一块。1.2 项目定位AI 编码代理到底是个什么角色很多人会把“AI 编码助手”和“AI 编码代理”混为一谈。我的理解是助手只能在你给它划好的轨道里回答问题或改文件代理不一样它有一个自主循环——先理解任务然后自己规划步骤调用工具去执行观察执行结果再决定下一步做什么直到任务完成为止。换句话说代理是一个“主动干活”的角色而不是一个被动的问答框。在这个项目里代理的工作模式大致是这样的你给它一个任务描述比如“把项目里所有的 TODO 注释整理成一份 Markdown 报告并用 GUI 打开给我看”它就拆解成几个子目标——扫描代码、收集 TODO、生成报告、用文本编辑器打开报告。前面几步靠命令和代码操作完成最后一步就需要 GUI 能力了。这个过程完全在本地运行模型 API 由你自己配置费用透明没有黑盒也不会把你整个项目的代码悄悄传到某个不透明的服务器上。目标用户其实很清晰一是做 AI 自动化和 RPA 方向研究的开发者二是希望给现有开发工作流增加“最后一公里”能力的工程师三是对大模型应用感兴趣、想学习 Agent 架构的爱好者。这个项目不追求商业级产品的打磨程度但胜在足够轻、足够开放你可以随便改、随便扩展。2. 整体架构与技术选型分析2.1 GUI 操控的三条技术路线与选择真正动手做 GUI 自动化时你会发现可选的技术路线其实就那么几条各自优劣势非常明显。我整理了一个对比表也是我最初调研时给自己列的技术路线实现方式优点缺点屏幕截图 像素定位截全屏CV/多模态模型识别目标坐标再模拟点击实现简单跨应用通用精度受分辨率/遮挡影响token 开销大OCR 坐标映射用 OCR 识别屏幕文字位置按文字点击适合文本密集的界面比像素级稳定OCR 有识别误差对纯图形按钮无效UI 自动化树通过系统无障碍接口拿控件树如 Windows UIA、macOS AX返回语义化元素操作精准稳定token 消耗低依赖平台 API部分程序不暴露控件Web 自动化Playwright/Puppeteer 操作浏览器 DOM对 Web 应用极其稳定只适用于浏览器不能操作原生桌面软件这个项目采用的是“UI 自动化树优先OCR 兜底”的混合方案。原因很实际我们面对的主要场景是 IDE、系统设置、文件对话框这些桌面程序它们大多数都暴露了无障碍接口可以从控件树里拿到按钮、输入框、菜单这些语义化元素。相比截图后让大模型去猜坐标UI 树的信息更干净也不占多少 token。只有在某些控件拿不到、或者目标窗口完全没有无障碍支持时我才会回退到截屏加 OCR让模型根据截图内容确认目标。还有一个很容易踩的坑是 DPI 缩放。如果显示器的缩放比例不是 100%截图坐标和系统逻辑坐标会不一致点击位置就会偏。解决方法是调进程的 DPI Awareness然后按缩放系数换算坐标。这个细节看起来小但如果不处理整个代理在大多数 Windows 电脑上都会点不准。2.2 为什么要把 MCP 做成第一公民MCPModel Context Protocol模型上下文协议现在已经成为模型工具生态的一种事实标准简单理解它就是一个给大模型挂外部工具的“统一接口”工具箱里的工具都遵循同一套 JSON-RPC 2.0 通信规范。你在代码里不需要为每个外部服务单独写适配器只要有一份 MCP 客户端去连接各个 MCP server工具描述和调用结果就会自动变成标准格式给到模型。我在这个项目里选择把 MCP 做成第一公民原因有两个。第一生态价值大。GitHub、数据库、Figma、浏览器、文件系统等等已经有大量的现成 MCP server我挂上它们就等于立刻让代理有了连接外部世界的能力省去了把时间花在重复封装 API 上。第二反向价值也很大。这个代理本身也可以作为一个 MCP server 暴露给其他客户端比如 Claude Desktop、自己写的其他 Agent这样别人也能通过标准协议调用我的 GUI 操作能力。MCP 本身是一个非常轻的协议核心就几个调用initialize 握手、tools/list 拿工具列表、tools/call 调用工具。代理在启动时读取一个 JSON 格式的 server 配置用 stdio 方式把本地命令跑起来然后通过这个子进程和 MCP server 通信。整个过程不需要网络不依赖某个云服务商隐私和可控性都在自己手里。2.3 单文件运行的技术取舍为什么一定要做成单文件我的出发点很朴素一个工具如果部署起来要先装 Python 环境、再拉一堆依赖、还要处理版本冲突绝大多数人就不会用了。打包成单个可执行文件之后用户体验就是“下载一个文件双击运行”这对一个开源小项目来说非常重要。我自己在分发东西时最烦的就是依赖地狱所以这个项目从一开始就把单文件当成硬性指标。技术选型上我最终用的是 Python 加 PyInstaller 的 --onefile 模式。为什么不用 Go 或 Rust因为 GUI 自动化这个领域最成熟的开源库几乎都在 Python 这边比如 pyautogui、mss、uiautomation而 MCP 官方的 Python SDK 也很完善生态可以直接吃。Go/Rust 虽然也能调系统 API但很多封装要自己重写做小项目时间成本太高。代价也很明确Python 打包出来的单文件体积偏大尤其把大模型 SDK 和一堆依赖打进去很容易到一两百 MB启动时还要解压到临时目录所以第一次启动明显慢一点个别杀毒软件还会对 PyInstaller 特征误报。这些我在后面“单文件打包的配置与体积优化”一节里会讲清楚怎么处理。3. 核心实现细节与实操要点3.1 让大模型“看见”屏幕截图、OCR 与 UI 树三合一整个 GUI 操控模块的入口是“观察屏幕”。它有三个数据来源全屏截图、OCR 文本、无障碍控件树。我封装了一个统一接口让上层不用关心具体数据格式。先说说最常用的无障碍控件树。在 Windows 上我用的是 uiautomation 这个库它背后是系统自带的 UI Automation 框架能拿到绝大多数原生程序的控件结构。比如你打开记事本控件树里会有一个 Document 控件里面是 Edit 区域窗口标题栏上还有 System Menu Bar 按钮等等。这些控件都带属性ControlType、Name、AutomationId、BoundingRectangle足够的上下文让模型判断该操作什么。但是控件树有一个烦人的问题数据量大。一个复杂的窗口可能有好几百个节点全量塞给模型不仅浪费 token还可能把模型“带偏”。所以我在送模型之前会做一轮过滤只保留可交互的控件比如按钮、菜单项、编辑框、复选框忽略掉静态文本和装饰性元素。这步过滤能砍掉大概 70% 的冗余信息模型的理解准确率反而更高。再看截图和 OCR 的兜底路径。在代码里我用 mss 做高效截图然后根据任务复杂度决定走哪条路如果 UI 树已经给了足够信息就直接用控件定位如果控件树拿不到或目标元素属于自绘控件比如某些游戏引擎界面、Electron 的特殊区域就把截图传给 OCR或者直接把压缩后的图片交给多模态模型识别。这里有个小经验传图给多模态模型时别传原始分辨率按宽度 1024 以内缩一下既能保住关键信息又能省大量 token。OCR 我一般用 Windows 自带的 OCR 引擎或 PaddleOCR具体看目标机器的环境。对于“窗口中某个按钮上的文字被识别出来然后点击对应坐标”这种任务稳定度比想象中高只要界面不是动画特别频繁的状态基本都能点中。3.2 MCP 工具注册与调用的具体流程MCP 模块在项目里承担“给代理接上外部世界”的职责。我设计了三层结构配置层、客户端层、桥接层。配置层是一份 JSON 文件里面列出要启动的 MCP server。配置格式大致是这样{ mcpServers: { filesystem: { command: npx, args: [-y, modelcontextprotocol/server-filesystem, /workspace], env: {} }, github: { command: docker, args: [run, --rm, -i, mcp/github], env: { GITHUB_TOKEN: xxx } } } }代理启动时会逐个读取这份配置用 subprocess 拉起子进程然后通过 stdio 和 MCP server 建立连接。这里值得注意的一点是MCP server 本身不要求必须是同一个语言写的它们只是通过标准输入输出来收发 JSON-RPC 消息所以你可以混用 Python、Node、Docker 里跑的 server只要协议正确就行。客户端层我用的是 MCP 官方 Python SDK处理了 initialize 握手、工具列表拉取、错误码这些底层细节。真正需要自己写的是桥接层把 MCP 的工具描述转换成大模型 API 认识的 function calling 格式。比如 filesystem server 里有一个read_file工具它的输入参数是一个文件路径我就把它包装成一个 name 为filesystem_read_file、parameters 里含path字段的 OpenAI 风格函数定义这样模型在规划阶段就能“看到”这些工具。调用过程也不复杂模型在回包里声明要调用某个 tool代理收到后把参数转交给 MCP 客户端客户端通过子进程发 tools/call拿到结果再回传给模型。整套流程里最容易出问题的是超时和异常处理因为 MCP server 可能是网络服务、可能是本地脚本任何一步都可能卡住。我在桥接层统一加了 timeout 和重试机制单个工具调用超时默认 60 秒超时后把错误信息塞回给模型让它决定要不要换一条路径。3.3 单文件打包的配置与体积优化PyInstaller 打包单文件本身不复杂真正的坑都在细节里。我的 .spec 配置大致如下# build.spec a Analysis( [main.py], pathex[.], binaries[], datas[(config.example.json, .)], hiddenimports[ mcp, mcp.server, mcp.client.stdio, uiautomation, mss, PIL, ], hookspath[], runtime_hooks[], excludes[tkinter], ) pyz PYZ(a.pure) exe EXE( pyz, a.scripts, a.binaries, a.datas, [], namecode_agent, debugFalse, stripFalse, upxTrue, consoleFalse, iconicon.ico, )几个关键点我单独说一下。第一hiddenimports 必须手动写全。MCP SDK 里有大量动态导入PyInstaller 的静态分析经常漏掉不写全的话打包出来的程序一运行就报ModuleNotFoundError而且只在客户机器上报你自己本机可能完全正常。第二upxTrue可以显著减小体积。UPX 是一个可执行文件压缩工具PyInstaller 可以直接调用它。但要注意个别杀毒软件对 UPX 压缩过的文件比较敏感如果遇到误报可以把 UPX 关掉重新打包体积大一点但更“干净”。第三consoleFalse会隐藏命令行窗口但代价是程序里如果有什么错误信息也看不到了。我的做法是加了一个--debug参数调试模式下重新编译成带 console 的版本方便看日志。发布版保持隐藏窗口避免弹出一堆黑框影响体验。体积优化方面我做了两步。第一步是排除无用库比如这个项目根本没用到 tkinter就干脆在 excludes 里删掉能省几 MB 而且少一些问题。第二步是精简依赖MCP 官方 SDK 的体积其实不小如果只用到 stdio transport就没必要把 SSE、WebSocket 相关的依赖模块全部带进来。这两步做完整个单文件从接近 200MB 降到了 120MB 左右对于一个塞了大模型 SDK 的工具来说还算能接受。4. 完整实操从空白目录到跑通一个场景4.1 项目初始化与依赖清单这个项目我用的是 Python 3.10理论上 3.8 以上都能跑但 3.10 对类型注解的支持更舒服。先创建一个虚拟环境然后把依赖装齐。python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install mcp mss pyautogui uiautomation openai anthropic pillow pyinstaller依赖清单里mcp是 MCP 官方 SDK负责和 MCP server 通信mss用来高效截屏pyautogui负责模拟鼠标键盘操作uiautomation读取 Windows 无障碍控件树openai和anthropic分别是两家大模型 API 的 SDK这个项目里你选一家配 API Key 就能用pillow用来做截图压缩和简单图像处理。整个项目的文件结构我保持得很简单核心就是三个 Python 文件加一个配置模板code_agent/ ├── main.py # 入口读取配置运行主循环 ├── screen.py # 截图、OCR、UI 树采集与过滤 ├── mcp_bridge.py # MCP 客户端封装和工具桥接 ├── executor.py # 把模型决策翻译成 GUI 动作和系统命令 ├── config.example.json # 配置模板 └── build.spec # PyInstaller 打包配置4.2 核心代码骨架讲解主循环是全项目最关键的部分它做的事情是拿到用户任务和模型多轮对话在每一轮里检查模型是否要求调用工具如果要求了就去执行然后把结果返回给模型循环往复直到模型给出完成信号。def run_agent(task: str): messages [{role: user, content: task}] tools load_global_tools() # 拼接 GUI 工具 MCP 工具 for step in range(MAX_STEPS): response llm.chat(messages, toolstools) messages.append(response) if response.finish_reason tool_calls: for call in response.tool_calls: result execute_tool(call.function.name, call.function.arguments) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) else: return response.content这里有一个我在调优中发现的点要给模型一个明确的工具使用规范。比如“点击某个按钮”之前必须先调用screen_get_uia_tree获取当前窗口的控件列表模型自己要判断目标按钮的坐标再调用gui_click。如果省掉这一步直接让模型猜坐标成功率会大幅下降。所以在系统提示词里我把步骤写成了固定的套路观察 → 决策 → 执行 → 再观察。GUI 工具的响应格式也很讲究。get_uia_tree返回的不是原始 XML而是一个经过过滤和精简的 JSON 数组每个元素就是{index, type, name, position}这种结构索引是快捷方式模型可以直接说“点击 index 5 的元素”省去理解一堆控件属性的开销。4.3 端到端实操案例让代理用 GUI 打开图片并另存光讲代码骨架有点抽象我拿一个实际跑通过的案例走一遍流程。任务很简单让代理找到本地一张图片用系统自带的画图程序打开再把尺寸调整一下并另存为 PNG。这个案例的特别之处在于它同时用到了 MCP 和 GUI 两条能力线。第一步代理先调用一个文件系统的 MCP 工具列出某个目录下的文件确认目标图片路径。这里的好处是文件扫描这种操作用代码实现更靠谱没必要通过 GUI 去翻文件夹。紧接着代理调用gui_launch_app启动画图程序等窗口出现后再调用uiautomation拿到菜单栏的控件树认出“文件”菜单的位置。等画图程序打开后代理开始操作用键盘热键 CtrlO 唤起“打开”对话框接着在对话框里用 UI 树找到文件名输入框填入图片路径按下回车。到这一步GUI 自动化的“打开文件”这段就走完了。接下来它用同样的思路从菜单栏里找到“调整大小”按钮点开之后在对话框里改成 50%确认。最后通过菜单把文件另存为 PNG 格式。执行过程中有一环让我印象深刻另存为对话框里的“保存类型”下拉框UI 树里拿到的是一个 ComboBox里面有一堆文件格式选项。模型一开始选错了格式但代理的执行器在点击之后会用 UI 树再拉一次当前对话框的状态发现类型还没变于是把错误信息反馈给模型模型重新读控件树找到正确选项再点一次。这种“观察-执行-验证”的闭环是代理能真正稳定工作的关键。纯命令行的代理做不到这一点因为它无法感知 GUI 里的状态变化。我把这个案例的每一段执行记录都打印到了日志里用户能看到代理每一步在做什么、为什么这么做透明度比黑盒调用来得高得多。这也是我认为代理类工具必须具备的素质它的每一步操作都可能出错如果用户不能介入修正这个工具就不可用。5. 常见问题与排查技巧实录5.1 高频问题速查表开发这个项目的过程中我踩过不少坑也收到过一些使用反馈。整理成速查表遇到类似问题可以直接对照解决。现象可能原因解决办法点击位置总是偏一点系统 DPI 缩放不是 100%调进程 DPI Awareness坐标乘缩放系数换算macOS 上控制不了窗口辅助功能权限未授权去系统设置 → 隐私与安全性 → 辅助功能里勾选应用UI 树里一堆乱码多语言界面或字体解析问题优先匹配 AutomationId而不是 NameMCP 工具调用直接超时子进程启动慢/网络服务慢调大 timeout或改成先探活再调用打包后运行就报模块缺失PyInstaller 漏了动态导入的包在 hiddenimports 里手动补全杀毒软件把单文件当木马PyInstaller 特征被误报关 UPX、加数字签名或提交白名单截屏得到的图片全黑安全软件/系统隐私限制了截图关闭相关权限或用 mss 指定显示器编号大模型总是不按步骤观察就点系统提示词约束不够强在提示词中明确“先观察后行动”必要时限制步骤顺序5.2 踩过最深的几个坑第一个坑是 UI 树数据过载。最开始我直接把整个窗口的控件树传给模型结果上下文一下子就爆了而且模型经常找错按钮。后来加了过滤和排序只保留可交互控件并且把窗口标题、位置这类元信息单独摘出来模型确实变“聪明”了。这个经验的核心是给模型的信息不是越多越好而是越有决策价值越好。有些开发者喜欢把所有状态都塞给模型这其实是浪费 token 还增加噪声的做法。第二个坑是窗口遮挡。代理在截屏时如果窗口被其他窗口盖住一多半UI 树仍然能拿到完整元素但像素级截屏就会得到残缺画面。如果这时候走 OCR 兜底就很容易识别错。我的解决办法是执行点击动作之前先用SetForegroundWindow或 macOS 的activate接口把目标窗口带到前台等它激活了再截屏。这个“激活窗口再观察”的顺序基本上能解决 90% 的遮挡问题。第三个坑是高 DPI 环境下的坐标换算。这个问题我在前面提过但值得再强调一遍。Windows 系统里如果你的代码拿到的控件坐标是“物理像素”而 pyautogui 默认用的是“逻辑像素”两者之间就有差值。你需要先调用ctypes.windll.shcore.SetProcessDpiAwareness(1)之类的接口再统一坐标体系。我最初的版本没有处理这个导致 4K 屏加 150% 缩放的机器上代理点击的准确率惨不忍睹。这个问题排查了很久才发现归根结底是因为我自己一直用 100% 缩放的显示器调试根本没有暴露出来。第四个坑是 MCP 子进程的编码。在 Windows 上有些 MCP server 启动后输出的不是 UTF-8 编码或者带 BOM 头Python 的 subprocess 在解析 stdio 时就会报错。处理方式是在启动参数里强制设置环境变量PYTHONUTF81并且用errorsreplace做解码兜底。这种问题非常隐蔽但一旦遇到整个代理的 MCP 能力就瘫痪了。6. 后续扩展建议与个人体会6.1 这个架构还能往哪里延伸项目做到现在这个程度其实已经是一个很顺手的骨架了。以我目前的规划接下来有几个明确的方向。第一个方向是接入视觉语言模型做屏幕理解。现在的方案以 UI 树为主但面对没有无障碍接口的程序还是有点吃力。如果能本地跑一个 Qwen2.5-VL 这类小参数视觉模型只用来做“截图里的按钮在哪”这种识别任务不参与主对话就能在不消耗主模型太多 token 的情况下补齐最后一环。第二个方向是把代理反过来暴露成 MCP server。这个项目目前是 MCP host去连接别人家的 server但它也可以被别人当作 server 来用。比如你在 Claude Desktop 里给它接上告诉它“帮我打开本地的某某工程跑一下里面的脚本”Claude 就能通过 MCP 调用这个代理进而操作你电脑上的 GUI 程序。这个玩法很有意思等于给自己常用的编程助手也接上了一双“手”。第三个方向是任务记忆和长期规划。现在的代理还是“一拳一个目标”式的执行完一张图片处理任务就结束不会记住你上次的设置偏好。如果把用户历史操作、界面偏好、常用工具配置持久化到本地再让模型在规划时参考这些记忆它就会越用越顺手。这其实也是 Agent 类产品从“能用”走向“好用”的关键一步。6.2 做这个项目的一些真实感受写代码本身不难难的是把异常路径处理得足够稳。我在调试过程中发现AI 代理的最大问题通常不是模型不够聪明而是工具链不够可靠。一次点击失败、一次 MCP 超时、一次坐标换算错误就足以让整个任务在中途崩掉。模型虽然有纠错能力但它能纠正的次数是有限的前提是错误信息能准确返回到它手里。所以在这个项目里我对“执行反馈”的重视程度远高于对“规划能力”的重视程度——规划错了模型自己会改执行错了反馈不全面模型就只能在原地打转。这个工具本身是免费开源的我也没打算靠它盈利。当时做的时候的一个朴素想法是如果有一天有人想给自己的开发流程加一双“看得见摸得着的手”能有一个开箱即用的起点那这个项目的价值就实现了。现在它已经能稳定跑通不少场景如果你也感兴趣拉一份代码下来装好依赖按着这篇文章里的思路把它跑起来再顺手加几个自己需要的工具说不定它很快就会变成你日常开发里离不开的那个小助手。