ARTICLE DETAIL

资讯详情

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

AI编码代理实战:从GUI自动化到MCP协议的单文件Agent构建指南

AI编码代理实战:从GUI自动化到MCP协议的单文件Agent构建指南 上个月朋友甩给我一个需求他们公司有个老旧的桌面进销存系统每天要重复往表格里填一百多条数据每单要点七八次鼠标他问我能不能用AI自动搞定。我第一反应是让AI写个脚本结果试了一圈现有的AI编码代理coding agent发现它们大多只会读写文件、跑终端命令面对一个只能在GUI里操作的老软件完全无能为力。于是我才动手做了这个免费的小工具一个支持操控GUI和MCP协议、还能单文件运行的AI编码代理。简单说这个项目就是一个自带Agent循环的智能体程序你给它一句自然语言指令它能自动拆解任务、调用工具、观察界面状态、再决定下一步动作。它最大的特点是三个一是能像人一样操作桌面图形界面不是只写代码二是实现了MCPModel Context Protocol客户端可以直接接入文件系统、浏览器、数据库、GitHub这类第三方工具服务器三是整个程序打包成一个可执行文件下载就能跑不用装Node、不用配Python环境依赖。这篇文章我会把整个项目的设计思路、核心实现、以及我踩过的坑完整写出来。适合两类人看一类是想自己搭建AI编码代理的开发者另一类是需要在桌面GUI场景里做自动化的朋友。我不准备把代码全贴出来项目还在打磨但核心架构和关键实现都会讲清楚照着这个思路基本能复现一版。1. 从装环境装到崩溃说起为什么需要单文件AI代理1.1 AI编码代理的现状扫描现在的AI编码代理大致分成三类我列个表方便对比类型代表产品强项痛点终端型Claude Code、Codex CLI、Codeium重构代码、跑命令、git操作只管命令行碰不了GUI依赖Node等运行时IDE型通义灵码、GitHub Copilot补全、对话、上下文感知绑死在IDE里自动化能力弱自定义Agent各类LangChain/Dify项目灵活、可接工具依赖满天飞配置复杂不适合分发给非技术同事我遇到的实际问题是朋友要的不是帮忙写代码而是直接把那个老系统里的表单填完。这种任务的落点根本不是代码是GUI操作。终端型代理做不了IDE型代理更不可能。那为什么不用现成的RPA工具我也考虑过但RPA的录制脚本太脆界面稍微动一下位置就全废。AI代理的优势是看情况决策——按钮位置变了、弹窗内容变了它能重新理解。1.2 三个硬性需求在动手之前我给自己定了三个需求后面所有技术选型都围着它们转能操控GUI屏幕截图、识别界面元素、点击、输入、拖拽跨Windows和macOS。支持MCP协议不自己造一堆私有工具接口直接对接MCP生态今天接文件系统明天接浏览器互操作。单文件运行好朋友那边不懂Python不可能让他去装依赖、配虚拟环境。我要给他一个文件双击就能跑。第三个需求其实是最反常规的。很多开发者写工具习惯了一堆依赖、环境变量、Docker但做出来之后根本没法分发给别人用。我后来单文件化的时候吃了不少苦具体方案放在第5节。1.3 单文件带来的分发优势单文件不只是一个懒人福利它直接改变了这个工具的使用方式拷到U盘就能走朋友电脑上没有Python也能跑用PyInstaller打包的exe或者有Python但不想装依赖也能跑用zipapp打包的.pyz。升级就是替换一个文件不用处理依赖冲突。我可以在自己的机器上编译好直接发给别人源码不暴露虽然对这种个人项目不太重要。所以整个项目的第一原则是先把运行形态想清楚再决定怎么写代码。我后面所有模块设计都考虑了要不要把某个依赖打进去这跟平时写web服务是完全不同的思路。2. Agent核心循环规划、执行、观察是如何串起来的2.1 工具调用是骨架不是锦上添花AI编码代理跟普通聊天机器人的本质区别在于它每回答一步都有可能真的去执行一个操作然后根据操作结果继续推理。这个推理-行动-观察-再推理的循环是Agent的骨架。很多人第一次写Agent会犯一个错用生成一大段JSON然后解析的方式做工具调用。这在早期少量工具时还行工具一多、参数一复杂各种解析bug就来了。正确做法是用LLM官方的function calling / tool calling能力让模型结构化成要调用哪个工具、传什么参数而不是让它自由格式输出JSON。2.2 主循环最小实现我的核心循环非常朴素伪代码大概是这样的def agent_loop(user_request: str, max_steps: int 20) - str: messages [system_prompt(), user_request] for step in range(max_steps): response llm.chat(messages, toolstool_schema) if response.is_final_answer: # 模型没有要求调用工具 return response.text for call in response.tool_calls: result dispatch_tool(call.name, call.arguments) messages.append(tool_result_message(call.id, result)) log(f[step {step}] {call.name}({call.arguments}) {truncate(result)}) return 已达到最大步数任务可能未完成关键就三步把距今为止的所有消息发给LLM附带工具定义列表。LLM决定是直接回答还是调用某个工具。如果是调用工具我执行它把结果以tool_result消息塞回对话进入下一轮。循环本身没有任何魔法真正的复杂度在两个地方工具调度和上下文管理。2.3 上下文窗口管理与工具结果截断Agent跑长了之后消息列表会越来越膨胀。尤其是GUI截图返回的base64字符串动不动几十KB两三轮就把上下文塞爆了。我的处理方案是三级工具结果截断纯文本结果最多保留2000字符超过的部分头尾各留500字符中间用...省略N字符...代替。截图工具返回的图片单独走最近一张可见策略上一轮的截图自动从消息里删掉只留描述。历史压缩超过10轮之后把最老的消息用LLM做一次摘要替换成一条history_summary消息。实测下来摘要损失可以接受。强制终止单任务最多20步。超过直接停防止Agent陷入死循环这种场景在GUI自动化时特别常见后面会讲。2.4 工具注册表设计工具不是一个一个硬编码的if-else我做成了一张注册表每个工具是一个带元数据的函数tool( namegui_click, description点击屏幕上指定坐标位置坐标基于屏幕截图, parameters{ x: {type: integer, description: 横坐标像素值}, y: {type: integer, description: 纵坐标像素值}, double: {type: boolean, description: 是否双击, default: False} } ) def gui_click(x: int, y: int, double: bool False): import pyautogui pyautogui.click(x, y, clicks2 if double else 1)注册表本质上就是函数名字、描述、参数JSON Schema、可执行函数四个字段的组合。MCP工具接入的时候就是把外部工具描述翻译成同一套Schema执行时再翻译回去。这个抽象在后面省了非常多的事。3. GUI操控让代理真正看得见、点得着桌面应用3.1 两条技术路线屏幕视觉 vs 辅助功能接口GUI自动化有两条完全不同的路线我一开始都试了各自的优缺点很鲜明路线原理优点缺点视觉路线截屏 - 多模态模型识别元素 - 映射坐标点击跨平台、通用性强、不依赖具体应用识别精度受模型影响DPI换算容易出问题辅助功能路线Windows UI Automation / macOS Accessibility元素定位精准、支持读取文本属性很多老软件没有无障碍接口跨平台要写两套我最终选择以视觉路线为主、辅助功能为辅。原因很现实那个进销存系统是Delphi写的二十年前的老程序UI Automation完全读不到任何控件信息但视觉路线只要看得见就能操作不挑应用。3.2 实测最佳组合mss截图 多模态模型 pyautogui视觉路线的三个核心动作是截图、看、点。截图我用的是mss库它是目前Python里最快的跨平台截屏方案比pyautogui自带的截图快好几倍支持多显示器import mss def capture_screen() - str: with mss.mss() as sct: # 截取主显示器全屏保存为临时文件 sct.shot(mon1, output/tmp/agent_screen.png) return /tmp/agent_screen.png截完图把这张图连同用户指令一起发给多模态模型让它在图的坐标层面做标注response llm.chat( messages[ {role: user, content: [ {type: text, text: 请定位表单中客户名称输入框的中心坐标}, {type: image, image_path: /tmp/agent_screen.png} ]} ] )模型返回类似输入框位于(530, 420)之后调用pyautogui执行点击和输入import pyautogui def gui_type_text(text: str): pyautogui.hotkey(ctrl, a) # 全选已有内容 pyautogui.typewrite(text, interval0.02)这套组合跑通之后整个Agent就有了眼睛和手。实际用下来的体验是对于表单填写、按钮点击、菜单导航这类任务视觉方案的准确率在90%左右已经可以规模化用了。3.3 坐标换算和DPI缩放坑这是我踩得最深的一个坑必须单独拿出来说。在Windows上如果显示器缩放比例不是100%比如常见的125%、150%那么mss截屏返回的像素坐标和pyautogui实际点击的物理坐标是不一致的。后果就是模型明明看到按钮在(500, 400)鼠标却精确地点到了按钮偏左下的位置。解决方案是做一个坐标换算层import ctypes def get_win_scaling() - float: try: ctypes.windll.shcore.SetProcessDpiAwareness(1) scale ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100 return scale except Exception: return 1.0 def screen_to_physical(screen_x: int, screen_y: int) - tuple[int, int]: scale get_win_scaling() return int(screen_x * scale), int(screen_y * scale)每台机器启动Agent时先检测一次缩放比之后所有GUI点击坐标都先换算再执行。macOS上还有Retina屏幕截图分辨率是逻辑分辨率的两倍也是同样的逻辑。我后来把截图统一放在逻辑分辨率下进行这样模型看到的坐标就是pyautogui能直接用的坐标。3.4 权限配置清单GUI自动化涉及系统级权限不同系统的配置差异很大我整理了一份清单Windows管理员权限不是必须的但有些老的桌面软件以管理员运行时屏幕内容会被隔离Agent只能截到黑屏。这种情况需要用提权方式启动Agent。macOS需要到系统设置-隐私与安全性里给终端打开两个权限一个是辅助功能控制鼠标键盘一个是屏幕录制截屏。这两个权限不打开程序会静默失败不报错只是动作无效果排查起来很痛苦。LinuxX11需要有显示环境权限有些桌面环境还需要装xdotool辅助点击。4. MCP协议接入从工具函数到开放生态4.1 MCP是什么、解决了什么问题MCP全称Model Context Protocol模型上下文协议。它的核心思想非常简单把模型能力和外部工具解耦用一个统一协议连接起来。我自己的理解是MCP就是把工具做成了USB接口模型是电脑插上哪个设备就能用哪个设备。以前我要给Agent加一个读取数据库能力得自己写连接池、写查询函数、写权限控制。现在只要跑一个现成的MCP server告诉Agent这个server提供哪些工具就能直接用。这个协议现在生态已经非常丰富了。文件系统、SQLite、GitHub、浏览器控制、Figma、各类数据库都有官方或社区的MCP server实现。我接入这些东西再也不用自己写适配代码。4.2 最小MCP客户端握手、列工具、调用MCP基于JSON-RPC 2.0。我一开始是自己用WebSocket和stdio实现的客户端后来发现官方提供了Python SDK直接用更省心import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def connect_server(command: str, args: list[str]): server_params StdioServerParameters(commandcommand, argsargs, envNone) reader, writer await stdio_client(server_params) session await ClientSession(reader, writer) await session.initialize() tools [] async for tool in session.list_tools(): tools.append({ name: tool.name, description: tool.description or , parameters_schema: tool.inputSchema }) return session, tools建立连接就三步启动子进程、握手初始化、拉取工具列表。拉回来的工具描述直接转成Agent的tool schema我们的Agent循环完全不用改就能使用MCP server上的工具。调用工具时通过session.call_tool()转发参数就是JSON格式result await session.call_tool(read_text_file, {path: /tmp/foo.log})4.3 把MCP工具接入Agent工具表接入MCP之后马上会遇到一个实际问题工具重名。比如很多MCP server都有read_file、write_file这类通用命名如果同时挂了三个serverAgent的tool list里就会出现三份同样的名字模型调用时会混乱。我的处理方案是给每个MCP工具加命名空间前缀def build_mcp_tool(session_name: str, tool) - dict: namespaced_name f{session_name}__{tool.name} return { name: namespaced_name, # ... dispatch: lambda args: asyncio.run(call_mcp_tool(session_name, tool.name, args)) }比如文件系统server的read_file会变成filesystem__read_file浏览器server的click会变成browser__click。这样即使两个server有同功能工具Agent也能区分而且提示词里可以明确告诉它查数据库用database__search写代码用filesystem__write_file。4.4 调试MCP server的三个常见错误MCP调试是我整个项目里最耗时的一环踩过的坑基本上就三类server往stdout打印日志导致协议崩溃。MCP的stdio传输规定了子进程的stdout只准输出JSON-RPC消息如果server代码里有个console.log(hello)客户端解析直接断掉。解决方式写MCP server时日志一律写stderr或日志文件永远别碰stdout。握手超时。很多node实现的MCP server首次启动要下载依赖几十秒没响应。我给initialize加了30秒超时并在启动前先用命令行手动跑一遍npx xxx确保依赖已就绪。工具参数类型不匹配。MCP的call_tool参数要求是JSON对象有些server内部是用严格类型校验的传1和传1结果完全不同。我给Agent的提示词里加了一条规则调用MCP工具前先检查参数的JSON类型与Schema定义一致。接入MCP最大的价值是今天我可以接一个Playwright MCP让Agent自己开浏览器操作网页明天接一个数据库MCP让Agent查线上数据后天再接一个Figma MCP读取设计稿——这些都是现成的能力我用同一套代码就全部打通了。5. 单文件打包的完整操作zipapp、内嵌依赖与启动体验5.1 zipapp原理Python官方自带的单文件方案单文件打包我首选了Python标准库的zipapp模块而不是PyInstaller。原因有三个生成的.pyz文件很小不带Python解释器只有代码和依赖、跨平台效果一致、而且排查问题方便。zipapp的原理并不复杂.pyz本质上就是一个在前面拼了一段Python引导代码的zip压缩包。执行时Python解释器先运行这段引导代码把zip包挂在sys.path上然后去执行包里的__main__.py。5.2 依赖内嵌与二进制解压真正麻烦的是依赖。zipapp虽然能把.py文件直接放进zip但动态链接库和.pyd/.so这类二进制文件不能直接从zip里加载因为它们需要被真实文件系统加载。我的解决方案是bootstrapper解压模式把这些二进制依赖在启动时自动解压到系统临时目录然后往sys.path插入路径import sys, tempfile, pathlib, zipfile def bootstrap_binary_deps(): # 项目内嵌了依赖解压标记文件 marker pathlib.Path(__file__).parent / _embedded_deps.json if not marker.exists(): return # 源码运行时不需要 import json deps json.loads(marker.read_text()) dest pathlib.Path(tempfile.mkdtemp(prefixagent_deps_)) for rel_path in deps: # 从当前zip中读取二进制文件解压到临时目录 data pathlib.Path(__file__).parent.joinpath(rel_path).read_bytes() target dest / rel_path target.parent.mkdir(parentsTrue, exist_okTrue) target.write_bytes(data) sys.path.insert(0, str(dest)) # 让动态库可见这个函数必须在import任何第三方库之前执行。比如mss这个库在Windows下有mss.dll、pyautogui依赖的一些底层的Windows API库都要走这条路。而纯Python的依赖比如mcp官方SDK的纯Python部分可以直接把包源码放进zip包的根目录不需要解压import就能用。5.3 完整打包命令和启动体验打包脚本大致是这样# 1. 把项目源码组织成 src/agent/ 包结构 # 2. 把纯Python依赖直接复制到 src/agent/_vendor/ 下 # 3. 把二进制依赖放置在 src/agent/_bin/ 下 # 4. 写一个 __main__.py 作为唯一入口 python -m zipapp src/agent \ -p /usr/bin/env python3 \ -o ai-coding-agent.pyz \ -m agent.__main__:main加上-p参数后Linux和macOS上直接chmod x ai-coding-agent.pyz就能当可执行文件运行。Windows上双击会调用关联的Python解释器也可以改名成ai-coding-agent.pyz后手动运行python ai-coding-agent.pyz --gui-demo如果目标是给完全没有Python环境的Windows用户我会额外用PyInstaller生成ai-coding-agent.exe。但日常我自己用还是.pyz因为体积小20MB以内、启动快改动代码后重新打包只需要一秒。打包完成后一定要做三件事在一台干净的机器上测试没有开发依赖。检查临时目录解压是否成功看启动日志。测试MCP相关的动态库能否正常加载。因为MCP的客户端SDK涉及网络IO某些环境下需要额外拷贝证书相关文件。6. 实测场景与翻车案例哪些任务真正能提效6.1 场景一GUI表单批量填写回到开头那个进销存系统。实际跑通的流程是这样的用MCP的文件系统工具读取Excel里的客户订单数据。对每条记录用GUI工具打开系统、定位新增订单按钮、点击。视觉模型识别表单各个输入框位置依次输入客户名、金额、备注。点击保存然后截图确认系统弹出了保存成功的提示框。一轮任务的Agent步骤大概是15-20步单条数据耗时约15秒。原来是人工1分钟一条提效4倍而且是无人值守的。6.2 场景二浏览器MCP自动化测试我在自己的web项目里试了用浏览器MCP server Agent跑前端测试。流程是让Agent打开某个页面点击注册按钮填测试账号故意输错两次密码观察校验提示。最有价值的一次是Agent发现了一个bug——点击提交后没有出现校验错误提示原因是前端两个字段的name属性对不上导致校验规则没绑定。这个bug如果人工回归测试可能要翻好几轮页面才能发现。Agent通过截图-观察-再操作的循环能注意到肉眼容易略过的界面细节。6.3 场景三日志定位与补丁生成这个场景是我的日常程序报错后让Agent先用MCP的文件系统工具读日志文件再用数据库MCP查关联数据最后直接用工具链修改代码文件并运行测试。整个过程Agent是自主完成的我只需要在最后review diff。和之前相比排查这个报错数据是哪来的这类问题时间从半小时压缩到了十几分钟。6.4 翻车案例三个必须写下来的教训翻车1多显示器坐标偏移我的副屏幕在笔记本左边Windows显示器的虚拟屏幕坐标系是有负坐标的。mss截图只能按显示器1、2分别截模型看到的是独立画面返回的坐标却是基于单屏的。我在点击时直接把坐标用在全局坐标系里结果每次都点到主屏幕的对应位置。修复方式截图前记录每块屏幕的物理偏移量模型返回坐标后先加上该屏幕的偏移再换算成物理坐标。代码很简单但这个问题不实测根本发现不了。翻车2GUI自动化的死循环有一次Agent在填写表单时截图识别保存按钮的位置但点击后页面没反应因为某个必填项没填Agent截图看到还是保存按钮又点又没反应来回打了十几个回合。我的修复方案是在系统提示词里加了一条规则同一个操作连续执行两次后如果界面状态没有明显变化必须停止并报告操作可能无效建议尝试切换方案并在主循环里加了动作去重检测。现在这个情况基本不会再发生了。翻车3MCP工具超时导致整个Agent卡死MCP的call_tool如果遇到网络慢的server会一直阻塞。而我的Agent主循环是同步的导致整个程序假死。我后来给每个MCP工具调用套了asyncio超时控制超时后返回一个特殊的工具错误结果让LLM自行决策是跳过还是重试而不是卡死整个任务async def call_mcp_with_timeout(session, tool_name, args, timeout60): try: return await asyncio.wait_for(session.call_tool(tool_name, args), timeouttimeout) except asyncio.TimeoutError: return {_error: f工具 {tool_name} 调用超时请检查server状态或更换方案}7. 安全边界与后续计划能控GUI的Agent必须先谈风险7.1 为什么能跑GUI的Agent要先谈安全当Agent能截图、能点鼠标、能敲键盘、还能执行本地命令的时候它实际上已经拥有了使用这台电脑的完整权限。这意味着如果指令设计有缺陷、或者prompt被注入比如网页内容喂给Agent后诱导它执行恶意操作后果会非常直接。我在项目初期就定了一条原则能力越强安全约束就要做得越硬不能指望模型自己守规矩。7.2 我的安全机制目前项目里有五层防护命令黑名单终端命令执行器内置正则黑名单rm -rf、format、diskpart等一律拒绝执行。这些命令连用户确认的机会都不会给。危险操作确认删除文件、覆盖文件、执行系统级命令默认需要用户在控制台输一次y确认。Agent在等待期间会提示我需要执行XX操作是否同意。MCP server白名单默认不启动任何MCP server必须用户显式指定才会挂载。Agent对MCP工具的使用不受干预但能用哪些工具由用户决定。工作目录沙箱文件操作、代码运行默认限制在一个指定目录内。Agent想读写工作目录之外的文件需要显式路径并在日志中留下记录。全量操作日志每一步工具调用、参数、关键截图、最终结果都会写成日志文件。出事后可以复盘Agent到底干了什么。这些机制不需要做得特别复杂但它们保证了即使模型想干坏事或者被诱导它也翻不出太远。7.3 已知不足和后续路线这个Agent目前的局限也很明显视觉识别上限对过小、过密、或者图标化的按钮识别率不稳定。有些中文软件里按钮文字细、间距小模型会把确定看成取消。这类情况我会在提示词里强制要求点击前先描述相对位置不要只给坐标。内存占用偏高因为内嵌了多模态模型调用的客户端、MCP SDK和各类GUI库打包后的.pyz大约20MB运行时要额外解压依赖到临时目录内存峰值能到300MB左右。对现代机器不算什么但在老电脑上会有点吃力。MCP依赖网络资源很多MCP server要用npx或uvx启动头一次会下载依赖首次启动会有十几秒延迟。后续我计划做三件事一是把GUI操作从坐标点击升级到控件语义操作优先用辅助功能接口读取界面元素属性配合视觉做兜底二是增加一个人机协作模式复杂步骤让我确认后再执行三是把Agent做成MCP server本身这样其他Agent也能通过MCP来调用我的GUI能力Agent之间互相协作。如果你也想动手做一个类似的东西我的建议是先想清楚你的单一核心场景是什么然后让最小闭环跑通再去追MCP生态和单文件化。我最初也是一步一步把这三块拼起来的——先是命令行跑通Agent再接入GUI操作最后加MCP和打包。别一开始就求大而全那只会把自己淹没在依赖地狱里。
返回列表