ARTICLE DETAIL

资讯详情

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

从零构建单文件AI编码代理:能看屏、动鼠标、接MCP

从零构建单文件AI编码代理:能看屏、动鼠标、接MCP 1. 为什么我要做一个“会动手”的AI编码代理我一直有个很烦的点大多数AI编码代理都是“终端里的哑巴”你让它改代码、跑测试、查日志它干得确实不赖但一旦涉及图形界面上的操作它基本就抓瞎了。我平时的工作流里有一大堆事情是绕不开GUI的比如打开一个桌面端的数据库工具连上测试库看数据、在Figma里按注释调整标注、在浏览器的可视化页面上点点菜单、在IDE里用鼠标调个断点。这些事情你说它有多难真不难但让编码代理去做它只能干瞪眼。命令行工具确实覆盖了很大一部分自动化场景但现实世界不全是终端。尤其我这种经常要跟各种内部系统、可视化工具打交道的场景最理想的状态是我对AI说一句“去把那个接口测试一下把返回结果截图给我看”然后它自己打开GUI、操作鼠标键盘、看屏幕反馈、分析结果全流程跑完。市面上的工具不是没有类似的思路但大多要么闭源收费要么重度依赖云端环境要么要装一大堆运行时和依赖折腾半天还没跑起来。所以我花了一个周末的时间自己动手做了一个轻量级的AI编码代理。它不是一个云端套壳服务也不是几十个文件的项目工程就一个单文件免费的你拿到手就能跑。核心能力有两块一是能看屏幕、模拟鼠标键盘真正“操作”图形界面二是支持MCP可以把外部工具统一接进来让代理的能力不局限于我自己写死的那几个函数。我给它定的设计目标是这么几条单文件运行不用依赖虚拟环境、不用安装复杂的SDK包拿到手直接执行就完事免费代码开源模型调用部分你用自己的API Key甚至也可以用本地模型能操控GUI截屏、解析屏幕内容、执行点击和输入这条路必须跑通接入MCP不用自己重复造轮子能用统一的接口调用现成的MCP工具服务。这篇文章不是我做完之后的“展示型总结”而更接近一份把从需求到实现、再到踩坑的过程完整梳理的实战记录。看完之后你不仅能理解它的整体架构也能直接用同样的思路自己复刻一个。如果你正好需要一个能干活、能看屏幕的编码代理这篇应该帮得上忙。2. 单文件设计为什么不让它散成一地依赖做这类工具的人第一反应往往是建个工程目录把视觉模块、动作执行模块、模型调用模块、MCP客户端拆成一个个子目录再配上依赖清单。但我的选择恰恰相反整个项目就压成一个文件。这个选择不是偷懒而是有意为之。2.1 单文件的好处被低估了单文件最直接的价值是分发成本为零。我可以把文件传到任何一台机器上只要有Python环境和对应的基础依赖就能跑不用克隆仓库、不用解决依赖冲突、不用关心包版本。对于这种“代理工具”来说能在一个环境里快速跑起来价值比代码结构上的优雅重要得多。还有一点是可验证性。一个文件让使用者信任成本大幅降低——你不用逐个目录审查代码一个文件展开就能看完所有逻辑。很多工具做出来没人敢用就是因为黑盒感太强。单文件至少意味着透明。从调试角度讲单文件也更方便。出了问题你直接打开文件搜函数就能定位跨文件跳来跳去反而容易绕晕。我理想中的这个项目形态应该像一个“瑞士军刀”体积不大但功能都在坏了也好修。2.2 核心模块是怎么塞进一个文件里的整个文件内部虽然没有物理拆分但逻辑上被我分成了清晰的几层。按我的设计大概是这样# agent_solo.py 核心结构示意 # # 模块一视觉层负责屏幕截图与图像预处理 # 模块二模型层负责调用多模态大模型解析屏幕图像并生成动作 # 模块三执行层负责把模型返回的动作指令映射为系统级鼠标键盘操作 # 模块四MCP层负责与MCP工具服务器通信统一获取外部能力 # 模块五循环调度把上述模块串成一个“感知-决策-执行”的闭环你可能会觉得这不就是分层架构的微型版嘛。对架构思想没有变只是实现上不做物理拆分。在这个文件里每一层大概对应一个类或一组函数代码量的分配大概是模型调用和Prompt拼装占三成执行层占两成MCP通信占两成视觉层占两成剩下的是主循环和辅助工具函数。这种设计最关键的好处是当你需要改一个行为时你非常清楚它在这条主循环链路的哪个位置。比如模型老是点错位置你会去看视觉层MCP工具返回超时你会去看MCP层的重试逻辑鼠标连点没有响应你会去查执行层的动作间隔。2.3 依赖取舍的底线虽然我追求轻量但完全不依赖第三方库是不现实的。我的原则是尽量把依赖控制在一个相对合理的范围内。模型调用大多数局域网内能跑通的接口直接用HTTP请求就够了不封装SDK屏幕截图如果用纯标准库我只能截取整个屏幕再手动转格式很折腾这里我会用一个轻量的截图库GUI操作负责模拟鼠标和键盘的库是必须的它是执行层的物理基础。实际上我把依赖数量控制在个位数而且都是常见、稳定的基础库。这带来的直接体验就是拷过去的机器基本不用额外折腾环境跑不起来的情况极少。提示单文件不等于代码质量可以打折。反而因为所有代码挤在一起更要重视函数边界和命名清晰。不然改到第三周你自己打开文件都要做心理建设。3. GUI操控链路怎么让模型真正“看见”并操作屏幕这一节是整个项目最核心的部分。编码代理能“操控GUI”本质上要做三件事看采集屏幕信息、想决定下一步动作、做把决策变成操作。听起来简单但每一步都有坑。3.1 视觉回路的搭建先说“看”。我的方案是周期性截屏把当前屏幕图像发给多模态模型。这其实是目前比较通用的做法我选择它不是因为它最优雅而是它是适用范围最广的路径。为什么不直接用可访问性树或者DOM解析因为GUI的世界太多样了浏览器里的网页有DOM原生桌面应用有独立的控件树截图里的普通软件什么都没有。想通吃所有GUI视觉方案是唯一能保障“万物皆可操作”的路径。你截个图如果用的是多模态大模型它天然就能理解当前屏幕上是什么界面、有哪些按钮、大概在什么位置。截屏这里有个细节值得说分辨率与缩放。现代操作系统普遍有显示缩放截出来的图和真实逻辑坐标不一定一一对应。我第一次测试时模型看截图说“点击右上角那个按钮”返回的坐标在截图里是对的但真正执行鼠标点击时却偏了很远。原因是Windows的长城缩放比如150%导致坐标映射错位。后来我在执行层里加了一步坐标换算把截图坐标乘以缩放系数问题才彻底解决。下面是视觉回路的核心循环代码可以简化为def run_once(): # 1. 截取当前屏幕 screenshot capture_screen() # 2. 将截图加上任务描述与历史上下文一起发给多模态模型 response vision_model.chat( images[screenshot], promptbuild_action_prompt(current_task, last_action_result) ) # 3. 解析模型返回的动作描述 action parse_action(response) # 4. 执行动作 execute_action(action) # 5. 等待片刻重新截屏以观察界面变化 sleep(0.8)这个循环看起来简单但真正重要的是第二步的Prompt设计。3.2 动作协议让模型“说人话”还不够得让它说“结构化的话”模型不能只回复“我点击了确认按钮”这种自然语言。我需要对它的输出做严格约束让它返回一个结构化的动作指令方便程序直接执行。我定义的动作协议大概长这样{ reasoning: 当前弹窗提示确认保存点击确定按钮, action: click, x: 860, y: 345, parameter: }这个协议支持的动作类型包括click指定坐标左键单击input在指定位置输入文本parameter字段放文本内容keypress模拟键盘按键比如CtrlSparameter放组合键描述scroll滚动鼠标滚轮方向和距离放在parameter里finish认为任务已完成终止循环。之所以用“坐标类型”模型是为了把理解交给模型、把执行留给程序。模型从截图里解读语义定位到坐标程序只负责忠实地移动鼠标、执行点击输入。两者分工明确不越界。3.3 执行与验证一次点击不代表任务完成真正让这个代理“靠谱”起来的是执行后的验证机制。很多类似的工具容易犯的毛病是打一下没看结果就认为这一步成功了结果后续全乱套。我的处理方式是每次执行完动作后先截屏把模型的操作结果作为下一轮Prompt的一部分反馈给模型。模型看到界面变化后自己判断“这个操作是否生效、是否已经完成任务”。如果不生效它会尝试换一种方式如果连续几次都没效果它会记录失败原因并选择放弃或重试。我曾经用这个代理做过一个测试任务“在浏览器里打开一个本地网页填写表单并提交”。第一次跑的时候模型正确输入了表单内容但提交后页面没刷新——原因是它点击的提交按钮上方有一个广告遮罩层实际点击被吃掉了。有意思的是模型在执行完并重新截屏后自己发现页面没有变化随后自动把鼠标稍微偏移了一下再次点击这次页面刷新成功了。这让我意识到GUI代理的可能性不取决于它第一下点得多准而取决于它有没有“看完结果再决定下一步”的闭环能力。只要验证闭环在模型就有机会自我纠错。4. MCP接入把外部工具统一成可调用的接口做编码代理的都知道模型能力再强不能调外部工具就等于没有手脚。而MCPModel Context Protocol恰恰是为了解决“让模型能调工具”这个问题的通用协议。把它接进来之后我这个单文件代理就不再是孤岛了。4.1 MCP到底解决的是什么问题用一个生活化的类比来理解MCP它就像USB-C接口。以前你给手机充电要带好几种线苹果的、安卓的、Micro-B的MCP要做的就是给“AI调用工具”这一领域定一个统一标准。任何工具只要实现了MCP服务端任何AI应用只要实现了MCP客户端就能直接对接不需要针对每个工具单独开发适配器。在MCP的世界里有两方MCP服务器服务端负责连接一个具体的工具或功能比如文件系统、Git仓库、网页抓取、数据库查询等MCP客户端客户端也就是我的代理本身负责发现可用的工具、调用工具、获取结果。所以我在代理里做的事情不是去实现某个私有接口而是实现一个标准的MCP客户端然后它就自动能连上任何MCP服务器。4.2 手写了一个极简的MCP客户端既然目标是单文件且依赖最少我决定不引入官方SDK而是直接用JSON-RPC格式与本地MCP服务器通信。整个通信逻辑简化后其实就三个核心操作initialize建立会话协商协议版本tools/list拉取该服务器支持的工具列表tools/call调用指定工具。这三个操作走的就是标准JSON-RPC报文核心请求样例如下def mcp_call(endpoint, method, paramsNone): payload { jsonrpc: 2.0, id: next_request_id(), method: method, params: params or {} } resp requests.post(endpoint, jsonpayload, timeout30) result resp.json() if error in result: raise RuntimeError(fMCP Error: {result[error]}) return result.get(result, {})然后我把tools/list返回的工具转换成代理内部的工具清单在Prompt里告诉模型“你现在可以调用哪些外部工具”。模型在决定行动时如果发现当前任务需要读取一个文件或者查一段文档就会触发tools/call请求把结果作为下一轮推理的上下文。4.3 接入MCP之后代理的外延能力发生了什么变化只靠GUI操作代理能干的事其实偏向“点按钮、填表、截图看结果”接上MCP之后它相当于多了一整套工具库。举个例子我给它挂了两个MCP服务器文件系统MCP服务器代理可以直接读取工作目录里的代码文件了解项目结构再根据看到的内容去编辑器里操作GUI网页抓取MCP服务器当任务涉及某个外部链接时代理可以先抓取页面的结构化内容而不是纯粹靠视觉猜测界面。这两者结合之后的效果非常明显。之前它打开一个IDE窗口写代码纯靠视觉识别代码内容和报错信息效率很低。而接入文件系统MCP后它可以直接读取文件内容来判断代码逻辑再回到IDE里精确点击目标位置。视觉负责“定位和操作”MCP负责“读取和理解”两者互补整个代理的完成度立刻提升了一个量级。不过引入MCP也带来一个新的复杂性工具选择策略。当代理同时具备多个外部工具时它需要判断什么时候该用GUI视觉什么时候该走MCP工具什么时候两者结合。我目前的做法是把工具列表塞进Prompt让模型自己权衡但实际操作中发现模型偶尔会做出“明明可以直接读文件却非要去GUI里打开文件再看一眼”这种低效决策。后续我打算加一个简单的“成本偏好提示”让模型优先选择它认为成本更低的路径。5. 实测里被教育过的坑坐标偏移、误触、空转与断连正因为这个项目是真实的工具不是在演示环境里跑跑就好我花了不少时间处理各种“现实世界对理想模型的毒打”。这一节算是本篇文章里我觉得最值钱的部分全是我实际踩过、并且最终伸手解决了的问题。5.1 高分屏与缩放比例导致的坐标偏移这是我遇到的第一个拦路虎。在部分环境下代为截屏的分辨率是逻辑分辨率而鼠标事件用的坐标是物理分辨率中间隔着一个缩放系数。如果代理直接按截图返回的坐标点击往往会点偏。解决方式是在执行层统一做坐标换算def convert_position(x, y): # 获取当前显示器的缩放比例 scale get_screen_scale_factor() return int(x * scale), int(y * scale)这个坑之所以值得单独写是因为它不做任何提示、也不报错只会表现为“代理每次都点不中”看起来像是模型智障其实是坐标系错乱。5.2 模型“误触”敏感弹窗的风险与安全兜底测试过程中有一次让我冷汗直冒代理在执行任务时屏幕上弹出了一个不知名的系统提示窗而模型没能正确识别这个窗口的内容直接点击了提示窗上的某个按钮。虽然没造成什么实质性后果但它让我意识到一个能在桌面上乱点的代理必须要有明确的安全边界。我加的第一道防线是动作白名单默认情况下代理只能对当前任务相关的窗口区域进行操作比如限定在特定的应用窗口内不能全屏乱点。第二道防线是关键操作需要二次确认如果动作涉及“删除”“提交”“格式化”等高危操作我会要求代理停下来把即将执行的指令反馈给用户确认。提示这类GUI代理本质上是“有手有脚”的程序权限给得越大出事的可能就越大。除非你很清楚自己在做什么否则务必给它设置操作边界。不要贪全。5.3 空转循环模型在同一步上反复横跳还遇到过一种颇具喜感的场景代理在某个步骤失败后开始疯狂重复同一个错误动作。点击同一个坐标、等待、截屏、发现没变化、再次点击同一个坐标无限循环。这背后其实是模型的“探索策略”出了问题——它只想到了当前这一种可能的解法又没有足够的随机性去尝试新方案。我的解决方式是给循环加了一道“重复动作检测器”if same_action_count 3: action_policy change_strategy prompt 提示你连续执行了多次类似动作都没有效果请换一种策略或考虑放弃该步骤。 same_action_count 0加上这道检测后模型空转的现象少了很多。它会在提示的引导下重新审视屏幕或者尝试不同的路径来完成目标。5.4 MCP连接的不稳定性MCP服务端如果崩溃或者网络超时代理可能直接卡在等待结果的状态。这里我的建议是MCP请求必须要有超时和重试而且重试要有上限。我设置的是超时30秒重试两次仍失败就把错误信息返回给模型让模型决定是换工具还是将该步骤标记为失败。这个机制在实际使用中帮我避免了很多挂死等待。5.5 模型“看错”界面信息的场景视觉模型并非总能把屏幕上的文字识别准确尤其是较小字号、对比度较低的界面。这里我的经验是不要把关键任务的判断完全压在视觉上能通过MCP读取结构化数据的尽量走MCP。视觉方案是大而全的兜底但结构化数据源永远是更可靠的选项。6. 跑起来的方法与下一步真正想做的扩展说了这么多可能你最关心的是这东西到底怎么跑起来实际使用方式其实非常朴素python agent_solo.py --task 打开本地数据库工具查询users表中最近注册的10个用户将结果整理成JSON输出 --model gpt-4o执行之后代理会先截屏分析当前桌面环境是否有目标应用如果没有它会尝试启动该应用接下来就是一路“截屏-决策-执行-验证”的循环。整个过程的日志会实时打印你能看到它每一步在想什么、要做什么也可以随时中断。我现在给它设定的典型使用场景大概是这几个需要在多个系统之间切换操作时的“搬砖”任务对不提供API的旧系统做界面自动化结合MCP对项目代码进行理解和改造后的GUI回归快速演示、教学场景让AI自己演示一段操作流程。至于不适合的场景说实话也有比如超高频、超精准的操作它不如直接写脚本稳定色彩识别要求极高的场景它也会力不从心还有涉及敏感数据的高危操作不建议放给它自动执行。认清工具的边界比追求“什么都能干”更重要。下一步我自己想做的扩展大致有三条线第一条是让动作反馈更细粒度不只依赖再次截屏而是结合窗口事件判断操作是否成功减少等待时间第二条是强化跨应用状态管理让代理记住自己当前打开了哪些窗口减少在多个界面间来回找目标的开销第三条是工具调用偏好学习根据历史经验自动调整是否优先使用MCP还是GUI视觉。这三条线我都已经摸到了一些可行的实现思路下面有空的时候会逐步落地。
返回列表