核心内容摘要
铜铜铜锵锵锵:一场穿越时空的听觉盛宴,唤醒你内心深处的澎湃
本文详细介绍如何使用Gemini 3 Flash等通用大模型从零开始构建PC GUI Agent。
通过实现多模态对话、记忆功能、坐标归一化和LangGraph工作流打造能理解屏幕内容并自动执行操作的AI助手。
整个实现过程简单易用半小时即可完成无需复杂感知系统直接利用大模型的GUI理解能力实现跨应用的自动化操作。
前言什么是GUI Agent简单来说就是一个能够看懂屏幕mobile/pc/web并进行自动操作的AI Agent。
比如用户发送指令“整理文件”PC GUI Agent就可以基于纯GUI界面理解页面内容进行逐步决策操作直到完成用户任务。
随着GUI Agent的应用前景逐渐明朗GUI Agent在25年的发展很迅猛其中一个比较明显的新趋势是通用大模型也在训练GUI上的能力因此搭建一个好用的GUI Agent变得越来越简单。
本文介绍如何使用通用大模型API快速从0到1搭建一个可用性强的PC GUI Agent可以用于自动完成PC任务操作。
GUI Agent基础原理介绍一个GUI Agent通常包含以下几个部分设备环境Environment比如手机、电脑这些用来操作的设备是交互的 “载体“本文仅以电脑为示例。
感知系统Preceptor负责 “接收信息“这主要是针对GUI能力较弱的通用大模型。
而Gemini 3 Flash这样本身具备较强GUI页面理解能力的大模型通常不再需要单独设计感知系统。
大模型决策器相当于 “大脑”—— 结合知识系统里存的信息分析感知到的界面内容决定要完成任务下一步应该如何操作。
交互系统Operator相当于“手脚” —— 把大模型的决策结果操作动作坐标转化为具体的操作与设备进行交互。
为了完成一个完整的任务通常需要进行 “感知-决策-执行” 的多次循环直到任务完成。
下文具体介绍PC GUI Agent的搭建方式。
逐步搭建GUI Agent
最简单的模型对话在开始构建复杂的Agent之前我们先实现最基础的功能让AI看懂一张图片。
# utils/model.pyimport osimport base64from openai import OpenAIfrom typing import List, Dict, AnyAPI_KEY os.getenv(OPENAI_API_KEY)BASE_URL os.getenv(OPENAI_BASE_URL)class LVMChat: 多模态大模型聊天类 def __init__(self, api_key: str API_KEY, base_url: str BASE_URL, model: str gemini-3-flash-preview): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model def _encode_image(self, image_path: str) - str: 将图片编码为base64 with open(image_path, rb) as image_file: return base
b64encode(image_file.read()).decode(utf-
def get_multimodal_response(self, text: str, image_paths: str) - str: 最简单的图文对话 Args: text: 你的问题 image_paths: 图片路径 Returns: 模型的回答 #
加载图片 base64_image self._encode_image(image_paths) #
构建消息 messages [{ role: user, content: [ {type: image_url, image_url: {url: fdata:image/jpeg;base64,{base64_image}}}, {type: text, text: text} ] }] #
调用API response self.client.chat.completions.create( modelself.model, messagesmessages ) return response.choices[0].message.content测试一下chat LVMChat()response chat.get_multimodal_response( text这张图片里输入框坐标, image_pathsimage.png)print(response)注意看这里模型本身是有定位能力的也就是说可以直接输出操作对象的坐标位置所以不需要格外的感知器来锚定具体UI元素。
加入记忆 - 让AI记住上下文上面的代码有个问题AI每次都是失忆的不记得之前说过什么。
对于GUI Agent来说这是很致命的。
举个简单的例子任务“在输入框输入‘你好’”如果Agent没有记忆那么后序执行不记得自己已经决策过输入操作就会陷入输入的循环在输入框中多次输入‘你好’而不会停止。
相反有上文记忆的Agent知道自己前一次已经决策了输入操作结合当前页面内容判断本次应为“finish”指令就不会陷入输入循环。
为此我们需要升级 LVMChat 类class LVMChat: 支持会话记忆的多模态聊天类 def __init__(self, api_key: str API_KEY, base_url: str BASE_URL, model: str gemini-3-flash-preview): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model # 核心改动添加会话历史记录 self.conversation_history: List[Dict[str, Any]] [] def get_multimodal_response(self, text: str, image_paths: str, use_history: bool False) - str: 支持记忆的图文对话 Args: text: 你的问题 image_paths: 图片路径 use_history: 是否使用会话历史记住之前的对话 #
加载图片并构建当前消息 base64_image self._encode_image(image_paths) current_message { role: user, content: [ {type: image_url, image_url: {url: fdata:image/jpeg;base64,{base64_image}}}, {type: text, text: text} ] } #
关键如果启用历史把之前的对话也带上 if use_history: messages self.conversation_history [current_message] print(f 使用历史上下文共 {len(self.conversation_history)} 条) else: messages [current_message] #
调用API response self.client.chat.completions.create( modelself.model, messagesmessages ) result response.choices[0].message.content #
更新历史记录 if use_history: self.conversation_history.append(current_message) self.conversation_history.append({ role: assistant, content: result }) return result def clear_history(self): 清空记忆 self.conversation_history []记忆的原理# 第一轮对话conversation_history [ {role: user, content: [图片1, 在输入框输入‘你好’]}, {role: assistant, content: {Thought 输入框在页面中间位置我需要输入文案, Action: type(content\你好\)}}]# 第二轮对话时把历史也带上messages conversation_history [ {role: user, content: [图片2, 在输入框输入‘你好’]}]# 现在AI能看到完整的对话链知道自己做过什么决策。
在第二轮返回:{Thought 上一轮已经完成输入操作并且文案已经正确显示在输入框任务已经完成, Action: finished}
搭建Agent框架现在组装成一个完整的Agent我们使用LangGraph来构建工作流。
1Agent的核心循环一个GUI Agent的工作流程是这样的2执行器Operator首先我们需要一个能够真正控制鼠标键盘的工具类。
在这里强烈推荐pyautogui, 模拟键鼠操作非常简单易用。
但是有一个坑需要注意输入操作需要用复制粘贴操作而不是打字因为 pyautogui.write() 不支持中文而用剪贴板粘贴可以完美支持中英文、特殊字符# operator/execute.py - import pyautoguiimport pyperclipimport mssimport time# 允许鼠标移动到屏幕角落默认会触发fail-safepyautogui.FAILSAFE Falseclass Operation: GUI操作工具类 def click(self, x: int, y: int): 点击指定坐标 print(f️ 点击坐标 ({x}, {y})) pyautogui.click(xx, yy) def input(self, text: str): 输入文本使用粘贴方式支持中文 print(f⌨️ 输入: {text}) pyperclip.copy(text) # 复制到剪贴板 pyautogui.hotkey(command, v) # Mac用commandWindows用ctrl def screenshot(self, save_path: str): 截图并保存 with mss.mss() as sct: sct.shot(outputsave_path) print(f 截图已保存: {save_path}) def hotkey(self, *keys): 按下组合键如ctrlc print(f⌨️ 按下组合键: { .join(keys)}) pyautogui.hotkey(*keys) def wait(self, seconds: float
1.
: 等待指定时间 print(f⏱️ 等待 {seconds} 秒...) time.sleep(seconds)3PromptGUI Agent的提示词告诉AI应该如何思考和行动的规则需要定义好操作空间和输出格式我在UI-TARS官方Prompt的基础上做了很小的改动增加了Output Example因为我发现Gemini 3 Flash使用UI-TARS的prompt输出格式不是很稳定。
# utils/prompts.pyCOMPUTER_USE_UITARS You are a GUI agent. You are given a task and your action history, with screenshots. You need to perform the next action to complete the task.## Action Spaceclick(pointpointx1 y1/point)left_double(pointpointx1 y1/point)right_single(pointpointx1 y1/point)drag(start_pointpointx1 y1/point, end_pointpointx2 y2/point)hotkey(keyctrl c) # Split keys with a space and use lowercase. Also, do not use more than 3 keys in one hotkey action.type(contentxxx) # Use escape characters \\, \\\, and \\n in content part to ensure we can parse the content in normal python string format. If you want to submit your input, use \\n at the end of content. scroll(pointpointx1 y1/point, directiondown or up or right or left) # Show more information on the direction side.wait() #Sleep for 5s and take a screenshot to check for any changes.finished(contentxxx) # Use escape characters \\, \\, and \\n in content part to ensure we can parse the content in normal python string format.## Note- Use Chinese in Thought part.- Write a small plan and finally summarize your next action (with its target element) in one sentence in Thought part.- One action per turn.## Output Example## User Instruction{instruction}4坐标归一化不同电脑屏幕分辨率不同1920x
2560x1440等GUI Agent往往会采用千归一化坐标的策略进行处理。
因此拿到大模型的返回之后需要将归一化坐标 (
范围) 还原成实际像素坐标# AI返回归一化坐标: (500,
- 表示屏幕中心# 在1920x1080屏幕上: (500,
- (960,
# 在2560x1440屏幕上: (500,
- (1280,
def normalize_coords(self, x: int, y: int) - tuple[int, int]: 将归一化坐标(0-
转换为实际像素坐标 actual_x int(x /
1
0 * self.screen_width) actual_y int(y /
1
0 * self.screen_height) return actual_x, actual_y5LangGraphLangGraph是一个用于构建Agent工作流的框架图(Graph)的方式定义Agent的执行逻辑。
# 传统方式硬编码def run_agent(): whilenot finished: screenshot() decision model_decide() execute(decision) if check_finished(): break# LangGraph方式声明式workflow StateGraph(AgentState)workflow.add_node(screenshot, take_screenshot)workflow.add_node(decide, model_decide)workflow.add_node(execute, execute_action)workflow.add_edge(screenshot, decide)workflow.add_edge(decide, execute)workflow.add_conditional_edges(execute, should_continue, {...})优势✅ 逻辑清晰可视化工作流 ✅ 易于修改添加节点/改变流程很简单 ✅ 状态管理自动在节点间传递状态。
AgentState 是整个工作流的记忆载体在各个节点间传递# Step 1: 截图节点state { instruction: 打开浏览器, screenshot_path: s/step_
png, step: 1, finished: False}# Step 2: 决策节点接收state更新后返回state { **state, # 保留之前的数据 thought: 需要点击Chrome图标, action: click(pointpoint100 200/point)}# Step 3: 执行节点state { **state, # 继续保留 # 执行点击操作...}6Agent主程序现在把所有模块组装起来#!/usr/bin/env python3# -*- coding: utf-8 -*-GUI Agent - 自动化GUI测试Agent截图 - 模型决策 - 解析Action - 执行 - 循环直到finishedimport reimport jsonfrom datetime import datetimefrom typing import TypedDictfrom pathlib import Pathfrom langgraph.graph import StateGraph, ENDfrom operator.execute import Operationfrom utils.model import LVMChat, Modelfrom utils.prompts import COMPUTER_USE_UITARS# 定义Stateclass AgentState(TypedDict): instruction: str # 用户指令 screenshot_path: str # 当前截图路径 step: int # 当前步骤 thought: str # 模型思考 action: str # 模型输出的动作 finished: bool # 是否完成class GUIAgent: GUI自动化Agent def __init__(self, instruction: str, model_name: str Model.GOOGLE_GEMINI_3_FLASH_PREVIEW.value): self.instruction instruction self.operation Operation() self.lvm_chat LVMChat(modelmodel_name) self.s_dir Path(s) self.s_dir.mkdir(exist_okTrue) # 获取屏幕尺寸用于坐标映射 import pyautogui self.screen_width, self.screen_height pyautogui.size() print(f️ 屏幕尺寸: {self.screen_width}x{self.screen_height}) def normalize_coords(self, x: int, y: int) - tuple[int, int]: 将归一化坐标(0-
转换为实际像素坐标 actual_x int(x /
1
0 * self.screen_width) actual_y int(y /
1
0 * self.screen_height) print(f 归一化坐标 ({x}, {y}) - 实际坐标 ({actual_x}, {actual_y})) return actual_x, actual_y def take_screenshot(self, state: AgentState) - AgentState: 步骤1: 截图并保存 step state.get(step,
1 timestamp datetime.now().strftime(%Y%m%d_%H%M%S) screenshot_path str(self.s_dir / fstep_{step}_{timestamp}.png) self.operation.screenshot(screenshot_path) return { **state, instruction: self.instruction, screenshot_path: screenshot_path, step: step, finished: False } def model_decide(self, state: AgentState) - AgentState: 步骤2: 模型决策自动使用会话历史 prompt COMPUTER_USE_UITARS.format(instructionstate[instruction]) # 调用多模态模型use_historyTrue 自动保留上下文 response self.lvm_chat.get_multimodal_response( textprompt, image_pathsstate[screenshot_path], res_formatjson, use_historyTrue# 启用会话历史模型会记住之前的所有交互 ) print(f\n Step {state[step]} - 模型响应:\n{response}\n) # 解析JSON响应 try: result json.loads(response) thought result.get(Thought, ) action result.get(Action, ) except json.JSONDecodeError: # 如果不是JSON格式尝试正则提取 thought_match re.search(rThought:\s*([^]*), response) action_match re.search(rAction:\s*([^]*), response) thought thought_match.group(
if thought_match else action action_match.group(
if action_match else return { **state, thought: thought, action: action } def execute_action(self, state: AgentState) - AgentState: 步骤3: 解析并执行动作 action state[action] ifnot action: print(⚠️ 没有可执行的动作) return {**state, finished: True} # 检查是否完成 if action.startswith(finished(): content_match re.search(rfinished\(content([^]*)\), action) content content_match.group(
if content_match else任务完成 print(f✅ 任务完成: {content}) return {**state, finished: True} # 解析并执行动作 try: self._parse_and_execute(action) except Exception as e: print(f❌ 执行动作失败: {e}) print(f 动作: {action}) return state def _parse_and_execute(self, action: str): 解析动作字符串并执行 print(f 执行动作: {action}) # click(pointpointx y/point) 或 click(pointx y) if action.startswith(click(): # 尝试带标签的格式 point_match re.search(rpoint(\d)\s(\d)/point, action) ifnot point_match: # 尝试不带标签的格式 point_match re.search(rpoint[\](\d)\s(\d)[\], action) if point_match: x, y int(point_match.group(
), int(point_match.group(
) actual_x, actual_y self.normalize_coords(x, y) self.operation.click(actual_x, actual_y) else: print(f⚠️ 无法解析点击坐标: {action}) # left_double(pointpointx y/point) 或 double_click(pointx y) elif action.startswith(left_double(): # 尝试带标签的格式 point_match re.search(rpoint(\d)\s(\d)/point, action) ifnot point_match: # 尝试不带标签的格式 point_match re.search(rpoint[\](\d)\s(\d)[\], action) if point_match: x, y int(point_match.group(
), int(point_match.group(
) actual_x, actual_y self.normalize_coords(x, y) self.operation.double_click(actual_x, actual_y) else: print(f⚠️ 无法解析双击坐标: {action}) # type(contentxxx) elif action.startswith(type(): content_match re.search(rcontent[\]([^\]*)[\], action) if content_match: text content_match.group(
# 处理转义字符 text text.replace(r\, ).replace(r\, ).replace(r\n, \n) self.operation.input(text) # hotkey(keyctrl c) elif action.startswith(hotkey(): key_match re.search(rkey[\]([^\]*)[\], action) if key_match: keys key_match.group(
.split() self.operation.hotkey(*keys) # scroll(pointpointx y/point, directiondown) 或 scroll(pointx y, directiondown) elif action.startswith(scroll(): # 尝试带标签的格式 point_match re.search(rpoint(\d)\s(\d)/point, action) ifnot point_match: # 尝试不带标签的格式 point_match re.search(rpoint[\](\d)\s(\d)[\], action) direction_match re.search(rdirection[\]([^\]*)[\], action) if point_match and direction_match: x, y int(point_match.group(
), int(point_match.group(
) actual_x, actual_y self.normalize_coords(x, y) direction direction_match.group(
# 移动到位置并滚动 import pyautogui pyautogui.moveTo(actual_x, actual_y) scroll_amount 3if direction in [up, left] else-3 pyautogui.scroll(scroll_amount) # wait() elif action.startswith(wait(): self.operation.wait(seconds
# drag(start_pointpointx1 y1/point, end_pointpointx2 y2/point) elif action.startswith(drag(): # 尝试带标签的格式 start_match re.search(rstart_point[\]point(\d)\s(\d)/point[\], action) end_match re.search(rend_point[\]point(\d)\s(\d)/point[\], action) ifnot start_match: # 尝试不带标签的格式 start_match re.search(rstart_point[\](\d)\s(\d)[\], action) end_match re.search(rend_point[\](\d)\s(\d)[\], action) if start_match and end_match: x1, y1 int(start_match.group(
), int(start_match.group(
) x2, y2 int(end_match.group(
), int(end_match.group(
) actual_x1, actual_y1 self.normalize_coords(x1, y
actual_x2, actual_y2 self.normalize_coords(x2, y
import pyautogui pyautogui.moveTo(actual_x1, actual_y
pyautogui.drag(actual_x2 - actual_x1, actual_y2 - actual_y1, duration
0.
# 等待一下让界面响应 self.operation.wait(seconds
def should_continue(self, state: AgentState) - str: 判断是否继续循环 returnendif state.get(finished, False) elsecontinue def run(self): 运行Agent # 构建graph workflow StateGraph(AgentState) # 添加节点 workflow.add_node(screenshot, self.take_screenshot) workflow.add_node(decide, self.model_decide) workflow.add_node(execute, self.execute_action) # 添加边 workflow.set_entry_point(screenshot) workflow.add_edge(screenshot, decide) workflow.add_edge(decide, execute) workflow.add_conditional_edges( execute, self.should_continue, { continue: screenshot, end: END } ) # 编译并运行 app workflow.compile() print(f 开始执行任务: {self.instruction}\n) # 设置递归限制为100步 config {recursion_limit: 100} final_state app.invoke( {instruction: self.instruction, step: 0}, configconfig ) print(f\n 任务完成! 共执行 {final_state[step]} 步) return final_stateif __name__ __main__: agent GUIAgent(instruction打开浏览器查询GUI, 找到wikipedia的介绍页面进行查看) agent.run()
执行效果文章前言部分的demo, 模型的部分决策内容如下:对于中间步骤模型会利用到上文的内容与当前页面状态一起作为决策依据最后如果模型判断任务完成会输出finished指令程序停止
四、
总结本文采用简洁易用的方案搭建了一个 PC 端的 GUI Agent该 Agent 不仅能在 Windows 和 macOS 系统上直接运行还可操作 Web 应用。
得益于以 pyautogui键鼠模拟作为核心执行器该 Agent 能够实现跨应用的操作能力。
未来可通过补充滑动、拖拽等更多样的交互方式并结合知识库的构建进一步强化其针对特定业务场景的适配性打造功能更强大的 GUI Agent。
如何学习大模型 AI 由于新岗位的生产效率要优于被取代岗位的生产效率所以实际上整个社会的生产效率是提升的。
但是具体到个人只能说是“最先掌握AI的人将会比较晚掌握AI的人有竞争优势”。
这句话放在计算机、互联网、移动互联网的开局时期都是一样的道理。
我在一线科技企业深耕十二载见证过太多因技术卡位而跃迁的案例。
那些率先拥抱 AI 的同事早已在效率与薪资上形成代际优势我意识到有很多经验和知识值得分享给大家也可以通过我们的能力和经验解答大家在大模型的学习中的很多困惑。
我们整理出这套AI 大模型突围资料包✅ 从零到一的 AI 学习路径图✅ 大模型调优实战手册附医疗/金融等大厂真实案例✅ 百度/阿里专家闭门录播课✅ 大模型当下最新行业报告✅ 真实大厂面试真题✅ 2025 最新岗位需求图谱所有资料 ⚡️ 朋友们如果有需要《AI大模型入门进阶学习资源包》下方扫码获取~① 全套AI大模型应用开发视频教程包含提示工程、RAG、LangChain、Agent、模型微调与部署、DeepSeek等技术点② 大模型系统化学习路线作为学习AI大模型技术的新手方向至关重要。
正确的学习路线可以为你节省时间少走弯路方向不对努力白费。
这里我给大家准备了一份最科学最系统的学习成长路线图和学习规划带你从零基础入门到精通③ 大模型学习书籍文档学习AI大模型离不开书籍文档我精选了一系列大模型技术的书籍和学习文档电子版它们由领域内的顶尖专家撰写内容全面、深入、详尽为你学习大模型提供坚实的理论基础。
④ AI大模型最新行业报告2025最新行业报告针对不同行业的现状、趋势、问题、机会等进行系统地调研和评估以了解哪些行业更适合引入大模型的技术和应用以及在哪些方面可以发挥大模型的优势。
⑤ 大模型项目实战配套源码学以致用在项目实战中检验和巩固你所学到的知识同时为你找工作就业和职业发展打下坚实的基础。
⑥ 大模型大厂面试真题面试不仅是技术的较量更需要充分的准备。
在你已经掌握了大模型技术之后就需要开始准备面试我精心整理了一份大模型面试题库涵盖当前面试中可能遇到的各种技术问题让你在面试中游刃有余。
以上资料如何领取为什么大家都在学大模型最近科技巨头英特尔宣布裁员2万人传统岗位不断缩减但AI相关技术岗疯狂扩招有
年经验大厂薪资就能给到50K*20薪不出1年“有AI项目经验”将成为投递简历的门槛。
风口之下与其像“温水煮青蛙”一样坐等被行业淘汰不如先人一步掌握AI大模型原理应用技术项目实操经验“顺风”翻盘这些资料真的有用吗这份资料由我和鲁为民博士(北京清华大学学士和美国加州理工学院博士)共同整理现任上海殷泊信息科技CEO其创立的MoPaaS云平台获Forrester全球’强劲表现者’认证服务航天科工、国家电网等1000企业以第一作者在IEEE Transactions发表论文50篇获NASA JPL火星探测系统强化学习专利等35项中美专利。
本套AI大模型课程由清华大学-加州理工双料博士、吴文俊人工智能奖得主鲁为民教授领衔研发。
资料内容涵盖了从入门到进阶的各类视频教程和实战项目无论你是小白还是有些技术基础的技术人员这份资料都绝对能帮助你提升薪资待遇转行大模型岗位。
以上全套大模型资料如何领取