ARTICLE DETAIL

资讯详情

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

免费开源AI编码代理:单文件实现GUI操控与MCP接入

免费开源AI编码代理:单文件实现GUI操控与MCP接入 做这个项目的起因其实挺直白我手头有几个需要频繁“看界面、点按钮”的活儿传统的编码助手又只会往终端里扔代码实在不想再当人肉指挥。于是断断续续折腾了一个免费开源的 AI 编码代理支持直接操控 GUI 界面也支持通过 MCP 协议接入外部工具而且整个项目压缩成单个文件运行。这篇文章不吹不黑把我踩过的坑、设计取舍、实操过程全部摆出来适合想自己折腾 AI Agent、又不想一上来就背一堆框架依赖的人看。先交代一下它能干的事你给它一个自然语言目标比如“打开这个表格软件把第三列数据导出成 CSV”它会自己截图看屏幕、移动鼠标、点击窗口、输入文本也能调用 MCP 服务器暴露出来的工具接口比如读取本地文件、查询数据库、调用内部 API。整个过程不需要你手写一套 RPA 流程也不需要预先配置复杂的图形识别管线。因为它本质是一个极简的 Agent 循环模型判断下一步动作执行器把动作落到真实系统再把结果反馈回去继续决策。1. 项目为什么长这样一次被“只长嘴”逼出来的重构最开始我没打算做单文件更没打算碰 GUI。那时我用的方案是“Agent 负责写 Python 代码我定期把代码塞进解释器跑”听起来很自动化实际跑起来三天两头翻车。写代码时模型自信满满一执行就报错报错信息贴回去又是一轮往返。而且在 Windows 上经常遇到路径编码、权限弹窗、程序卡住这类破事模型根本看不见只能靠我手动描述。几次下来我意识到编码代理缺的不是更聪明的写码能力而是一双能看见屏幕、能落在真实窗口上的手。1.1 市面上的编码助手缺了什么现在市面上的 AI 编码助手大致分两类。一类是 IDE 插件比如通义灵码这类擅长补全、解释、重构代码跟我的需求完全不搭——它活在编辑器里管不了编辑器外面的世界。另一类是命令行 Agent能把自然语言拆成 shell 命令、读文件、改文件但同样困在终端里。一旦任务涉及到桌面软件、图形配置界面、网页里的拖拽操作这些工具就集体失明。有意思的是社区里早就有不少半成品尝试。比如有人给 IDA Pro 写 MCP 插件让模型能直接读取反汇编结果也有人试图把 x32dbg 的调试能力变成 MCP 工具方便大模型参与逆向分析还有人问“Codex 怎么接入 Figma 的 MCP”本质上都是想给模型开一扇通往“模型原本看不到的领域”的门。这些需求集中爆发但缺少一个通用的壳子把 GUI 和 MCP 两件事统一接起来。我的做法是反过来不追求 IDE 里的深度融合而是做一个独立于环境的代理进程。它像一个人坐在机位前面眼睛是截图手是鼠标键盘耳朵是 MCP 工具返回的数据大脑是大模型 API。这样无论你面前是 STM32 的图形配置工具、SAP GUI 客户端、调试器还是某个老旧的管理后台代理都能用同一套逻辑去应付。1.2 单文件运行从部署焦虑里做出的决定单文件不是噱头是被部署折腾烦了之后的理性选择。做这类工具最容易陷入的坑是写完功能后开始堆依赖要用 GUI 定位库、要用 MCP SDK、要用模型 SDK、还要处理配置文件和虚拟环境。等你把环境搭好用户光安装就耗掉半小时劝退率极高。我给自己定了一条硬规矩最终交付物必须是一个可执行的单文件拷贝到目标机器上双击就能跑。哪怕意味着要牺牲一些外部库的便利也要保证运行时的独立性。这个决定直接影响了后面的技术选型比如 GUI 控件识别我优先选纯 Python 实现的方案MCP 客户端尽量自己写轻量实现模型调用只保留 HTTP 接口而不是捆绑某个大厂 SDK。在 Windows 上用 PyInstaller 打包单文件其实很成熟麻烦的部分在后面资源文件路径、临时目录、多进程启动方式这些都得专门适配。后面的章节我会把具体处理方式写出来。1.3 GUI 操作让 Agent 真正接管完整流程很多人听到“AI 操控 GUI”第一反应是用视觉大模型识别 UI 元素然后输出坐标。这个思路能跑通 demo但离“能用”差得远。视觉识别在简单界面上很准一旦遇到深色主题、缩放布局、弹窗遮挡模型给出的坐标经常偏出目标几个像素。我的方案是截图 像素特征定位 精确鼠标/键盘输入。具体来说每次决策前代理截取当前主屏幕的 PNG交给视觉模型描述界面结构。模型返回一个高层的意图比如“点击右上角的保存按钮”而不是直接给坐标。代理再用基于图像匹配的定位器在截图里搜索目标按钮的特征区域换算成真实屏幕坐标。执行鼠标点击或键盘输入后再截一张图作为反馈给模型。这算是视觉大模型和传统图像处理的折中。纯靠视觉模型的坐标输出不稳定纯靠图像模板匹配又识别不了复杂界面两者结合后成功率显著提升。1.4 MCP 接入生态顺势扩展MCP 全称是 Model Context Protocol直白点说就是大模型工具调用的统一接口协议。它解决了 AI 工具生态里一个很痛的问题每个 Agent 都要单独适配每个工具接口标准五花八门维护成本成倍增长。MCP 像 USB-C 一样定义了工具列表、参数结构、调用方式和结果格式谁符合这个标准谁就能被 Agent 直接使用。这个项目接入 MCP 的直接价值是我不需要为每个数据源写专属插件。比如团队内部有一个自建的 Dify 流程暴露了浏览器 MCP 服务有人写了 Nuclei GUI 扫描器还有人给 CMake GUI 封装了工具接口这些只要是 MCP 协议我的代理就能调用。用“搜索热词”看更直观CherryStudio 有人做 MCP 工具流式输出内容到文件Codex 接入 Figma MCP 时碰到授权问题——大家都在想办法打通“模型 ↔ 专业软件”这条链路而 MCP 成了共识性的连接层。2. 核心细节解析GUI 定位、MCP 客户端和 Agent 循环这一章深入讲三个核心模块的设计。不是贴一堆源码草草了事而是把每个模块为什么这么设计、中间有哪些坑交代清楚。2.1 GUI 操控的核心机制截图、模板匹配与坐标落点首先明确一个概念GUI 操控不等于简单的“截屏–给模型–模型返回坐标”。如果真这么干你会遇到三层问题。第一层是屏幕坐标系的换算。不同机器的 DPI 缩放不同你截到的 1920×1080 图片物理屏幕可能是 2560×1440 且缩放 150%。如果模型直接在截图上给坐标落点必然偏。解决方法是维护一个 DPI 缩放因子把截图坐标除以缩放比例再换算成真实屏幕坐标。代码上可以用 ctypes 读取 Windows 的缩放参数也可以用 pyautogui 的 size 对比实际分辨率推算。第二层是鼠标动作的可靠性。pyautogui 这类库的 click 本质是瞬间按下抬起有些老旧 GUI 程序对点击间隔敏感识别不了这么快的操作。我后来会故意在两段动作之间加 30ms 到 80ms 的延时。别小看这个细节SAP GUI 这类重量级客户端就吃这一套。第三层是定位策略。我用的是“两步定位”先让视觉模型给出目标区域的语义描述再通过图像匹配在这个区域内找具体特征点。匹配算法用 OpenCV 的模板匹配就能满足大部分需求不需要上 SIFT 或深度学习。如果模板匹配分数低于阈值就退一步把截图整体重新丢给模型让模型重新判断界面是否发生了变化。这套策略在普通桌面软件上的成功率能到八成以上剩下的靠 Agent 循环自纠错兜底。2.2 MCP 客户端轻量实现但要兼容标准协议MCP 的标准传输方式有 stdio 和 HTTP/SSE 两种。stdio 适合本地工具比如启动一个子进程通过标准输入输出通信HTTP 适合远程服务比如接入局域网内另一台机器上的工具。我的实现两者都支持但本地优先理由很简单安全性和稳定性更好不会因为网络波动让 Agent 中途失联。MCP 的通信本质是 JSON-RPC 2.0。客户端启动时先发送initialize请求服务器返回协议版本和工具列表然后客户端发送tools/list获取工具清单之后每次调用就发tools/call。我自己维护了一个很薄的 MCP 客户端类不依赖官方 SDK。原因一是官方 SDK 体积大、依赖多打包进单文件要费不少功夫二是协议本身不复杂纯手动实现可控性更强。不过轻量实现有个代价对 MCP 服务器端各类实现偏差的容错能力变差。有些服务器会在initialize阶段就返回非零错误码有些在tools/call返回的时候把结果包装成不同结构还有的用 SSE 但没实现标准心跳。针对这些情况我的客户端里加了一个“宽容模式”解析结果时优先取result.content取不到再遍历整个响应对象找文本字段。虽然粗暴但兼容性显著提升。2.3 Agent 循环怎么让模型“看得见”错误并自我修正核心 loop 大概是这样接收用户目标作为系统提示词的一部分固定下来。截取屏幕读取当前 GUI 状态同时拉取 MCP 工具列表。把 GUI 状态摘要、工具描述、上一次动作的结果一起拼进请求。模型返回结构化动作例如{type: click, target: 保存按钮, description: 点击保存按钮}。代理执行动作截屏验证如果模型判断任务完成就结束否则继续循环。设置最大循环次数我默认 15 轮超过即终止并输出当前进度。为了让模型更容易改正每一步我都把“上一步发生了什么”写进上下文包括鼠标点击的坐标、键盘输入的字符串、以及执行后截图的简短描述。这相当于给模型装了一面回头镜点错了能看到输入错了能意识到。我试过让模型直接输出 Python 代码来控制鼠标这个方案的问题是无法结构化约束输出模型偶尔会在代码里写出不存在的 API。相比之下约束成 JSON 动作再用本地执行器翻译成真正的库调用稳定得多。3. 实操搭建从零到能用的完整过程这一章讲实际搭建过程按时间顺序记录了从环境准备、模块开发、打包到最终验收的全过程。你可以把它当作一份可复现的步骤清单。3.1 环境与技术栈选择我的主开发机是 Windows 11Python 版本是 3.11。之所以不用 3.12是因为 PyInstaller 对 3.12 的某些 hook 还不够成熟单文件打包容易出幺蛾子。最终技术栈如下模块选型说明截图PIL.ImageGrab跨平台、轻量截屏速度在 Windows 上最快鼠标键盘pyautogui跨平台基础输入点击、拖拽、滚轮都有图像匹配opencv-python-headless模板匹配速度快体积比完整版小不少模型调用OpenAI 兼容 HTTP 接口不绑定特定厂商 SDK兼容本地模型服务MCP 客户端自研轻量 JSON-RPC 客户端支持 stdio 和 HTTP 两种传输打包PyInstaller单文件模式配合自定义 hook这里有个容易被忽略的选择模型调用为什么不用官方 SDK因为不少本地推理框架、网关服务都提供 OpenAI 兼容的/v1/chat/completions接口。写了统一封装后今天用云端模型明天切本地模型只需要改一个 base_url 和 API key。3.2 核心代码片段与实现说明Agent 主循环的骨架长这样我做了精简去掉错误处理保留核心逻辑import json, base64, io, time from PIL import ImageGrab import pyautogui import cv2 import numpy as np class GUIAgent: def __init__(self, llm_client, dpi_scale1.0): self.llm llm_client self.dpi_scale dpi_scale self.max_steps 15 def capture_screen(self): img ImageGrab.grab() return img def template_locate(self, screen_png, target_desc, regionNone): # 在截图中搜索目标按钮特征 screen cv2.imdecode(np.frombuffer(screen_png, np.uint8), 1) # 假设 target_desc 是模板图片路径真实项目中模板从视觉模型描述中生成 template cv2.imread(target_desc) result cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result) # 落在真实屏幕坐标需要除以 DPI 缩放 x int(max_loc[0] / self.dpi_scale) y int(max_loc[1] / self.dpi_scale) return x, y, max_val def execute_action(self, action): a_type action[type] if a_type click: x, y action[x], action[y] pyautogui.click(x, y) time.sleep(0.05) return f已点击 ({x}, {y}) elif a_type type: pyautogui.write(action[text], interval0.02) return 已输入文本 elif a_type finish: return 任务完成 return 未知动作 def run(self, user_goal): for step in range(self.max_steps): png self.capture_screen() png_bytes io.BytesIO() png.save(png_bytes, formatPNG) # 拼请求让模型根据截图和工具列表输出动作 response self.llm.chat_with_image(user_goal, png_bytes.getvalue()) action json.loads(response) result self.execute_action(action) if action[type] finish: return result return 超过最大步数这段代码有几个细节值得说。capture_screen用ImageGrab.grab()在 Windows 上能拿到当前物理屏幕虚拟机的分辨率也适用。template_locate里的 DPI 换算我单独提出来因为有的机器上pyautogui的坐标系统和 PIL 截图的坐标系统不一致不分开放会非常难受。execute_action里的time.sleep(0.05)是给 GUI 程序的响应留时间某些软件反应慢点击后立刻截屏会看到还没刷新。MCP 客户端的核心就一段初始化 列出工具import json, subprocess class MCPClient: def __init__(self, command, argsNone): self.proc subprocess.Popen( [command] (args or []), stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, ) self.next_id 0 def _send(self, method, params): self.next_id 1 payload { jsonrpc: 2.0, id: self.next_id, method: method, params: params, } self.proc.stdin.write((json.dumps(payload) \n).encode()) self.proc.stdin.flush() line self.proc.stdout.readline() return json.loads(line.decode()) def initialize(self): return self._send(initialize, {protocolVersion: 2024-11-05, capabilities: {}}) def list_tools(self): resp self._send(tools/list, {}) return resp.get(result, {}).get(tools, []) def call_tool(self, name, arguments): return self._send(tools/call, {name: name, arguments: arguments})initialize里的protocolVersion需要注意。MCP 协议迭代速度不慢有些服务器的实现比较老写版本号时最好先跟服务器端确认。如果是在本地连自己写的 MCP server版本直接用最新如果连第三方服务建议做一次版本协商先发自己的版本如果返回错误再降级。3.3 单文件打包过程PyInstaller 的坑与解法打包是这项目里最容易让人心态爆炸的部分。我最终用 PyInstaller 的命令行参数是pyinstaller --onefile --noconsole --add-data templates;templates agent.py--noconsole是故意的程序跑起来之后不弹黑色控制台窗口所有日志写进文件。--add-data把模板目录放进包里因为 PyInstaller 单文件模式解压到临时目录时不会自动带资源文件。真正踩到的一个大坑是OpenCV 在打包后需要额外的数据文件来支持各类图像格式读取。不加处理的话打包出来的程序调用cv2.imread时偶尔会报错。解决方法是找到 OpenCV 安装目录里的config和data文件夹一并加进--add-data。另一个坑是运行时路径。PyInstaller 单文件运行时当前工作目录不一定是解压目录引用资源文件要用sys._MEIPASSimport sys, os def resource_path(relative_path): base_path getattr(sys, _MEIPASS, os.path.abspath(.)) return os.path.join(base_path, relative_path)如果不加这段你在开发环境里跑得好好的一旦打成单文件模板路径全部失效程序启动即报错。单文件的体积问题我也想说一句。用 PyInstaller 打出来的包大约 100MB其中 OpenCV 占掉一半。如果你完全不想为 GUI 图像匹配引入 OpenCV也可以直接用 PIL 的ImageChops做像素级匹配但那种方式的鲁棒性差很多。我最后的选择是留在 OpenCV换取稳定性和开发效率。3.4 验收流程怎么判断一个 Agent 做得到不到位验收比开发更重要。我整理了一个五级测试清单每个级别对应一个实际任务从易到难逐级通过层级测试任务标准L1打开记事本输入指定字符串字符完整无遗漏L2打开计算器点击按钮完成加法结果正确L3打开浏览器搜索关键词并截图页面加载完成L4用 MCP 工具读取本地 JSON 文件内容返回内容正确L5GUI MCP 混合读取文件里的值填入 GUI 表单表单值正确且可见L5 是最贴近实际场景的测试。比如你把一批用户数据放在 JSON 里让代理打开一个内部管理系统把数据一条条填到表单里。这个流程同时考验 GUI 定位、MCP 调用和 Agent 循环的上下文管理能力。我第一次跑 L5 时模型点错了表单字段把 A 字段的数据填进了 B 字段后来给模型的反馈信息里加上“当前聚焦字段名称”这个观察项错误率立刻下降不少。4. 常见问题与排查技巧实录从开发到现在机制上的问题基本都遇到了。挑五个最常见的按症状和排查方向列成表格症状可能原因排查方向点击总是偏右下角DPI 缩放未换算检查截图分辨率与屏幕分辨率核对缩放系数MCPtools/list返回空服务器端未声明工具手动用命令行启动 MCP server逐行读输出模型反复点击同一个位置上一步执行失败但反馈不明确在上下文里增加“点击前后截图对比”描述打包后无法调用 MCP 子进程PyInstaller 环境缺 PATH 条目用sys._MEIPASS拼接子进程路径模型输出 JSON 不合法输出了 markdown 代码块做后处理剥离 json 标记后再 parse4.1 GUI 操作翻车现场点击位置与预期不符有一次我让代理点击一个窗口右上角的“关闭”按钮它每次都点到了窗口标题栏把窗口最小化了。排查后发现是模板匹配的模板图片问题它用的模板是最大化状态下的按钮样式但测试窗口是还原状态按钮大小差了几个像素。这个问题暴露了一个原则模板图片尽量来自被测程序的真实截图不要手工拼凑。4.2 MCP 兼容性为什么有的服务器连不上MCP 服务器虽然协议统一但实现千差万别。有的服务器启动后要先走一个握手阶段只接受 HTTP 升级请求有的 stdio 模式下还会输出日志到 stdout把协议输出给污染掉。这两类问题排查方法完全不同。前者要关闭 HTTP 模式改走 stdio或者把连接超时调到 10 秒以上后者要给子进程的 stdout 做一层过滤只保留符合 JSON-RPC 格式的行。实际项目中我见过不少把日志打印到 stdout 的 MCP 实现这在调试时让人很头疼。我的防御性方案是在子进程 stdout 上做一次“JSON 行过滤”非 JSON 开头的一律丢弃。代价是部分服务器会把日志写到 stderr不影响通信。4.3 单文件体积控制与启动速度PyInstaller 单文件每次启动要解压到临时目录100MB 的包在机械硬盘上可能要 3 到 5 秒才能进入主逻辑。我后来用 UPX 压缩了部分二进制的资源文件体积降到 80MB 左右但阿 OpenCV 的某些模块压缩后启动反变慢所以只挑闲杂 DLL 做 UPX核心模块保持原样。还有一个更彻底的办法就是把程序拆成“启动器 核心逻辑分离”的混合模式启动器只有 2MB负责检查环境然后把核心逻辑从外置目录加载。不过这偏离了我“单文件”的初始目标所以只作为备选方案放着。4.4 模型上下文长度焦虑这个项目里最容易被低估的是上下文消耗。每一次循环都会附上截图、工具描述、上一步结果跑 15 轮下来光视觉输入就占了大量 token。我试过两种缓解方式。第一种是“截图压缩”先把截图缩小到宽度 1024再转 JPEG 压缩质量 70视觉 token 能省将近一半。第二种是“遗忘策略”超过 8 轮后不再把早期的原始截图塞回上下文只保留早期的文字描述摘要。这样既能保住关键信息又能显著拉长 Agent 的有效工作轮次。4.5 模型输出不稳定的应对即使加了 JSON 结构化限制模型偶尔还是会输出一些奇怪的字段比如把坐标值写成字符串。我的兜底处理是写了一个resilient_parse函数尝试三次解析每次都尝试修复常见的 JSON 错误比如去掉多余的逗号、补全缺失的右括号。如果三次都失败就默认执行一次截屏操作把新截图继续给模型看让它重新决策。这招能解决大约九成的不稳定输出剩下的只能靠换更强的模型。写在最后这个项目还能怎么长我个人的体会是这类工具的价值不在于它本身多智能而在于它把“人的操作接口”变成了“模型的工具接口”。原来你需要在 GUI 上手工点的每一步现在都成了 Agent 循环里的一个动作原来要写的专属插件现在可以通过 MCP 协议一次性接入。如果你也想折腾我给三个实用建议。第一别一开始就追视觉模型的精度先用模板匹配把闭环跑通再逐步加智能元素。第二MCP 的工具命名要有规律模型才能准确理解每个工具的用途工具描述写得越具体调用越不容易出错。第三单文件打包的坑提前摸底Windows 上先做最小化打包测试再集成 OpenCV分步推进不要把未知变量一次性全加进去。后续我打算把 GUI 定位模块再往后扩展一层让它能根据窗口内的控件结构图来定位而不是纯靠像素模板。这样面对动态生成的界面稳定性还能再上一个台阶。目前这个版本已经能解决我日常工作里的相当一部分重复操作了而且它全程免费代码也开放愿意折腾的人完全可以从这篇文章里的骨架开始把它长成适合自己的样子。
返回列表