最近我做了一个免费开源的 AI 编码代理,它的核心卖点就三句话:能像人一样操控电脑上的图形界面(GUI)、原生支持 MCP(Model Context Protocol)工具协议、整个项目以单文件方式运行和分发。这个项目的体量不大,但日常开发里能帮的忙却很直接——你可以让它替你去打开某个软件、点几个按钮、读几个文件,再通过 MCP 顺路调用系统里现成的工具能力。说白了,我就是在给 Agent 装上一双手,外加一个统一规格的工具箱。
这阵子社区里聊 GUI Agent 的人明显多了,MCP 也从“概念名词”变成了越来越多项目的标配。我自己一直有个执念:编码代理不能只活在终端里,它应该能直接面向我们每天面对的真实软件。抱着这个想法,我花了几个晚上把之前的自动化脚本和 Agent 框架揉在一起,做了一个轻量版的工具,开源出去,并且坚持把“免费”和“单文件”作为两条硬门槛。下面是完整的设计思路、实操过程,以及踩过的那些坑。
1. 为什么我要做这个 AI 编码代理
1.1 现成的 Agent 大多缺一双手
市面上已经有不少编码代理,它们都能写文件、跑 Shell、调 API,能力很强。但你让它们去完成一个“打开某个软件,在配置界面改几个参数,然后导出结果”的任务,它们就立刻傻眼了。问题不在于模型不够聪明,而在于它们缺少一个和图形界面交互的通道。
我印象很深的一次场景:同事要用一个内部后台系统,把上百个设备挨个核对状态,再点进去改配置。这活儿如果交给脚本,得先分析 DOM、再模拟请求,遇到页面结构调整就全崩;如果人肉点,又耗时又无聊。我当时就想,要是能让模型直接“看”屏幕、“点”鼠标,完全按照人类操作电脑的方式去执行,这类工作就能被大幅简化。
这就是这个项目的出发点:一个不仅会读文件、写代码的 Agent,还能在 GUI 世界里行动。它不是要取代那些严谨的自动化测试框架,而是补上“即兴操作”“理解界面”这一环。
1.2 MCP 是这一轮工具化的第一公民
MCP(Model Context Protocol)做了一件很干净的事:把“模型怎么接入外部工具”这个过去每个项目都要单独重复一遍的活儿,变成了一个统一协议。工具开发者只需要按 MCP 标准写一个 Server,任何支持 MCP 的 Client 都能发现并调用它。
早期我做 Agent 时,每接一个工具就要写一段 Adapter,比如调搜索引擎要写一套请求封装,调数据库又要写一套连接管理,维护成本很高。接入 MCP 之后,这些工具通通变成一个个描述清晰的 Tool,Agent 根据模型返回的 function call 去执行任务,整个体系一下子清爽了。
我在这个项目里把 MCP 支持做成了默认能力,而不是可插拔插件。项目启动时读取配置文件,自动连接 Server、拉取工具列表、注册到模型可用的 function calling 里。工具生态的红利,几乎是一接入就能马上感受到的,这也是我推荐大家都去试试 MCP 的原因。
1.3 免费加单文件,是我给自己定的两条规矩
不少 Agent 工具功能很强,但安装步骤已经可以写成一本书:装 Python 环境、拉依赖、配 API Key、处理各种兼容性问题。劝退了不少人。我给自己定的第一条规矩就是:免费开源,不搞收费墙。项目用宽松的开源协议发布,模型层完全由使用者自己选,本地模型或远端 API 都行,工具本体没有成本和次数限制。当然,如果你用云端模型,那部分费用是云厂商收的,和这个工具本身无关。
第二条规矩更严:单文件运行。我把核心代码塞进一个独立的 Python 脚本,并提供打包成单个可执行文件的配置文件。这样用户拿过去不用装一堆东西,也不用面对复杂的目录结构。单文件也有利于透明审计——所有逻辑都在一个文件里,想查什么直接看代码,这对一个涉及 GUI 操控和外部工具调用的项目来说,本身就是一种安全承诺。
2. 核心特性拆解:GUI 操控与 MCP 对接
2.1 让 Agent 看得见、点得准的三条路线
要让模型操作 GUI,本质上只有三种路线,我做了个表对比:
| 方案 | 精度 | 实现难度 | 适用场景 |
|---|---|---|---|
| 截图 + 多模态模型定位 | 中低 | 低 | 任意可见界面,最通用 |
| OCR + 模板匹配 | 中高 | 中 | 界面文字可识别,按钮位置稳定 |
| 无障碍树 / UI Automation | 高 | 高 | 系统或软件提供了完整控件信息 |
实际项目里我选择三路并用:默认先拿截图交给多模态模型理解,让模型返回目标在屏幕上的坐标;如果发现截图方式不够稳定,再启用 OCR 辅助;对于 Windows 下面支持无障碍接口的窗口,直接用 UI Automation 拿到控件坐标,准确率最高。三套感知方式组合起来,才能应对“什么界面都可能遇到”的现实。
这里要提醒一下,模型“看得见”不等于“点得准”。模型返回的坐标是照片坐标系里的像素位置,而你实际点击时用的是屏幕坐标系,两者不换算直接去点,大概率会歪。下图这类问题是我在开发过程中遇到最多的情况,所以坐标换算被我单独做成一个模块。
2.2 坐标转换是 GUI 操控的命门
很多人第一次做 GUI 自动化的坑都出在坐标上。核心原因很常见:Windows 的 DPI 缩放。如果你的屏幕是 1920×1080 分辨率,系统缩放设为 150%,那么逻辑分辨率是 1280×720,截图出来的像素坐标系和真实点击坐标之间有一个 1.5 倍的缩放关系。在这个情况下,你直接把截图上的像素位置当作鼠标坐标去点击,每点必差一截。
我的做法是:截图时记录截图的真实像素宽高,同时用系统 API 查询当前屏幕的缩放比例。模型返回的是“相对坐标”,也就是它认为目标物品中心在整张图里的横向比例和纵向比例。然后我再用这个比例乘以物理屏幕的宽高,再除以缩放因子,得到逻辑坐标,最后交给鼠标事件去执行。
def rel_to_screen(rel_x: float, rel_y: float, frame, scale_factor: float): # frame: (width, height) 截图像素尺寸 # scale_factor: 1.0 表示 100%,1.5 表示 150% pixel_x = rel_x * frame[0] pixel_y = rel_y * frame[1] return (pixel_x / scale_factor, pixel_y / scale_factor)这段代码很短,但它是整套 GUI 自动化的地基。如果这个环节不处理好,后面所有的点击、输入、拖拽都会变得不可预期。
2.3 MCP 让工具生态不再“各自为政”
MCP 协议里有两个角色:Server 和 Client。Server 暴露三类能力——Tools(可执行的函数)、Resources(可读取的数据)、Prompts(可复用的提示词模板)。Client 则是模型和应用之间的桥梁,负责发现这些能力、调用它们,然后把结果返回给模型。
在我的代理里,MCP Server 以两种方式呈现:本地 stdio 进程,或是远程 streamable HTTP 服务。本地常见,比如文件系统 Server、Git Server;远程也不错,适合把一些公共服务以 MCP 方式暴露出来。
运行时,我会维护一个“工具注册表”,把每个 Server 传来的工具定义转换成模型能理解的 function calling 格式。模型只要说“我要调用 read_file”,我就能在注册表里找到对应的 Client Session 并把参数透传过去。整个过程对模型来说是透明的,这也是 MCP 最舒服的一点——接口统一,扩工具不用改 Agent 核心。
2.4 单文件到底带来了什么
单文件听起来很酷,但它并不是简单的“把所有代码粘进一个文件”,而是刻意保持了一种紧凑的组织方式。在这个项目里,单文件内部仍然按职责划分区块:感知模块、动作模块、MCP 模块、LLM 交互模块、配置加载模块,每个区块用清晰的注释分隔。核心逻辑一眼能看完,维护起来也不会比多文件项目差太多。
打包成单文件可执行程序,我用的是 PyInstaller 的 onefile 模式。依赖控制得很克制:感知层用 Pillow;动作层用 pyautogui 和 ctypes;MCP 用官方 Python SDK;LLM 用 OpenAI 兼容接口。四方依赖都相当稳定,打出来的包在 Windows 上直接双击就能跑,不需要额外装 Python。
有人可能担心单文件体积偏大,但这恰恰换来了分发的简单:一个文件,拷到任何一台机器上就是完整工具。对一个编码代理来说,透明、好部署、易审计,比优雅的目录结构更重要。
3. 整体架构与关键实现
3.1 运行循环:观察、思考、行动、验证
整个 Agent 的运行逻辑非常像一个人类实习生的工作方式:先看看当前屏幕,想想下一步做什么;执行一个动作之后,再重新看一下屏幕,确认动作有没有生效;确认不了就换个方案继续试。这个循环我简化为四步观察、思考、行动、验证。
while not task_finished: obs = capture_screen() plan = model_turn(obs, tool_schemas, history) action = plan.action # click / type / mcp_call / finish if action.name == "finish": break result = execute(action) history.append((obs, plan, result))关键设计在于“验证”不是独立的一步,而是隐含在下一次循环的观察里。也就是说,Agent 每次执行完动作都会重新截图,把新屏幕和预期结果做比较。如果屏幕状态没变,模型就会判断前一个动作是否失败,从而自动修正。这个反馈闭环让我不需要在代码里写死超时判断,完全交给模型临场发挥。
3.2 感知层:截图、OCR、无障碍树三件套
感知层贴上三个部分:
截图模块负责抓取当前屏幕画面,并附带帧尺寸信息。Windows 下我优先用 mss 库,速度比 PIL.ImageGrab 快不少,还能处理高 DPI 场景。
OCR 模块用在截图方案不可靠的时候。比如界面元素特别密集,或者模型对某块区域的判断来回摇摆,我就把这部分的截图裁出来跑 OCR,提取按钮文本和坐标,然后把文本和坐标一起送进模型上下文。模型看到的不再是模糊的图标,而是明确的“按钮名: 坐标”,判断成功率会明显上升。
无障碍树模块是终极方案。Windows 上可以通过 UIAutomation 拿到窗口的完整控件层级。例如一个“确定”按钮,从树里不仅能拿到它的文本,还能直接拿到它在屏幕上的准确坐标。这条信息比模型目测坐标可靠得多。我一般把无障碍树先查一遍,查不到再降级到截图。
def capture_screen(): frame = mss.mss().grab(...) ui_tree = get_uia_tree_if_possible() ocr_blocks = run_ocr_if_needed(frame) return {"image": frame, "size": frame.size, "ui_tree": ui_tree, "ocr": ocr_blocks}在实际任务里,这三路信息不会平均使用。我会让模型自己决定优先参考哪一路:有控件坐标时优先用;没有时用 OCR;再不行就靠视觉直接猜相对位置。这样既保证了准确性,又不至于因为某个模块失效而卡死。
3.3 行动层:鼠标、键盘、窗口与输入
行动层的命名空间必须窄,因为模型需要在有限的动作集合里做出选择。动作太泛会让模型无所适从;动作太碎又容易让模型陷入低效循环。我最终保留了六个基础动作:
| 动作名 | 参数 | 说明 |
|---|---|---|
click | x, y, button | 鼠标点击 |
type_text | text, interval | 键盘输入 |
press_key | key, modifiers | 快捷键组合 |
scroll | x, y, clicks | 滚动 |
launch_app | path/command | 启动程序 |
mcp_call | server, tool, args | 调用 MCP 工具 |
动作并不复杂,但要注意的是执行前的安全校验。我在click和type_text里都加入了坐标范围检查,凡是落在屏幕外面的数值直接丢弃;launch_app则放在一个白名单里,只允许用户预先配置好的程序路径,避免模型被诱导去启动奇怪的东西。
def execute(action): if action.name == "click": x, y = screen_to_logical(action.args.x, action.args.y) assert 0 <= x < LOGICAL_W, "x out of range" assert 0 <= y < LOGICAL_H, "y out of range" pyautogui.click(x, y, button=action.args.button) elif action.name == "mcp_call": session = mcp_sessions.get(action.args.server) return session.call_tool(action.args.tool, action.args.args)这套动作层实现起来不难,难的是让模型准确生成参数。我把每个动作的 schema 描述写得很具体,比如type_text的 interval 参数会说明它控制字符输入间隔,适合对输入速度不敏感的对话框。给模型的上下文里附上示例而不是抽象定义,通常比堆砌字段描述效果更好。
3.4 MCP 客户端适配:从发现工具到调用工具
MCP 客户端的接入逻辑我用了一个专门的管理类。启动时读取配置文件里的 server 列表,逐个建立会话,拉取 tool 列表,合并后送进 LLM。说起来简单,实际操作里最耗时的部分是“参数 schema 转换”。
class MCPManager: def __init__(self, config): self.servers = {} self.session_map = {} self.tools = [] async def start(self): for name, cfg in config.items(): params = StdioServerParameters(command=cfg.command, args=cfg.args) session = await connect_server(params) tools = await session.list_tools() self.session_map[name] = session self.tools.extend(convert_schema(tools)) log.info("server %s loaded, %s tools", name, len(tools))MCP 官方 SDK 已经做了很多底层工作,我只需要负责运维侧的事:进程启动失败时的错误提示、Server 没有按约定暴露工具时的日志处理、Server 异常退出后的自动重连。这些边角问题直接影响 Agent 的稳定性,我把它们都写进了日志系统,方便排障。
4. 实操:完整跑通一次 GUI + MCP 混合任务
4.1 准备环境与配置文件
在动手跑流程之前,需要准备一个最小的运行环境。我这里以 Windows 为例,Linux 和 macOS 原理一样,只是截图和点击层要换成对应平台的库。
pip install mcp openai mss pillow pyautogui配置文件的格式我用 JSON,结构很简单:
{ "llm": { "base_url": "http://localhost:11434/v1", "api_key": "ollama", "model": "qwen2.5vl:7b" }, "mcp_servers": { "fs": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "E:/CProject"] }, "git": { "command": "python", "args": ["mcp_git_server.py"] } }, "allowed_apps": ["C:/Windows/explorer.exe", "C:/Program Files/.../code.exe"] }这里 LLM 部分我默认写成了一个本地 Ollama 地址。你也可以换成任何 OpenAI 兼容的远端服务,地址和密钥改一下即可。工具本身不收费用,模型费用取决于你选择的提供方。
4.2 任务示例:打开文件夹,找到文件,借助 MCP 读取内容
我常用的演示任务是这样的:让 Agent 打开指定文件夹,找到最新的一个 Markdown 文件,然后用 MCP 的文件系统工具读取这个文件,并把内容摘要返回给用户。
整个过程如果由人类来做,大概十几秒;Agent 做起来则会经历若干轮屏幕观察和动作执行。启动命令非常简单:
python agent.py "打开 E:/CProject 目录,找出最新的 markdown 文件,读出内容并总结"Agent 收到任务后,第一步不是立即点鼠标,而是先截图观察当前桌面状态,判断现在有没有资源管理器窗口。随后它会按下Win + E打开资源管理器,在地址栏输入E:/CProject,然后回车。接下来它会滚动文件列表,识别出按修改时间排序后的候选文件,再调用fs这个 MCP Server 里的read_text_file工具读取内容。
这个例子很巧妙地同时覆盖了两类能力:GUI 操作用于启动应用、输入地址、滚动列表;MCP 调用用于读取文件内容。两者互相配合,正好展示了这个代理和纯命令行 Agent 的差别。
4.3 核心代码走查
几个关键的实现点值得单独拿出来说。首先是“截图 + 模型返回坐标”那一段。为了让模型返回稳定的结构化动作,我把系统提示词里写死了 JSON 输出格式,并规定坐标必须使用相对比例:
[{ "action": "open_explorer", "target": "" }, { "action": "click", "rel_x": 0.72, "rel_y": 0.05, "button": "left" }, { "action": "type_text", "text": "E:/CProject", "interval": 0.02 }, { "action": "press_key", "key": "enter" }]相对坐标的好处是,不管屏幕分辨率是多少,模型输出的比例都适用于当前帧。它不需要知道具体像素,只需要描述“目标在画面的右下区域”,换算由程序完成。
然后是 MCP 调用环节,模型会在自己的回复里生成一个mcp_call节点,指定 server、工具和参数。MCPManager 接收到之后,先在本地校验参数类型,再转发给对应 session。这一步其实等同于传统编程里的函数调用,不过函数签名来自远端协议,参数由模型自动生成,所以我额外加了一层宽容度:远端的报错信息原样返回给模型,让它自己调整重试,而不是直接终止任务。
def run_mcp_call(self, server_name, tool_name, args): session = self.session_map.get(server_name) if not session: return {"error": f"server not found: {server_name}"} result = session.call_tool(tool_name, arguments=args) return {"tool_result": result.content, "is_error": result.is_error}4.4 把更多 MCP Server 接入进来
任务跑通之后,扩展能力几乎变成写配置文件的事。我在配置里加一个 server,重启 Agent,新的工具就自动出现了。培养基上,不需要修改任何 Agent 逻辑。比如我加过一个 PostgreSQL 的 MCP Server,Agent 多了一个query_database能力;我又加过一个 Git 管理 Server,它立刻能执行提交、读日志、看分支。
这套机制的价值在于“工具和 Agent 解耦”。Agent 不需要在代码里理解每个工具的细节,它只需要知道“我能做哪些事”,然后根据任务目标选择调用哪几个。对使用者来说,每遇到一个新工具就多一分便利,几乎是无感扩展。MCP 的生态在持续增长,这个项目天然的就把增长红利带给了使用者。
5. 踩坑实录与常见问题排查
5.1 点击永远差一点,多半是 DPI 在作怪
我最开始测试时,Agent 点击总是偏在按钮左上方,一开始我以为是模型视觉误差,后来发现是截图坐标和逻辑坐标没换算。加了缩放因子之后,问题立刻消失。这里给大家一个建议:如果发现 Agent 点击位置整体偏移,先打印两个数字,一个是截图真实尺寸,一个是系统逻辑分辨率。两者有比例差异,就是 DPI 缩放问题。
5.2 模型“看走眼”时怎么补
多模态模型偶尔会认错图标,特别是界面特别花哨、图标风格复杂时。我会在客户端保留原始截图,并在日志里记录模型认为它点击了哪里。如果判断错了,我就会调整提示词,让模型优先依赖 OCR 或无障碍树,而不是裸猜。一两次之后,这种“视觉优先、树兜底”的方式稳定了很多。
5.3 MCP Server 连不上的几类原因
这是出现频率最高的问题。MCP Server 进程启动失败的常见原因有三类:
| 症状 | 可能原因 | 快速定位 |
|---|---|---|
spawn npx ENOENT | 系统 PATH 里没有 Node 或 Python | 在配置里写绝对路径 |
| Server 启动但没工具 | 协议版本不匹配 | 查看 MCP SDK 版本并统一 |
| 调用超时 | Server 在执行耗时任务 | 调长tool_call_timeout |
我个人最常踩的是第一类:配置文件里的命令用了npx,但 Agent 进程在无交互 Shell 下运行,PATH 不完整,找不到 npx。解决的方法很粗暴:在配置里把 command 改成 Node 或 Python 的绝对路径,args 里再带完整脚本路径。
5.4 单文件程序在别人电脑上跑不起来
PyInstaller 打出的 onefile 文件体积大、启动稍慢,但它基本能跑。不过有两个小坑:一是目标机器可能缺 Visual C++ 运行库,程序一启动就报缺失 DLL;二是 Windows Defender 偶尔会对 PyInstaller 的引导程序误报。前者我会让使用者先安装 VC++ 运行库,后者则通过给程序加数字签名或添加白名单解决。这些虽然不属于核心逻辑,但直接影响分发体验,所以值得注意。
5.5 权限与安全的护栏
GUI 自动化和 MCP 调用都意味着 Agent 获得了较高的系统权限。我做了三层护栏:第一层是动作白名单,所有可执行程序路径都必须提前配置;第二层是dry_run模式,开启后 Agent 只打印计划不执行;第三层是运行时的确认开关,当模型想要启动未配置程序或调用敏感 MCP 工具时,系统会暂停并等待用户确认。
这三层护栏并不复杂,但它们能从需求层面避免 Agent “擅自操作”带来的风险。对于任何涉及 GUI 自动化的工具,安全边界的设定和功能实现同等重要。
6. 使用边界:什么场景我建议你别用
6.1 不适合完全无人值守的场景
GUI 自动化和 MCP 调用听起来很强大,但我要泼一盆冷水:如果你的应用场景是不需要人盯着、让 Agent 自己去完成关键业务操作,尤其是涉及财务、支付、生产环境变更,我建议你别用。原因很简单,GUI 自动化的成功率再高也有概率出错,而且界面一旦出现弹窗、卡死、登录过期等意外,模型的自我纠错能力不能保证百分百正确。
如果你确实要无人值守,至少设计一个自动暂停策略:Agent 连续两次动作没有改变屏幕状态时,立刻停止并通知人来处理。这个兜底机制可以避免它原地死循环或反复尝试危险操作。
6.2 适合的用法与团队协作
对我个人来说,它最适合的场景是“半自动”:人来发起任务,Agent 负责执行,人在旁边盯结果。比如批量处理文件、快速探索一个陌生系统、辅助操作没有 API 的旧软件。这类任务的共同点是不能失败到引发严重后果,但非常浪费时间,交给 Agent 之后,即使偶尔出错,也远比人肉点击划算。
团队环境下,我更推荐把这类代理作为“入门演示工具”。它能让不熟悉底层技术的人看到 AI Agent 的能力边界,也能让后端同学直观理解 MCP 的价值。尤其是当你把配置文件共享出去以后,团队里的每个人都能用自己的模型账号跑同一套管道,几乎零成本试错。
6.3 后续可以做的扩展
这个项目目前的形态还比较基础,但它非常适合在此基础上继续叠加。我的下一步计划是加入多显示器支持和窗口切换更智能的逻辑;再下一步希望把音频信息也纳入感知层,这样 Agent 不仅能看屏幕,还能听系统提示音。真正有意思的方向是把多个 Agent 连起来,一个负责操作界面,一个负责查资料,一个负责调用重型 MCP 工具,通过消息总线协作完成更复杂的任务。
7. 一些经验体会与建议
这个项目做到最后,我最深的体会是:GUI Agent 的难点从来不在模型,而在工程细节。坐标缩放、OCR 降级、MCP 进程管理、动作白名单,每个环节单拎出来都不难,但把它们串成一个稳定的闭环,需要不断试错。
如果你也想做类似的东西,我的建议是先从一个非常窄的任务开始,比如“自动打开计算器并输入一串数字”。跑通这个闭环,你就能摸清截图、模型、点击、验证的最短链路,之后再去扩展 MCP、OCR、无障碍树这些增强能力,难度会降低不少。最后再提醒一句:所有自动操作都必须配护栏,这条经验值千金。