跳转至

GUI Agent

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

学习目标:理解GUI Agent的核心概念、技术架构与实现方法,掌握视觉感知、动作规划、工具操作的完整链路,能够构建Web/桌面自动化Agent。

📌 定位说明:对标 hello-agents GUI Agent专题章节,我们在覆盖其核心内容的基础上,增加了最新的多模态GUI Agent架构、实战代码和性能评测方法。


目录


1. GUI Agent概述

1.1 什么是GUI Agent

Text Only
GUI Agent vs 传统Agent
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

传统Agent:
  用户指令 → LLM推理 → API调用 → 返回结果
  特点: 通过结构化API与系统交互

GUI Agent:
  用户指令 → 视觉感知(截图) → LLM推理 → UI操作(点击/输入) → 观察结果 → 循环
  特点: 像人一样"看"屏幕并"操作"界面

核心差异:
┌──────────────┬─────────────────┬─────────────────┐
│ 维度         │ API Agent       │ GUI Agent       │
├──────────────┼─────────────────┼─────────────────┤
│ 感知方式     │ 结构化数据      │ 屏幕截图/DOM    │
│ 操作方式     │ API调用         │ 鼠标/键盘操作   │
│ 通用性       │ 需要专用API     │ 任何有GUI的软件 │
│ 稳定性       │ 高(API不易变)   │ 中(UI会改版)    │
│ 理解难度     │ 低              │ 高(需视觉理解)  │
│ 应用范围     │ 有API的系统     │ 所有GUI软件     │
└──────────────┴─────────────────┴─────────────────┘

1.2 GUI Agent的核心能力

Text Only
┌─────────────────────────────────────────┐
│            GUI Agent 能力栈              │
├─────────────────────────────────────────┤
│                                         │
│  🔴 任务规划  Task Planning              │
│     ├── 分解用户自然语言指令             │
│     ├── 生成多步操作计划                 │
│     └── 动态调整策略                     │
│                                         │
│  🟡 视觉感知  Visual Perception          │
│     ├── 屏幕截图理解                     │
│     ├── UI元素识别与定位(Grounding)      │
│     └── 文字识别(OCR)                    │
│                                         │
│  🟢 动作执行  Action Execution           │
│     ├── 鼠标操作(点击/拖拽/滚动)        │
│     ├── 键盘输入(打字/快捷键)           │
│     └── 等待/验证操作结果                │
│                                         │
│  🔵 记忆推理  Memory & Reasoning         │
│     ├── 操作历史记录                     │
│     ├── 错误恢复与重试                   │
│     └── 跨步骤上下文关联                 │
│                                         │
└─────────────────────────────────────────┘

1.3 发展脉络

阶段 代表工作 特点
预LLM时代 Selenium/Appium 规则驱动,脆弱的定位策略
早期LLM WebGPT (2021) LLM作为决策核心,文本描述UI
视觉LLM CogAgent (2023) 多模态直接理解截图
专用Agent SeeClick, OS-Atlas (2024) 专门训练的GUI Grounding模型
通用框架 Claude Computer Use, UFO (2024) 通用操作系统级Agent
最新前沿 UI-TARS, ShowUI (2025) 端到端视觉-动作模型

2. 视觉感知与UI理解

2.1 输入表示方式

Text Only
GUI Agent 的三种输入表示:
━━━━━━━━━━━━━━━━━━━━━━━━━━━

方式1: 纯截图 (Screenshot-only)
  ┌──────────────────┐
  │  📷 屏幕截图      │ → 多模态LLM理解
  └──────────────────┘
  优点: 通用,不依赖DOM
  缺点: 小元素难以识别

方式2: 结构化表示 (DOM/Accessibility Tree)
  <button id="submit" class="btn-primary">
    Submit Order
  </button>
  优点: 精确,包含语义信息
  缺点: 不是所有应用都有

方式3: 混合表示 (Screenshot + 结构化)
  截图 + 标注框 + Accessibility信息
  ← 目前最佳实践

2.2 Set-of-Mark (SoM) 标注

Python
"""
Set-of-Mark: 在截图上标注可交互元素的编号
这使得LLM可以通过编号引用具体元素
"""

from PIL import Image, ImageDraw, ImageFont
import json

class SetOfMarkAnnotator:
    """
    SoM标注器:在截图上标注UI元素
    """

    def __init__(self):
        self.elements = []

    def annotate_screenshot(self, screenshot_path: str,
                            elements: list) -> Image.Image:
        """
        在截图上标注可交互元素

        Args:
            screenshot_path: 截图路径
            elements: UI元素列表, 每个元素包含:
                - bbox: [x1, y1, x2, y2] 边界框
                - type: 元素类型 (button/input/link/...)
                - text: 元素文本
        Returns:
            标注后的图片
        """
        img = Image.open(screenshot_path)
        draw = ImageDraw.Draw(img)

        self.elements = elements
        colors = {
            'button': '#FF4444',
            'input': '#44FF44',
            'link': '#4444FF',
            'text': '#FFFF44',
            'other': '#FF44FF'
        }

        for i, elem in enumerate(elements):
            x1, y1, x2, y2 = elem['bbox']
            color = colors.get(elem.get('type', 'other'), '#FFFFFF')

            # 绘制边界框
            draw.rectangle([x1, y1, x2, y2], outline=color, width=2)

            # 绘制编号标签
            label = f"[{i}]"
            draw.rectangle([x1, y1-18, x1+30, y1], fill=color)
            draw.text((x1+2, y1-16), label, fill='white')

        return img

    def get_element_description(self) -> str:
        """生成元素描述文本,供LLM理解"""
        desc = "可交互元素列表:\n"
        for i, elem in enumerate(self.elements):
            desc += f"  [{i}] {elem.get('type', 'unknown')}: "
            desc += f"\"{elem.get('text', '')}\" "
            desc += f"位置: {elem['bbox']}\n"
        return desc

# 使用示例
annotator = SetOfMarkAnnotator()
elements = [
    {"bbox": [100, 200, 250, 240], "type": "input", "text": "搜索框"},
    {"bbox": [260, 200, 340, 240], "type": "button", "text": "搜索"},
    {"bbox": [100, 260, 300, 290], "type": "link", "text": "高级搜索选项"},
]

desc = annotator.get_element_description()
print(desc)

2.3 UI Grounding(元素定位)

Python
"""
UI Grounding: 将自然语言描述映射到具体的UI元素坐标

方法1: 基于OCR + 空间推理
方法2: 基于多模态LLM直接预测坐标
方法3: 基于专用grounding模型 (如SeeClick, OS-Atlas)
"""

class UIGrounding:
    """UI元素定位"""

    @staticmethod
    def ocr_based_grounding(screenshot, target_text: str,
                             ocr_results: list) -> dict:
        """
        基于OCR的元素定位
        1. OCR识别所有文字
        2. 匹配目标文字
        3. 返回对应位置
        """
        for result in ocr_results:
            if target_text.lower() in result['text'].lower():
                return {
                    'found': True,
                    'bbox': result['bbox'],
                    'confidence': result['confidence'],
                    'center': (
                        (result['bbox'][0] + result['bbox'][2]) / 2,
                        (result['bbox'][1] + result['bbox'][3]) / 2
                    )
                }
        return {'found': False}

    @staticmethod
    def build_grounding_prompt(task: str, screenshot_desc: str) -> str:
        """
        构造多模态LLM的grounding prompt
        让LLM直接输出要操作的坐标
        """
        prompt = f"""你是一个GUI操作助手。根据用户任务和屏幕截图,确定需要操作的UI元素位置。

当前屏幕内容:
{screenshot_desc}

用户任务: {task}

请回答:
1. 需要操作的元素是什么?
2. 该元素的坐标位置 [x, y](屏幕中心为参考点)
3. 需要执行的操作类型(click/type/scroll)
4. 如果是输入操作,输入的内容是什么?

以JSON格式回答:
{{"element": "...", "position": [x, y], "action": "...", "value": "..."}}
"""
        return prompt

print("UI Grounding核心挑战:")
print("1. 小图标/小按钮的精确定位")
print("2. 动态内容(如弹窗、加载中状态)")
print("3. 多语言界面的OCR准确性")
print("4. 相似元素的区分(多个按钮/链接)")

3. 动作空间设计

3.1 标准化动作空间

Python
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Tuple

class ActionType(Enum):
    """GUI Agent标准动作类型"""
    CLICK = "click"              # 单击
    DOUBLE_CLICK = "double_click" # 双击
    RIGHT_CLICK = "right_click"  # 右键
    TYPE = "type"                # 输入文字
    PRESS = "press"              # 按键(如Enter, Tab)
    HOTKEY = "hotkey"            # 快捷键(如Ctrl+C)
    SCROLL = "scroll"            # 滚动
    DRAG = "drag"                # 拖拽
    WAIT = "wait"                # 等待
    SCREENSHOT = "screenshot"    # 截图(用于观察)
    DONE = "done"                # 任务完成
    FAIL = "fail"                # 放弃/失败

@dataclass
class GUIAction:
    """标准化GUI动作"""
    action_type: ActionType
    position: Optional[Tuple[int, int]] = None     # (x, y) 坐标
    text: Optional[str] = None                      # 输入文本
    key: Optional[str] = None                       # 按键名
    direction: Optional[str] = None                 # 滚动方向
    end_position: Optional[Tuple[int, int]] = None  # 拖拽终点
    element_id: Optional[int] = None                # SoM元素编号

    def to_dict(self):
        return {k: v for k, v in self.__dict__.items() if v is not None}  # 字典推导+__dict__:过滤None值序列化对象属性

    def __str__(self):
        if self.action_type == ActionType.CLICK:
            if self.position is None:
                return "Click (no position)"
            return f"Click at ({self.position[0]}, {self.position[1]})"
        elif self.action_type == ActionType.TYPE:
            pos_str = f"({self.position[0]}, {self.position[1]})" if self.position else "(no position)"
            return f"Type '{self.text}' at {pos_str}"
        elif self.action_type == ActionType.PRESS:
            return f"Press {self.key}"
        elif self.action_type == ActionType.SCROLL:
            return f"Scroll {self.direction}"
        elif self.action_type == ActionType.DONE:
            return "Task completed"
        return f"{self.action_type.value}: {self.to_dict()}"

# 示例动作序列
actions = [
    GUIAction(ActionType.CLICK, position=(150, 220)),
    GUIAction(ActionType.TYPE, position=(150, 220), text="Python GUI Agent"),
    GUIAction(ActionType.PRESS, key="Enter"),
    GUIAction(ActionType.WAIT),
    GUIAction(ActionType.CLICK, position=(200, 350), element_id=3),
    GUIAction(ActionType.DONE),
]

print("示例动作序列:")
for i, action in enumerate(actions):
    print(f"  Step {i+1}: {action}")

3.2 动作解析器

Python
import re
import json

class ActionParser:
    """
    从LLM输出解析GUI动作

    支持两种格式:
    1. JSON格式: {"action": "click", "position": [100, 200]}
    2. 自然语言格式: "Click on the search button at (100, 200)"
    """

    @staticmethod
    def parse_json(llm_output: str) -> GUIAction:
        """解析JSON格式的动作"""
        # 提取JSON
        json_match = re.search(r'\{[^{}]+\}', llm_output)
        if not json_match:
            raise ValueError("No JSON found in output")

        try:
            data = json.loads(json_match.group())
        except json.JSONDecodeError as e:
            raise ValueError(f"Invalid JSON in LLM output: {e}")

        action_map = {
            'click': ActionType.CLICK,
            'type': ActionType.TYPE,
            'press': ActionType.PRESS,
            'scroll': ActionType.SCROLL,
            'done': ActionType.DONE,
        }

        action_type = action_map.get(data.get('action', ''), ActionType.CLICK)
        position = tuple(data['position']) if 'position' in data else None

        return GUIAction(
            action_type=action_type,
            position=position,
            text=data.get('text'),
            key=data.get('key'),
            element_id=data.get('element_id'),
        )

    @staticmethod
    def parse_natural_language(text: str) -> GUIAction:
        """解析自然语言描述的动作"""
        text = text.lower()

        # 提取坐标
        coord_match = re.search(r'\((\d+),\s*(\d+)\)', text)
        position = tuple(map(int, coord_match.groups())) if coord_match else None  # map将字符串坐标转为整数元组,匹配失败返回None

        if 'click' in text:
            return GUIAction(ActionType.CLICK, position=position)
        elif 'type' in text or 'input' in text:
            # 提取输入文本
            input_match = re.search(r"['\"](.+?)['\"]", text)
            input_text = input_match.group(1) if input_match else ""
            return GUIAction(ActionType.TYPE, position=position, text=input_text)
        elif 'scroll' in text:
            direction = 'down' if 'down' in text else 'up'
            return GUIAction(ActionType.SCROLL, direction=direction)
        elif 'done' in text or 'finish' in text:
            return GUIAction(ActionType.DONE)

        return GUIAction(ActionType.CLICK, position=position)

# 测试解析
parser = ActionParser()

json_output = '我需要点击搜索按钮 {"action": "click", "position": [260, 220], "element_id": 1}'
action = parser.parse_json(json_output)
print(f"JSON解析: {action}")

nl_output = "Click on the search box at (150, 220)"
action = parser.parse_natural_language(nl_output)
print(f"NL解析: {action}")

4. 主流GUI Agent架构

4.1 架构总览

Text Only
主流GUI Agent架构对比
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

方案A: 纯视觉方案 (CogAgent / SeeClick)
  截图 → 多模态LLM → 坐标+动作
  ✓ 最通用,不需要DOM
  ✗ 精度受限于视觉分辨率

方案B: DOM + LLM (WebAgent / MindAct)
  DOM/HTML → 文本LLM → 元素选择+动作
  ✓ 精确,利用结构信息
  ✗ 仅限Web,DOM可能很大

方案C: 混合方案 (SeeAct / UFO)
  截图 + DOM → 多模态LLM → 动作
  ✓ 兼顾通用性和精确性
  ✗ 信息冗余,上下文长

方案D: Computer Use (Claude / OpenAI)
  截图 → 大型多模态模型 → 系统级操作
  ✓ 最强泛化能力
  ✗ 成本高,延迟大

4.2 基础GUI Agent框架实现

Python
import time
from abc import ABC, abstractmethod
from typing import List, Dict, Any

class GUIAgent:
    """
    通用GUI Agent框架

    核心循环: Observe → Think → Act → Observe → ...
    """

    def __init__(self, llm_client, max_steps: int = 20):
        self.llm_client = llm_client  # LLM调用接口
        self.max_steps = max_steps
        self.action_history: List[Dict] = []
        self.observation_history: List[str] = []

    def run(self, task: str) -> Dict[str, Any]:
        """
        执行GUI任务

        Args:
            task: 用户的自然语言任务描述
        Returns:
            执行结果字典
        """
        print(f"📋 任务: {task}")
        print("=" * 60)

        for step in range(self.max_steps):
            print(f"\n--- Step {step + 1}/{self.max_steps} ---")

            # 1. Observe: 获取当前屏幕状态
            observation = self._observe()
            self.observation_history.append(observation)
            print(f"🔍 观察: {observation[:100]}...")

            # 2. Think: 让LLM决定下一步动作
            action = self._think(task, observation)
            print(f"🧠 决策: {action}")

            # 3. 检查是否完成
            if action.action_type == ActionType.DONE:
                print("\n✅ 任务完成!")
                return {"status": "success", "steps": step + 1,
                        "history": self.action_history}

            if action.action_type == ActionType.FAIL:
                print("\n❌ 任务失败")
                return {"status": "failed", "steps": step + 1,
                        "history": self.action_history}

            # 4. Act: 执行动作
            self._act(action)
            self.action_history.append({
                "step": step + 1,
                "action": str(action),
                "observation": observation[:200]
            })

            # 等待UI响应
            time.sleep(0.5)

        print("\n⚠️ 达到最大步数限制")
        return {"status": "max_steps", "steps": self.max_steps,
                "history": self.action_history}

    def _observe(self) -> str:
        """
        获取当前屏幕状态
        实际实现中:截图 + OCR + 元素检测
        """
        # 这里是框架代码,实际需要:
        # 1. 截图: pyautogui.screenshot()
        # 2. OCR: 识别文字
        # 3. UI检测: 识别可交互元素
        # 4. SoM标注: 给元素编号
        return "当前屏幕状态描述(实际为截图+标注)"

    def _think(self, task: str, observation: str) -> GUIAction:
        """
        调用LLM进行决策
        """
        # 构造prompt
        prompt = self._build_prompt(task, observation)

        # 调用LLM(伪代码)
        # response = self.llm_client.chat(prompt, images=[screenshot])
        # action = ActionParser.parse_json(response)

        # 示例返回
        return GUIAction(ActionType.CLICK, position=(100, 200))

    def _act(self, action: GUIAction):
        """
        执行GUI操作
        实际使用 pyautogui 或 playwright
        """
        # import pyautogui
        if action.action_type == ActionType.CLICK:
            # pyautogui.click(action.position[0], action.position[1])
            print(f"  ⚡ 执行点击: {action.position}")
        elif action.action_type == ActionType.TYPE:
            # pyautogui.click(action.position[0], action.position[1])
            # pyautogui.typewrite(action.text)
            print(f"  ⚡ 输入文本: '{action.text}'")
        elif action.action_type == ActionType.SCROLL:
            # pyautogui.scroll(-3 if action.direction == 'down' else 3)
            print(f"  ⚡ 滚动: {action.direction}")

    def _build_prompt(self, task: str, observation: str) -> str:
        """构造决策prompt"""
        history_text = ""
        for h in self.action_history[-5:]:  # 最近5步历史
            history_text += f"  Step {h['step']}: {h['action']}\n"

        prompt = f"""你是一个GUI自动化Agent。你需要通过操作图形界面来完成用户任务。

## 用户任务
{task}

## 操作历史
{history_text if history_text else "  (这是第一步)"}

## 当前屏幕
{observation}

## 可用动作
- click(x, y): 点击指定坐标
- type(x, y, text): 在指定位置输入文本
- press(key): 按下键盘键
- scroll(direction): 上下滚动
- done(): 任务已完成
- fail(): 无法完成任务

## 请决定下一步动作
分析当前屏幕并选择最合适的操作。以JSON格式回答:
{{"thought": "分析推理过程", "action": "click/type/press/scroll/done/fail", "position": [x, y], "text": "可选输入文本"}}
"""
        return prompt

# 框架使用示例
print("GUI Agent框架示例:")
agent = GUIAgent(llm_client=None, max_steps=10)
print("Agent已创建,核心循环: Observe → Think → Act")
print("实际运行需要: LLM API + 屏幕操作库(pyautogui/playwright)")

5. Web自动化Agent实战

5.1 基于Playwright的Web Agent

Python
"""
Web GUI Agent: 基于Playwright实现Web自动化

Playwright优势:
- 支持Chrome/Firefox/Safari
- 内置等待机制
- 可获取DOM和截图
- 支持无头模式
"""

# pip install playwright
# python -m playwright install

class WebGUIAgent:
    """
    Web自动化Agent
    结合Playwright操作 + LLM决策
    """

    def __init__(self, llm_client=None, headless: bool = False):
        self.llm_client = llm_client
        self.headless = headless
        self.browser = None
        self.page = None

    async def setup(self):
        """初始化浏览器"""
        from playwright.async_api import async_playwright

        pw = await async_playwright().start()
        self.browser = await pw.chromium.launch(headless=self.headless)
        self.page = await self.browser.new_page()

    async def navigate(self, url: str):
        """导航到指定URL"""
        await self.page.goto(url, wait_until='networkidle')

    async def observe(self) -> dict:
        """
        获取页面状态
        """
        # 1. 截图
        screenshot = await self.page.screenshot()

        # 2. 获取可交互元素
        elements = await self.page.evaluate("""() => {
            const interactable = document.querySelectorAll(
                'a, button, input, select, textarea, [role="button"], [onclick]'
            );
            return Array.from(interactable).map((el, idx) => {
                const rect = el.getBoundingClientRect();
                return {
                    id: idx,
                    tag: el.tagName.toLowerCase(),
                    type: el.type || '',
                    text: (el.textContent || el.value || el.placeholder || '').trim().slice(0, 50),
                    bbox: [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height],
                    visible: rect.width > 0 && rect.height > 0
                };
            }).filter(el => el.visible);
        }""")

        # 3. 获取页面标题和URL
        title = await self.page.title()
        url = self.page.url

        return {
            "screenshot": screenshot,
            "elements": elements,
            "title": title,
            "url": url,
            "element_description": self._format_elements(elements)
        }

    def _format_elements(self, elements: list) -> str:
        """格式化元素描述"""
        desc = []
        for el in elements[:30]:  # 限制数量
            text = el['text'][:30] if el['text'] else '(empty)'
            desc.append(f"  [{el['id']}] <{el['tag']}> {text}")
        return "\n".join(desc)

    async def execute_action(self, action: dict):
        """
        执行Web操作
        """
        action_type = action.get('action')

        if action_type == 'click':
            element_id = action.get('element_id')
            if element_id is not None:
                elements = await self.page.query_selector_all(
                    'a, button, input, select, textarea, [role="button"], [onclick]'
                )
                if element_id < len(elements):
                    await elements[element_id].click()
            elif 'position' in action:
                x, y = action['position']
                await self.page.mouse.click(x, y)

        elif action_type == 'type':
            element_id = action.get('element_id')
            text = action.get('text', '')
            elements = await self.page.query_selector_all('input, textarea')
            if element_id is not None and element_id < len(elements):
                await elements[element_id].fill(text)

        elif action_type == 'press':
            key = action.get('key', 'Enter')
            await self.page.keyboard.press(key)

        elif action_type == 'scroll':
            direction = action.get('direction', 'down')
            delta = 300 if direction == 'down' else -300
            await self.page.mouse.wheel(0, delta)

        # 等待页面响应
        await self.page.wait_for_load_state('networkidle')

    async def cleanup(self):
        """关闭浏览器"""
        if self.browser:
            await self.browser.close()

# 使用示例(伪代码)
print("""
Web Agent 使用示例:

async def main():
    agent = WebGUIAgent(llm_client=openai_client)
    await agent.setup()
    await agent.navigate("https://www.google.com")

    # 观察-决策-执行循环
    observation = await agent.observe()

    # LLM决策
    action = llm_decide(observation)

    # 执行
    await agent.execute_action(action)

    await agent.cleanup()
""")

5.2 DOM简化策略

Python
class DOMSimplifier:
    """
    DOM简化器

    问题: 完整DOM可能有几万个节点,超过LLM上下文限制
    解决: 智能过滤和压缩
    """

    @staticmethod
    def simplify_html(html: str, max_tokens: int = 4000) -> str:
        """
        简化HTML,保留关键交互元素
        """
        from html.parser import HTMLParser

        # 保留的标签
        keep_tags = {
            'a', 'button', 'input', 'select', 'option', 'textarea',
            'form', 'h1', 'h2', 'h3', 'h4', 'p', 'label', 'img',
            'nav', 'main', 'article', 'section', 'div'  # 结构性标签
        }

        # 移除的属性(减少噪音)
        remove_attrs = {
            'style', 'class', 'data-', 'aria-', 'tabindex',
            'onclick', 'onchange'  # 事件处理器
        }

        # 简化逻辑(基于规则)
        simplified = []
        lines = html.split('\n')
        for line in lines:
            line = line.strip()
            if not line:
                continue
            # 检查是否包含有用标签
            for tag in keep_tags:
                if f'<{tag}' in line.lower() or f'</{tag}>' in line.lower():
                    # 清理属性
                    simplified.append(line[:200])  # 限制行长度
                    break

        result = '\n'.join(simplified)

        # 截断到token限制
        if len(result) > max_tokens * 4:  # 粗略估算
            result = result[:max_tokens * 4] + "\n... (truncated)"

        return result

    @staticmethod
    def extract_accessibility_tree(page_content: str) -> str:
        """
        提取Accessibility Tree(更结构化的表示)

        Accessibility Tree比DOM更精炼:
        - 只包含对用户可见/可交互的元素
        - 包含语义角色(role)信息
        - 自然层级结构
        """
        # 示例Accessibility Tree结构
        example = """
[document] Google
  [navigation] 主导航
    [link] Gmail
    [link] 图片
    [button] 登录
  [main]
    [img] Google Logo
    [textbox] 搜索框 (focused)
    [button] Google 搜索
    [button] 手气不错
  [footer]
    [link] 关于Google
    [link] 隐私
        """
        return example

print("DOM简化策略:")
print("1. 只保留可交互元素 (input/button/link)")
print("2. 移除样式相关属性 (class/style)")
print("3. 保留语义信息 (aria-label/placeholder)")
print("4. 使用Accessibility Tree替代原始DOM")
print("5. 限制深度和节点数量")

6. 桌面自动化Agent

6.1 操作系统级Agent

Python
"""
桌面GUI Agent: 操作任何桌面应用
代表项目: Microsoft UFO, Claude Computer Use
"""

class DesktopAgent:
    """
    桌面自动化Agent骨架

    核心依赖:
    - pyautogui: 鼠标/键盘控制
    - Pillow: 截图处理
    - pytesseract: OCR文字识别
    """

    def __init__(self, llm_client=None):
        self.llm_client = llm_client
        self.screen_width = 1920   # 示例分辨率
        self.screen_height = 1080

    def take_screenshot(self):
        """截取全屏或区域截图"""
        # import pyautogui
        # screenshot = pyautogui.screenshot()
        # return screenshot
        pass

    def find_element_by_text(self, text: str):
        """基于OCR找到文字位置"""
        # import pytesseract
        # screenshot = self.take_screenshot()
        # data = pytesseract.image_to_data(screenshot, output_type=pytesseract.Output.DICT)
        #
        # for i, word in enumerate(data['text']):
        #     if text.lower() in word.lower():
        #         x = data['left'][i] + data['width'][i] // 2
        #         y = data['top'][i] + data['height'][i] // 2
        #         return (x, y)
        pass

    def execute(self, action: GUIAction):
        """在操作系统级别执行操作"""
        # import pyautogui
        #
        # if action.action_type == ActionType.CLICK:
        #     pyautogui.click(*action.position)
        # elif action.action_type == ActionType.TYPE:
        #     pyautogui.click(*action.position)
        #     pyautogui.typewrite(action.text, interval=0.05)
        # elif action.action_type == ActionType.HOTKEY:
        #     pyautogui.hotkey(*action.key.split('+'))
        # elif action.action_type == ActionType.SCROLL:
        #     delta = -3 if action.direction == 'down' else 3
        #     pyautogui.scroll(delta)
        pass

print("""
桌面Agent典型应用场景:

1. 办公自动化
   - "打开Excel,在A1输入今天日期,保存为report.xlsx"
   - "打开邮箱,给张三发一封会议通知"

2. 软件测试
   - "打开计算器,验证1+1=2"
   - "在Photoshop中打开图片,调整亮度+20"

3. 数据录入
   - "从PDF表格中提取数据,填入ERP系统"
   - "批量处理100张发票的信息录入"

4. 跨应用工作流
   - "从浏览器下载文件→解压→导入到数据库"
""")

6.2 UFO框架介绍

Text Only
Microsoft UFO (UI-Focused Agent)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

UFO是微软开源的Windows桌面Agent框架:

架构:
┌─────────────────────────────────────┐
│          AppAgent (应用层Agent)      │
│  理解特定应用(Word/Excel/Browser)   │
├─────────────────────────────────────┤
│          HostAgent (宿主Agent)       │
│  管理多应用协调,任务分配            │
├─────────────────────────────────────┤
│          Control Module              │
│  Win32 API / UI Automation / COM    │
└─────────────────────────────────────┘

特点:
1. 双Agent架构: HostAgent负责跨应用协调,AppAgent负责应用内操作
2. 利用Windows UI Automation API获取精确的元素信息
3. 支持自然语言任务描述
4. 自动学习新应用的操作方式

与Claude Computer Use的区别:
- UFO: 利用Windows特有API,更精确,仅限Windows
- Claude: 纯视觉方案,更通用,跨平台

7. 移动端GUI Agent

7.1 移动端特性

Text Only
移动端GUI Agent挑战:
━━━━━━━━━━━━━━━━━━━━━━

与桌面/Web的差异:
┌──────────────┬──────────────┬──────────────┐
│ 维度         │ 桌面端       │ 移动端       │
├──────────────┼──────────────┼──────────────┤
│ 输入方式     │ 鼠标+键盘    │ 触摸+手势    │
│ 屏幕大小     │ 大(1920+)    │ 小(360-428)  │
│ UI信息获取   │ DOM/WinAPI   │ Android    │
│              │              │ View层级     │
│ 滚动方式     │ 滚轮/滚动条  │ 滑动手势     │
│ 特殊操作     │ 右键/拖拽    │ 长按/双指缩放│
│ 响应延迟     │ 低           │ 动画多,需等│
└──────────────┴──────────────┴──────────────┘

移动端额外操作:
  - 滑动(Swipe): 上下左右
  - 长按(Long Press)
  - 捏合缩放(Pinch)
  - 系统返回键(Back)
  - 通知栏操作
  - App切换

7.2 移动端Agent架构

Python
class MobileGUIAction:
    """移动端GUI动作扩展"""

    ACTIONS = {
        "tap": "单击",
        "long_press": "长按",
        "swipe_up": "上滑",
        "swipe_down": "下滑",
        "swipe_left": "左滑",
        "swipe_right": "右滑",
        "type": "输入文本",
        "back": "返回",
        "home": "回到主屏",
        "app_switch": "切换应用",
        "scroll_to": "滚动到元素",
    }

print("""
移动端GUI Agent代表工作:

1. AppAgent (2023)
   - 多模态LLM + Android UI层级
   - 通过探索学习APP操作方式
   - 建立操作知识库

2. CogAgent (2023)
   - 高分辨率视觉编码
   - 端到端的UI理解和操作

3. DigiRL (2024)
   - 使用RL训练移动端Agent
   - 在真实Android环境中在线学习

4. UI-TARS (2025)
   - 统一的视觉-动作模型
   - 支持Web/桌面/移动端
""")

8. 评测基准与挑战

8.1 评测基准

Text Only
GUI Agent评测基准全景
━━━━━━━━━━━━━━━━━━━━━━━━━

Web端:
┌──────────────┬────────────────────────────────┐
│ 基准         │ 说明                            │
├──────────────┼────────────────────────────────┤
│ MiniWoB++    │ 100+简单Web任务(点击/填表)    │
│ Mind2Web     │ 真实网站,2000+任务             │
│ WebArena     │ 自托管网站环境(GitLab等)       │
│ VisualWebArena│ 图文混合Web任务               │
│ WebVoyager   │ 真实网站端到端任务              │
└──────────────┴────────────────────────────────┘

桌面端:
┌──────────────┬────────────────────────────────┐
│ OSWorld      │ Ubuntu/Windows/MacOS跨系统       │
│ WindowsArena │ Windows应用任务                  │
│ ScreenSpot   │ GUI元素定位准确率                │
└──────────────┴────────────────────────────────┘

移动端:
┌──────────────┬────────────────────────────────┐
│ AITW         │ Android in the Wild              │
│ AndroidEnv   │ Android环境标准接口              │
│ Mobile-Bench │ 移动端综合评测                   │
└──────────────┴────────────────────────────────┘

8.2 评测指标

Python
class GUIAgentEvaluator:
    """GUI Agent评测器"""

    @staticmethod
    def compute_metrics(predictions, ground_truths):
        """
        计算GUI Agent评测指标

        核心指标:
        1. 任务成功率 (Task Success Rate)
        2. 步骤效率 (Step Efficiency)
        3. 元素定位精度 (Grounding Accuracy)
        """
        metrics = {}

        # 1. 任务成功率:完成目标的比例
        successes = sum(1 for p, g in zip(predictions, ground_truths)
                       if p['status'] == 'success')
        metrics['success_rate'] = successes / len(predictions)

        # 2. 步骤效率:实际步数/最优步数
        efficiencies = []
        for p, g in zip(predictions, ground_truths):
            if p['status'] == 'success' and g['optimal_steps'] > 0:
                efficiency = g['optimal_steps'] / p['steps']
                efficiencies.append(min(efficiency, 1.0))
        metrics['step_efficiency'] = (sum(efficiencies) / len(efficiencies)
                                      if efficiencies else 0)

        # 3. 元素定位精度(IoU)
        ious = []
        for p, g in zip(predictions, ground_truths):
            if 'bbox' in p and 'bbox' in g:
                iou = GUIAgentEvaluator._compute_iou(p['bbox'], g['bbox'])
                ious.append(iou)
        metrics['grounding_accuracy'] = (sum(ious) / len(ious)
                                         if ious else 0)

        return metrics

    @staticmethod
    def _compute_iou(box1, box2):
        """计算两个边界框的IoU"""
        x1 = max(box1[0], box2[0])
        y1 = max(box1[1], box2[1])
        x2 = min(box1[2], box2[2])
        y2 = min(box1[3], box2[3])

        intersection = max(0, x2 - x1) * max(0, y2 - y1)

        area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
        area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])

        union = area1 + area2 - intersection
        return intersection / union if union > 0 else 0

# 示例评测
predictions = [
    {"status": "success", "steps": 5, "bbox": [100, 100, 200, 200]},
    {"status": "success", "steps": 8, "bbox": [150, 150, 250, 250]},
    {"status": "failed", "steps": 20, "bbox": [300, 300, 400, 400]},
]
ground_truths = [
    {"optimal_steps": 4, "bbox": [100, 100, 200, 200]},
    {"optimal_steps": 3, "bbox": [145, 148, 255, 252]},
    {"optimal_steps": 5, "bbox": [310, 305, 395, 395]},
]

metrics = GUIAgentEvaluator.compute_metrics(predictions, ground_truths)
print("GUI Agent评测结果:")
for k, v in metrics.items():
    print(f"  {k}: {v:.4f}")

8.3 当前挑战与未来方向

Text Only
GUI Agent面临的核心挑战:
━━━━━━━━━━━━━━━━━━━━━━━━━━

1. 视觉理解限制
   ├── 小图标/密集UI难以识别
   ├── 高分辨率输入需求 vs 模型限制
   └── 动态内容(动画/视频/弹窗)处理

2. 长序列推理
   ├── 复杂任务需要10-50步操作
   ├── 中间状态的记忆和回溯
   └── 错误恢复(死循环/误操作)

3. 安全性
   ├── 防止恶意操作(删除文件/发送消息)
   ├── 敏感信息处理(密码/个人数据)
   └── 操作可逆性和确认机制

4. 泛化与效率
   ├── 新应用/新界面的零样本泛化
   ├── 推理延迟(每步需要截图→LLM推理)
   └── API调用成本

未来方向:
━━━━━━━━━━
1. 端到端视觉-动作模型(不需要DOM/坐标标注)
2. World Model for GUI(预测操作结果,减少试错)
3. 多Agent协作(不同Agent负责不同应用)
4. 强化学习优化(在真实环境中在线学习)
5. 安全框架(沙箱执行 + 操作审核)

练习题

练习1:实现简单的SoM标注器

  1. 使用Playwright获取网页元素
  2. 在截图上标注可交互元素编号
  3. 生成元素描述文本

练习2:构建Web搜索Agent

  1. 使用Playwright + GPT-4o实现一个Web搜索Agent
  2. 任务:自动在Google搜索指定内容并返回前3条结果
  3. 实现错误恢复机制

练习3:评测框架

  1. 在MiniWoB++上评测你的Agent
  2. 实现任务成功率和步骤效率指标
  3. 对比纯视觉方案和DOM+视觉方案

📝 本章小结

本章系统学习了GUI Agent(图形界面智能代理)的核心知识:

  1. ✅ 理解了GUI Agent与传统API Agent的核心区别(视觉感知 vs 结构化接口)
  2. ✅ 掌握了GUI Agent的核心循环:Observe→Think→Act
  3. ✅ 学会了视觉感知技术:SoM标注、UI Grounding、无障碍树
  4. ✅ 掌握了动作空间设计(ActionType枚举、GUIAction数据结构、ActionParser解析器)
  5. ✅ 理解了四种主流架构方案(纯视觉、DOM+LLM、混合、Computer Use)
  6. ✅ 完成了Web Agent实战(Playwright自动化 + DOM简化)
  7. ✅ 了解了桌面/移动端GUI Agent的实现思路
  8. ✅ 掌握了GUI Agent评测指标(成功率、步骤效率、定位精度IoU)

✅ 学习检查清单

  • 能解释GUI Agent与API Agent的核心区别
  • 能描述Observe→Think→Act核心循环
  • 能解释Set-of-Mark(SoM)标注的作用和实现
  • 能列举标准GUI操作类型(点击、输入、滚动等)
  • 能对比四种主流GUI Agent架构方案的优缺点
  • 能基于Playwright实现基础Web Agent
  • 能解释为什么需要简化DOM(减少token、聚焦交互元素)
  • 能解释GUI Agent的评测指标(成功率、步骤效率、IoU定位精度)
  • 了解主要GUI Agent评测环境(WebArena、OSWorld、AndroidWorld)

🔗 相关章节

🔗 下一步

下一章我们将从零构建一个完整的Agent框架,深入理解Agent的运行机制。

继续学习: 11-从零构建Agent框架

📚 参考资料

  1. Hong et al. "CogAgent: A Visual Language Model for GUI Agents" (2023)
  2. Zheng et al. "SeeClick: Harnessing GUI Grounding for Advanced Visual GUI Agents" (2024)
  3. Zhang et al. "UFO: A UI-Focused Agent for Windows OS Interaction" (2024)
  4. Anthropic "Introducing computer use" (2024)
  5. Yang et al. "Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding" (2023)
  6. Zhou et al. "WebArena: A Realistic Web Environment for Building Autonomous Agents" (2023)
  7. Xie et al. "OSWorld: Benchmarking Multimodal Agents for Open-Ended Tasks" (2024)
  8. Bai et al. "DigiRL: Training In-The-Wild Device-Control Agents with Autonomous RL" (2024)
  9. Qin et al. "UI-TARS: Pioneering Automated GUI Interaction with Native Agents" (2025)
  10. Gur et al. "A Real-World WebAgent with Planning, Long Context Understanding, and Program Synthesis" (2023)

祝你学习愉快! 🎉


最后更新日期:2026-02-12 适用版本:AI Agent开发实战教程 v2026