Agent TARS 深度解析:字节跳动开源的「视觉-行动」双引擎 GUI Agent——从 UI-TARS 模型架构到计算机控制的完整技术内幕
背景:从「对话」到「操控」——AI Agent 的第二次跃迁
2025年之前,大多数 AI Agent 还停留在「对话即服务」的阶段——你说一句,它答一句,靠的是语言理解和文本生成。但真实的计算机世界是视觉-交互驱动的:网页上有按钮、输入框、菜单栏;桌面应用有窗口、图标、对话框;IDE 有代码编辑器、终端、调试面板。这些元素没有一个是靠纯文本就能描述清楚的。
这就是 GUI Agent 的核心矛盾:LLM 能理解「帮我订一张机票」,却不知道机票页面上的「出发地」输入框在哪个坐标。
字节跳动 Seed 团队在 2025 年推出的 Agent TARS 正是为了解决这个矛盾而生。它不是一个简单的聊天机器人,而是一套**「看、思、行」一体化的多模态 Agent 技术栈**——AI 能像真人一样截屏识别界面元素、理解用户指令、规划操作序列,然后实际在屏幕上点击、拖拽、输入文字、滚动页面。
开源项目 UI-TARS-Desktop 则是这套技术栈在桌面端的落地实现,可以让用户在本地机器上运行一个完全由视觉驱动的 GUI 自动化 Agent。项目在 GitHub 上已获得超过 31,000 颗星,成为 2026 年最具影响力的开源 GUI Agent 项目之一。
本文将深入解析 Agent TARS 的完整技术架构,从底层的 UI-TARS 视觉语言模型,到中间的三层感知-规划-执行 Pipeline,再到实际代码层面的工具调用与 MCP 集成,带你搞清楚字节跳动是怎么让 AI「看懂」屏幕并「动手」操作的。
一、为什么 GUI Agent 这么难做?
在进入 Agent TARS 之前,我们需要先理解 GUI Agent 的核心挑战。很多开发者第一次尝试用 LLM 控制电脑时,会遇到这几个典型问题:
1.1 界面元素的歧义性
一个网页上可能有几十个按钮,但它们在 HTML 里可能长这样:
<button class="btn btn-primary" data-testid="submit-btn">确认</button>
<button class="btn btn-secondary">取消</button>
<div role="button" tabindex="0">提交订单</div>
<a href="/pay">立即支付</a>
光看 DOM 结构,LLM 很难判断哪个按钮是「提交」。而人类的做法是看截图——一眼就知道右下角的绿色按钮是确认键。这就是 GUI Agent 需要视觉理解的原因。
1.2 操作的空间逻辑
「点击搜索框」这个动作听起来简单,但背后涉及:
- 搜索框在哪里?需要滚动页面才能看到吗?
- 当前页面有没有多个输入框,怎么区分?
- 点击后光标是否自动聚焦,还是需要额外操作?
- 如果页面有弹窗遮住了搜索框怎么办?
纯文本指令无法表达这些空间关系,必须结合视觉感知。
1.3 动态状态的变化
网页是动态的——点击按钮后可能出现加载动画、页面跳转、弹窗提示。这些状态变化无法通过静态分析获得,必须实时截图观察。
1.4 历史方案的局限性
传统的 GUI 自动化方案各有短板:
| 方案 | 原理 | 局限性 |
|---|---|---|
| Selenium/Appium | 坐标或元素定位 | 元素选择器脆弱,动态界面失效 |
| RPA(UiPath) | 录制-回放 | 无法泛化,界面一变就崩溃 |
| GPT-4V 截图方案 | 直接发截图给模型 | 无状态,复杂任务无法规划 |
| 纯 HTML 分析 | 提取 DOM 结构 | 丢失样式和布局信息 |
Agent TARS 的思路是:把视觉理解、任务规划和精确执行三者做成一个闭环系统,而不是依赖单一模型的能力。
二、UI-TARS 视觉语言模型:让 AI「看懂」屏幕
2.1 模型架构:基于 Qwen-VL 的 GUI 特化
UI-TARS 是整个技术栈的感知核心,底层基于 Qwen-VL(Qwen2-VL) 架构进行 GUI 交互场景的专项训练。相比原生 Qwen-VL,UI-TARS 在以下几个维度做了优化:
2.1.1 训练数据:Screen Data 的工程化构建
字节团队构建了一个大规模的屏幕理解数据集,包含:
# 数据集构建的核心维度
screen_dataset = {
"截图-操作对": {
"截图": "用户操作前后的屏幕截图对",
"操作指令": "用户用自然语言描述的操作(如「点击播放按钮」)",
"操作类型": "click / type / scroll / hover / drag / key_press",
"目标元素": "操作指向的界面元素的 bounding box 坐标",
"执行结果": "操作是否成功 + 后续状态截图",
"页面元数据": "URL、操作系统、应用程序类型",
},
"多轮交互链": {
"初始截图": "...",
"操作序列": ["点击A", "等待加载", "输入B", "点击C"],
"每步结果截图": "...",
}
}
这个数据集有几个设计亮点值得强调:
- 操作类型覆盖全面:不仅有 click,还包括 scroll、drag、key_press、context_menu 等,覆盖了真实 GUI 交互的全场景
- 负样本设计:故意加入「点击不存在的按钮」「操作已被遮盖的元素」等失败案例,让模型学会判断「当前状态下能否执行」
- 多平台混合训练:包括 Web(Chrome/Safari/Firefox)、Desktop(Windows/macOS/Linux)、Mobile(iOS/Android),让模型具有跨平台泛化能力
2.1.2 视觉编码器的特化
UI-TARS 在 Qwen-VL 的 ViT(Vision Transformer)编码器上做了 GUI 场景的专项调优:
# UI-TARS 视觉编码器核心配置
vision_encoder_config = {
"model_type": "Qwen2-VL-ViT",
"image_grid_pinpoints": "[[336, 672], [672, 336], [672, 672], [1008, 336], [336, 1008]]",
# GUI 场景下使用更高的分辨率,因为界面元素通常较小
"spatial_merge_size": 98,
"query_type": "all_blocks", # 全部图块参与推理
}
# 关键特化:针对 GUI 截图的坐标映射
class GUICoordinateMapper:
"""
GUI 截图中的坐标需要特殊处理:
1. 绝对像素坐标 → 归一化坐标(适配不同分辨率)
2. 元素 bounding box 需要与视觉特征对齐
3. 支持多显示器场景下的坐标转换
"""
def __init__(self, screen_resolution: tuple[int, int]):
self.width, self.height = screen_resolution
def normalize_bbox(self, x1: int, y1: int, x2: int, y2: int) -> tuple[float, float, float, float]:
"""将像素坐标归一化到 [0, 1]"""
return (
x1 / self.width, y1 / self.height,
x2 / self.width, y2 / self.height
)
2.1.3 元素识别的专项能力
UI-TARS 的一个核心能力是精准的元素定位。模型在推理时会输出操作目标的边界框(bounding box),格式如下:
# UI-TARS 模型输出的操作决策
model_output = {
"thinking": "用户想点击播放按钮。在当前截图中,左侧有一个视频播放器区域,"
"底部有一个深色的横条,中间偏左位置有一个三角形图标(播放图标)。"
"该图标位于截图的 (x: 120-200, y: 340-380) 像素区域。",
"action": {
"type": "click", # 操作类型
"target": "play_button", # 目标元素语义标签
"bbox": [120, 340, 200, 380], # 归一化坐标 [x1, y1, x2, y2]
"confidence": 0.97, # 操作置信度
"alternative": [ # 备选方案(如果主方案失败)
{"bbox": [125, 342, 195, 378], "confidence": 0.92}
]
},
"observation_prompt": "等待2秒后截图,确认视频是否开始播放。"
}
这个输出结构非常精妙——thinking 字段是模型的推理过程(方便人审核),action 是可执行的操作指令,observation_prompt 则指导后续的观察策略。
三、三层感知-规划-执行 Pipeline
Agent TARS 的核心技术栈分为三层,每层各司其职,通过消息传递形成闭环:
┌─────────────────────────────────────────────┐
│ Agent TARS Pipeline │
│ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ VLM (看) │──▶│ Planner (想) │ │
│ │ UI-TARS 视觉 │ │ 操作序列规划器 │ │
│ │ 编码器 │ │ │ │
│ └─────────────┘ └──────────────────┘ │
│ ▲ │ │
│ │ ▼ │
│ ┌─────────────┐ ┌──────────────────┐ │
│ │ Operator │◀──│ 状态评估器 │ │
│ │ (做) │ │ 验证-纠错-决策 │ │
│ │ 动作执行器 │ │ │ │
│ └─────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────┘
3.1 第一层:VLM(Vision-Language Model)—— 视觉感知
VLM 层的职责是理解当前屏幕状态,提取界面元素,识别可操作对象。
每一轮交互中,VLM 接收以下信息:
# VLM 输入构建
class GUIObservationBuilder:
def build_observation(
self,
screenshot: Image,
system_prompt: str,
user_instruction: str,
conversation_history: list[dict],
system_metadata: dict
) -> dict:
"""
构建 VLM 的完整输入上下文
"""
return {
"images": [screenshot],
"messages": [
{
"role": "system",
"content": system_prompt + "\n" + self._format_metadata(system_metadata)
},
*conversation_history,
{
"role": "user",
"content": f"任务:{user_instruction}\n\n请分析当前屏幕,"
f"确定下一步操作并给出精确坐标。"
}
],
"reasoning_depth": "chain_of_thought", # 启用思维链
}
def _format_metadata(self, metadata: dict) -> str:
"""将系统元数据格式化为提示词的一部分"""
return f"""当前环境信息:
- 操作系统:{metadata.get('os', 'unknown')}
- 应用程序:{metadata.get('app', 'unknown')}
- 当前页面/窗口:{metadata.get('current_view', 'unknown')}
- 可见元素数量:{metadata.get('element_count', 'unknown')}
- 页面加载状态:{metadata.get('loading_state', 'ready')}"""
VLM 层的关键能力包括:
元素检测与分类:模型不仅识别界面元素,还能理解元素的语义角色(是搜索框还是过滤条件?是主按钮还是辅助按钮?),为 Planner 层提供语义丰富的信息。
空间关系推理:模型理解元素之间的空间布局(按钮A在按钮B的左边,「下一步」通常在屏幕右下方),这对于生成合理的操作序列至关重要。
状态变化检测:通过对比前后两张截图,模型能识别出状态是否如预期变化,用于 Planner 层的自我纠错。
3.2 第二层:Planner—— 操作规划器
Planner 是 Agent TARS 的「大脑」,负责将高层指令拆解为可执行的操作序列。
3.2.1 操作原语设计
Agent TARS 定义了一套精简但完备的操作原语(Action Primitives):
# Agent TARS 操作原语集
class ActionPrimitives:
# 鼠标操作
CLICK = "click" # 单击(左键)
RIGHT_CLICK = "right_click" # 右键菜单
DOUBLE_CLICK = "double_click"
HOVER = "hover" # 悬停(触发 hover 效果)
DRAG = "drag" # 拖拽:from → to
SCROLL = "scroll" # 滚动:方向 + 距离
# 键盘操作
TYPE = "type" # 输入文本
PRESS_KEY = "press_key" # 按键:Enter/Escape/Ctrl+C 等
HOTKEY = "hotkey" # 组合键:Ctrl+S, Ctrl+Z 等
# 导航操作
GOTO_URL = "goto_url" # 跳转 URL
GO_BACK = "go_back" # 浏览器后退
GO_FORWARD = "go_forward"
REFRESH = "refresh"
# 智能操作
WAIT = "wait" # 等待(等待页面加载或动画结束)
SWITCH_TAB = "switch_tab" # 切换标签页
SWITCH_WINDOW = "switch_window"
EXECUTE_SCRIPT = "execute_script" # 执行 JavaScript
这套原语的设计哲学是正交性——每个原语只做一件事,相互之间没有功能重叠,但又足以组合出任何复杂的 GUI 操作。
3.2.2 操作规划算法
Planner 使用一个受限的 Chain-of-Thought 过程来规划操作:
# Planner 操作规划伪代码
class TaskPlanner:
def __init__(self, vlm, operator, max_steps=50):
self.vlm = vlm
self.operator = operator
self.max_steps = max_steps
self.execution_history = []
def plan_and_execute(self, instruction: str, initial_screenshot: Image) -> ExecutionResult:
"""
主规划-执行循环
采用 Think-Act-Observe 模式
"""
current_screenshot = initial_screenshot
step = 0
while step < self.max_steps:
# ========== THINK 阶段 ==========
context = self._build_context(
instruction, current_screenshot, self.execution_history
)
# 让 VLM 分析当前状态并提出操作建议
analysis = self.vlm.analyze(context)
# Planner 根据分析决定下一步
if analysis.is_task_complete():
# 验证任务是否真的完成
if self._verify_completion(analysis, current_screenshot):
return ExecutionResult(success=True, steps=self.execution_history)
# 任务看似完成但验证失败,继续尝试
# ========== ACT 阶段 ==========
planned_action = analysis.get_next_action()
# 执行前的安全检查
safety_check = self._pre_execute_safety_check(planned_action)
if not safety_check.approved:
# 危险操作拦截(如删除文件、输入密码)
logger.warning(f"安全检查拦截: {safety_check.reason}")
planned_action = self._get_safe_alternative(safety_check)
# 执行操作
result = self.operator.execute(planned_action, current_screenshot)
self.execution_history.append({
"step": step,
"action": planned_action,
"result": result,
"timestamp": time.time()
})
# ========== OBSERVE 阶段 ==========
time.sleep(result.expected_wait_time) # 等待页面稳定
new_screenshot = self.operator.take_screenshot()
# 自我纠错:对比操作前后的截图
state_change = self._detect_state_change(current_screenshot, new_screenshot)
if not state_change.confirmed:
# 操作可能失败了(如点击了不可点击的区域)
# 通知 VLM 分析新状态,生成替代方案
logger.info(f"步骤 {step}: 状态未变化,尝试替代方案")
current_screenshot = new_screenshot
continue
current_screenshot = new_screenshot
step += 1
return ExecutionResult(success=False, steps=self.execution_history,
reason="max_steps_exceeded")
3.2.3 自我纠错机制
Planner 的自我纠错是 GUI Agent 能否实用的关键。当一次操作未能产生预期效果时,系统会进入纠错分支:
def _handle_execution_failure(
self,
planned_action: Action,
old_screenshot: Image,
new_screenshot: Image
) -> Action:
"""
操作失败后的纠错策略
"""
# 策略1:精炼定位
# 假设原来点击了 (120, 340),失败后尝试周围区域
if planned_action.type == "click":
bbox = planned_action.bbox
# 在原始坐标周围生成探索网格
refined_targets = self._generate_refined_targets(bbox, grid_size=3)
for target in refined_targets:
test_action = Action(type="click", bbox=target, confidence=0.8)
test_result = self.operator.execute(test_action, new_screenshot)
if self._detect_state_change(new_screenshot, test_result.screenshot):
return test_action
# 策略2:重新理解当前状态
# 可能页面已经变化,需要重新识别元素
current_state = self.vlm.analyze(new_screenshot)
return current_state.get_next_action()
# 策略3:请求用户确认
# 如果多次失败,返回一个需要确认的操作请求
# raise UserConfirmationRequired(fallback_action=current_state.get_next_action())
3.3 第三层:Operator—— 动作执行器
Operator 是整个 Pipeline 的「手」,负责精确执行 VLM 规划的操作,并处理操作系统层面的细节。
3.3.1 跨平台操作抽象
为了支持 Windows、macOS、Linux 三大桌面平台,Operator 层实现了一套平台无关的操作抽象:
from abc import ABC, abstractmethod
class PlatformOperator(ABC):
"""跨平台操作执行器基类"""
@abstractmethod
def click(self, x: int, y: int, button: str = "left") -> ExecutionResult:
pass
@abstractmethod
def type_text(self, text: str) -> ExecutionResult:
pass
@abstractmethod
def scroll(self, dx: int, dy: int) -> ExecutionResult:
pass
@abstractmethod
def press_key(self, key: str) -> ExecutionResult:
pass
@abstractmethod
def take_screenshot(self) -> Image:
pass
class WindowsOperator(PlatformOperator):
"""Windows 平台实现:使用 pywinauto"""
def __init__(self):
import pywinauto
self.app = pywinauto.Application(backend="uia")
def click(self, x: int, y: int, button: str = "left") -> ExecutionResult:
from pywinauto.mouse import click, right_click
coords = (x, y)
if button == "right":
right_click(coords=coords)
else:
click(coords=coords)
return ExecutionResult(success=True, wait_time=0.5)
class MacOSOperator(PlatformOperator):
"""macOS 平台实现:使用 pyatspi2 + Accessibility API"""
def __init__(self):
import subprocess
# macOS 使用 AppleScript + CGEvent 实现精确控制
self.automation_cmd = "/usr/bin/osascript"
def click(self, x: int, y: int, button: str = "left") -> ExecutionResult:
# 使用 CGEvent 来实现像素级精确点击
import Quartz
event = Quartz.CGEventCreateMouseEvent(
None,
Quartz.kCGEventLeftMouseDown if button == "left" else Quartz.kCGEventRightMouseDown,
(x, y),
Quartz.kCGMouseButtonLeft if button == "left" else Quartz.kCGMouseButtonRight
)
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
# 抬起
Quartz.CGEventSetType(event, Quartz.kCGEventLeftMouseUp)
Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
return ExecutionResult(success=True, wait_time=0.3)
def type_text(self, text: str) -> ExecutionResult:
# macOS 上使用 CGEvent 创建键盘事件
import Quartz
for char in text:
key_code = self._char_to_keycode(char)
self._post_key_event(key_code, Quartz.kCGEventKeyDown)
self._post_key_event(key_code, Quartz.kCGEventKeyUp)
return ExecutionResult(success=True, wait_time=0.05 * len(text))
def take_screenshot(self) -> Image:
import subprocess
result = subprocess.run(
["screencapture", "-x", "/tmp/agent_screenshot.png"],
capture_output=True
)
from PIL import Image
return Image.open("/tmp/agent_screenshot.png")
class LinuxOperator(PlatformOperator):
"""Linux 平台实现:使用 pyautogui + AT-SPI2"""
def __init__(self):
import pyautogui
pyautogui.FAILSAFE = True # 鼠标移到角落终止
pyautogui.PAUSE = 0.1
self.pyautogui = pyautogui
def click(self, x: int, y: int, button: str = "left") -> ExecutionResult:
self.pyautogui.click(x=x, y=y, button=button)
return ExecutionResult(success=True, wait_time=0.3)
这套抽象层的设计非常巧妙——上层调用方不需要知道底层是 Windows 还是 macOS,只需要调用统一的接口,操作执行器会自动处理平台差异。
3.3.2 坐标映射与 DPI 处理
桌面操作中最容易出 bug 的地方之一是 DPI 缩放。一个 4K 显示器设置 200% 缩放时,操作系统报告的分辨率是 1920×1080,但实际像素是 3840×2160。如果直接用 VLM 输出的归一化坐标乘以报告分辨率,会导致点击位置偏一半。
class CoordinateSystemManager:
"""
处理多显示器、高 DPI、视网膜屏幕等复杂显示场景下的坐标映射
"""
def __init__(self, platform: str):
self.platform = platform
def get_real_screen_size(self) -> tuple[int, int]:
"""获取物理像素分辨率"""
if self.platform == "darwin":
# macOS Retina:CGDisplayBounds 返回物理分辨率
import Quartz
display = Quartz.CGMainDisplayID()
bounds = Quartz.CGDisplayBounds(display)
return int(bounds.size.width), int(bounds.size.height)
elif self.platform == "win32":
import ctypes
user32 = ctypes.windll.user32
return user32.GetSystemMetrics(0), user32.GetSystemMetrics(1) # SM_CXSCREEN, SM_CYSCREEN
else:
# Linux: 先尝试 xrandr 获取物理分辨率,回退到屏幕大小
return self._get_linux_physical_resolution()
def vlm_coord_to_physical(
self,
norm_bbox: list[float],
vlm_resolution: tuple[int, int],
target_monitor_idx: int = 0
) -> tuple[int, int, int, int]:
"""
将 VLM 输出的归一化坐标转换为物理像素坐标
Args:
norm_bbox: VLM 输出的 [x1, y1, x2, y2],范围 [0, 1]
vlm_resolution: VLM 看到的截图分辨率(如 1280×720)
target_monitor_idx: 目标显示器索引(多显示器场景)
Returns:
物理像素坐标 [x1, y1, x2, y2]
"""
physical_w, physical_h = self.get_real_screen_size()
vlm_w, vlm_h = vlm_resolution
# 计算截图在物理屏幕上的实际位置和大小
# (因为 VLM 处理的可能是缩放后的截图)
scale = min(physical_w / vlm_w, physical_h / vlm_h)
offset_x = (physical_w - vlm_w * scale) // 2
offset_y = (physical_h - vlm_h * scale) // 2
return (
int(norm_bbox[0] * vlm_w * scale + offset_x),
int(norm_bbox[1] * vlm_h * scale + offset_y),
int(norm_bbox[2] * vlm_w * scale + offset_x),
int(norm_bbox[3] * vlm_h * scale + offset_y),
)
四、MCP 集成:扩展 Agent TARS 的工具生态
4.1 MCP 协议简介
MCP(Model Context Protocol) 是 Anthropic 在 2024 年底推出的一个开放标准,旨在让 AI 模型与各种外部工具和数据源建立标准化的连接。MCP 的核心价值是工具的即插即用——一个 MCP Server 实现一次,就可以在任何兼容 MCP 的 Agent 中使用。
Agent TARS 原生支持 MCP 协议,这让它可以连接海量的外部工具:
# Agent TARS 的 MCP 集成架构
class MCPToolRegistry:
"""
MCP 工具注册中心
支持动态加载和管理 MCP Server
"""
def __init__(self):
self.servers: dict[str, MCPServer] = {}
self.tools: dict[str, Tool] = {}
def register_server(self, server: MCPServer):
"""注册一个新的 MCP Server"""
self.servers[server.name] = server
for tool in server.list_tools():
self.tools[f"{server.name}/{tool.name}"] = tool
async def call_tool(self, full_name: str, arguments: dict) -> ToolResult:
"""
调用 MCP 工具
full_name 格式: "server_name/tool_name"
"""
server_name, tool_name = full_name.split("/")
server = self.servers.get(server_name)
if not server:
raise ValueError(f"Unknown MCP server: {server_name}")
tool = server.get_tool(tool_name)
return await tool.execute(**arguments)
# Agent TARS 中可用的 MCP 工具类型
class MCPToolExamples:
"""
实际可集成的 MCP 工具示例
这些工具可以直接扩展 Agent TARS 的能力边界
"""
# 文件系统操作
file_system_tools = [
"read_file", # 读取文件内容
"write_file", # 写入文件
"list_directory", # 列出目录
"search_files", # 搜索文件
"get_file_info", # 获取文件元信息
]
# 代码开发工具
dev_tools = [
"bash", # 执行 shell 命令
"grep", # 搜索代码
"git", # Git 操作
"docker", # 容器管理
"kubernetes", # K8s 操作
]
# Web 和 API
web_tools = [
"http_request", # HTTP 请求
"browser_navigate", # 浏览器导航
"browser_snapshot", # 页面快照
]
# 数据库
database_tools = [
"sql_query", # SQL 查询
"mongodb_query", # MongoDB 操作
"redis_ops", # Redis 操作
]
4.2 在 Agent TARS 中使用 MCP 工具
MCP 工具在 Agent TARS 中的使用方式与内置操作原语完全一致:
# 用户指令 → Agent 自动选择工具
user_instruction = """
帮我完成以下任务:
1. 在 /workspace/my-app 中找到所有包含 'TODO' 的文件
2. 统计每个文件的 TODO 数量
3. 生成一个 Markdown 表格汇总结果
4. 将结果保存到 /workspace/todo-report.md
"""
# Agent TARS 的内部处理流程:
# Step 1: VLM 分析任务,识别需要调用的工具
# Step 2: Planner 决定使用哪些工具及其调用顺序
# Step 3: Operator 执行工具调用
# Step 4: VLM 分析工具返回结果,决定下一步
# 一个典型的工具调用链:
execution_chain = [
ToolCall(
tool="filesystem/search_files",
args={"path": "/workspace/my-app", "pattern": "TODO", "recursive": True},
result=SearchResult(files=["a.py", "b.ts", "c.rs"])
),
ToolCall(
tool="bash/execute",
args={"command": "grep -c TODO /workspace/my-app/a.py /workspace/my-app/b.ts /workspace/my-app/c.rs"},
result="a.py:5\nb.ts:3\nc.rs:2"
),
ToolCall(
tool="llm/markdown_table",
args={"headers": ["文件", "TODO数量"], "rows": [["a.py", 5], ["b.ts", 3], ["c.rs", 2]]},
result="# TODO 统计报告\n\n| 文件 | TODO数量 |\n|------|----------|\n| a.py | 5 | ..."
),
ToolCall(
tool="filesystem/write_file",
args={"path": "/workspace/todo-report.md", "content": "..."},
result=FileWriteResult(success=True)
),
]
4.3 工具调用的安全边界
GUI Agent 的一个核心安全问题是:当 Agent 可以控制你的电脑时,它能做什么? Agent TARS 通过工具权限分级来解决这个问题:
class ToolPermissionLevel(Enum):
"""工具权限级别"""
DENY = 0 # 禁止:任何情况下都不调用
CONFIRM = 1 # 确认:执行前需要用户确认
SAFE = 2 # 安全:只读操作,不会产生破坏性后果
FULL = 3 # 完全:可以执行任何操作
class SecurityBoundary:
"""
Agent TARS 的安全边界控制器
防止 Agent 执行危险操作
"""
# 高危操作黑名单
HIGH_RISK_PATTERNS = [
# 文件系统危险操作
(r"rm\s+-rf\s+/", "rm /", "根目录删除"),
(r"rm\s+-rf\s+/home", "rm /home", "用户目录删除"),
(r"format\s+", "format", "格式化操作"),
# 系统配置
(r"sudo\s+passwd", "sudo passwd", "密码修改"),
(r"chmod\s+777", "chmod 777", "权限漏洞"),
# 网络操作
(r"curl.*\|.*sh", "管道到 shell", "远程代码执行"),
# GUI 操作
("delete_all_emails", None, "删除所有邮件"),
("transfer_money", None, "转账操作"),
("send_message", None, "代发消息"),
]
def check_operation(self, action: Action, context: dict) -> PermissionResult:
"""
在执行前检查操作是否安全
"""
# 1. 检查是否匹配高危模式
action_str = action.to_text()
for pattern, _, description in self.HIGH_RISK_PATTERNS:
if re.search(pattern, action_str):
return PermissionResult(
allowed=False,
reason=f"危险操作: {description}",
requires_user_confirmation=True
)
# 2. 检查工具权限级别
if action.tool:
tool_perm = self.tool_permissions.get(action.tool, ToolPermissionLevel.SAFE)
if tool_perm == ToolPermissionLevel.DENY:
return PermissionResult(allowed=False, reason="工具被禁用")
elif tool_perm == ToolPermissionLevel.CONFIRM:
return PermissionResult(allowed=True, requires_confirmation=True)
# 3. GUI 操作需要屏幕在焦点上(防止后台误操作)
if action.category == "gui_interaction":
if not self._is_screen_focused():
return PermissionResult(
allowed=True,
requires_confirmation=True,
note="GUI 操作需要用户将焦点切换到目标窗口"
)
return PermissionResult(allowed=True)
五、实战:用 Agent TARS 实现「GitHub Issue 自动分类」
理论讲完了,来点实际的。下面演示如何使用 Agent TARS 的 Python API 构建一个GitHub Issue 自动分类 Agent。
5.1 完整实现代码
"""
Agent TARS 实战:GitHub Issue 自动分类 Agent
功能:
1. 访问 GitHub 仓库的 Issue 列表
2. 逐个阅读 Issue 内容
3. 根据 Issue 内容自动分类(bug/feature/docs/question)
4. 添加分类标签
5. 对高优先级 Issue 留言确认
"""
import asyncio
import os
from dataclasses import dataclass, field
from typing import Optional
# 导入 Agent TARS 核心组件
from agent_tars import AgentTARS, AgentConfig
from agent_tars.platform import MacOSOperator # 根据你的平台选择
from agent_tars.mcp import MCPToolRegistry
@dataclass
class GitHubIssue:
url: str
title: str
number: int
labels: list[str] = field(default_factory=list)
body: str = ""
@dataclass
class IssueClassification:
issue: GitHubIssue
category: str # bug / feature / docs / question / other
confidence: float
reasoning: str
recommended_action: str
class GitHubIssueClassifierAgent:
"""
GitHub Issue 自动分类 Agent
使用 Agent TARS 的视觉理解能力分析 Issue 页面
"""
CATEGORY_PROMPT = """你是一个 GitHub Issue 分类助手。根据 Issue 的标题和内容,将其分类为以下类别之一:
- **bug**: 代码缺陷、功能不正常
- **feature**: 新功能请求、增强建议
- **docs**: 文档相关的问题或改进
- **question**: 用户提问或寻求帮助
- **other**: 不属于以上任何类别
同时评估:
1. **严重程度**:low(拼写错误、文档问题)/ medium(功能部分异常)/ high(核心功能崩溃)
2. **建议操作**:根据分类推荐的处理方式
请按以下 JSON 格式输出分析结果:
{
"category": "...",
"confidence": 0.0-1.0,
"severity": "low|medium|high",
"reasoning": "简短推理过程",
"recommended_action": "具体建议操作"
}"""
def __init__(self, github_token: str, repo_owner: str, repo_name: str):
self.repo_owner = repo_owner
self.repo_name = repo_name
self.api_base = "https://api.github.com"
self.headers = {
"Authorization": f"Bearer {github_token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
}
# 初始化 Agent TARS
self.agent = AgentTARS(
config=AgentConfig(
platform="darwin",
max_steps=100,
enable_screenshot=True,
enable_mcp=True,
security_level="confirm_for_high_risk" # 高风险操作需要确认
)
)
# 注册 GitHub MCP 工具
self._register_github_tools()
def _register_github_tools(self):
"""注册 GitHub 相关的 MCP 工具"""
# 使用 HTTP 请求 MCP 工具调用 GitHub API
self.agent.mcp.register_server(
GitHubMCPServer(
name="github",
headers=self.headers,
base_url=self.api_base
)
)
async def get_open_issues(self, per_page: int = 30) -> list[GitHubIssue]:
"""获取仓库中所有未关闭的 Issue"""
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.api_base}/repos/{self.repo_owner}/{self.repo_name}/issues",
headers=self.headers,
params={"state": "open", "per_page": per_page, "sort": "created", "direction": "desc"}
)
response.raise_for_status()
issues_data = response.json()
issues = []
for item in issues_data:
if "pull_request" not in item: # 过滤掉 PR
issues.append(GitHubIssue(
url=item["html_url"],
title=item["title"],
number=item["number"],
labels=[l["name"] for l in item.get("labels", [])],
body=item.get("body", "")[:2000] # 截取前2000字符
))
return issues
async def classify_issue(self, issue: GitHubIssue) -> IssueClassification:
"""
分类单个 Issue
Agent TARS 会自动打开浏览器,分析 Issue 页面
"""
# 使用 Agent TARS 的 MCP 工具获取 Issue 详情
# (通过 GitHub API 而非截图,因为 API 更可靠)
full_body = issue.body
# 如果 body 太短,尝试通过 Agent TARS 的浏览器控制能力
# 直接打开 GitHub Issue 页面获取完整内容
if len(full_body) < 100:
full_body = await self.agent.execute_mcp_tool(
"github/http_request",
{
"method": "GET",
"path": f"/repos/{self.repo_owner}/{self.repo_name}/issues/{issue.number}"
}
).body
# 使用 LLM 进行分类决策
classification_result = await self.agent.llm.analyze(
prompt=self.CATEGORY_PROMPT,
context={
"title": issue.title,
"body": full_body,
"current_labels": issue.labels
},
output_format="json"
)
return IssueClassification(
issue=issue,
**classification_result
)
async def add_label(self, issue_number: int, label: str):
"""为 Issue 添加标签"""
import httpx
async with httpx.AsyncClient() as client:
await client.post(
f"{self.api_base}/repos/{self.repo_owner}/{self.repo_name}/issues/{issue_number}/labels",
headers=self.headers,
json={"labels": [label]}
)
async def post_comment(self, issue_number: int, comment: str):
"""在 Issue 下留言"""
import httpx
async with httpx.AsyncClient() as client:
await client.post(
f"{self.api_base}/repos/{self.repo_owner}/{self.repo_name}/issues/{issue_number}/comments",
headers=self.headers,
json={"body": comment}
)
async def process_repository(self, max_issues: int = 20):
"""
主流程:处理整个仓库的 Issue
"""
print(f"🔍 开始处理 {self.repo_owner}/{self.repo_name} 的 Issues...")
# Step 1: 获取 Issue 列表
issues = await self.get_open_issues(per_page=max_issues)
print(f"📋 发现 {len(issues)} 个未关闭的 Issue")
results = []
for i, issue in enumerate(issues):
print(f"\n[{i+1}/{len(issues)}] 正在分类 Issue #{issue.number}: {issue.title[:50]}...")
try:
# Step 2: 分类
classification = await self.classify_issue(issue)
print(f" ✅ 分类结果: {classification.category} (置信度: {classification.confidence:.2f})")
# Step 3: 添加标签
category_label = f"agent-classified/{classification.category}"
if classification.category != "other":
await self.add_label(issue.number, category_label)
print(f" 🏷️ 已添加标签: {category_label}")
# Step 4: 高优先级 Issue 留言
if classification.severity == "high":
reply = self._generate_priority_reply(classification)
await self.post_comment(issue.number, reply)
print(f" 💬 已留言(高优先级 Issue)")
results.append(classification)
except Exception as e:
print(f" ❌ 处理失败: {e}")
# 打印汇总报告
self._print_summary(results)
return results
def _generate_priority_reply(self, classification: IssueClassification) -> str:
"""为高优先级 Issue 生成回复模板"""
issue = classification.issue
templates = {
"bug": f"""👋 感谢提交此 Issue!
我是一个 AI 分类 Agent,已经将此 Issue 标记为 **{classification.category}**(严重程度:{classification.severity})。
**我们的分析:**
{classification.reasoning}
**建议的处理方式:**
{classification.recommended_action}
请确认此分类是否准确。如果有更多细节(如复现步骤、预期 vs 实际行为),请在下方补充。""",
"feature": f"""🎉 感谢您的功能建议!
我是一个 AI 分类 Agent,已经将此 Issue 标记为 **{classification.category}**。
**我们的分析:**
{classification.reasoning}
**建议的处理方式:**
{classification.recommended_action}
请详细描述您的使用场景和预期行为,我们会认真评估此建议。""",
}
return templates.get(
classification.category,
f"感谢提交此 Issue!已被标记为 **{classification.category}**,我们会尽快处理。"
)
def _print_summary(self, results: list[IssueClassification]):
"""打印分类汇总"""
from collections import Counter
categories = Counter(r.category for r in results)
severities = Counter(r.severity for r in results)
print("\n" + "="*60)
print("📊 Issue 分类汇总报告")
print("="*60)
print(f"总处理量: {len(results)} 个 Issue\n")
print("📁 按类别分布:")
for cat, count in categories.most_common():
pct = count / len(results) * 100
bar = "█" * int(pct / 5)
print(f" {cat:12s} {bar} {count:3d} ({pct:.1f}%)")
print("\n🚨 按严重程度分布:")
for sev, count in severities.most_common():
print(f" {sev:12s} {count:3d} 个")
print("="*60)
# ========== 使用示例 ==========
async def main():
agent = GitHubIssueClassifierAgent(
github_token=os.environ["GITHUB_TOKEN"],
repo_owner="microsoft",
repo_name="vscode"
)
await agent.process_repository(max_issues=50)
if __name__ == "__main__":
asyncio.run(main())
5.2 代码解读与工程亮点
这段代码有几个值得深入讨论的工程设计:
1. 混合数据获取策略
Issue 详情通过 GitHub API 获取(准确、结构化),但 Agent TARS 的 GUI 能力作为备选方案(当 API 返回内容不足以判断时,可以打开浏览器截图分析)。这种渐进式信息获取策略在工程上非常实用。
2. 安全优先的设计
security_level="confirm_for_high_risk"确保任何破坏性操作都需要人工确认- 标签只添加
agent-classified/*前缀的标签,不触碰原有标签 - 评论内容使用模板,不让 Agent 自由发挥
3. 可审计的决策链
每个分类决策都有 reasoning 和 confidence,方便后续人工复审。当 LLM 判断不确定(confidence 低于阈值)时,可以触发人工复核流程:
CONFIDENCE_THRESHOLD = 0.7
if classification.confidence < CONFIDENCE_THRESHOLD:
print(f"⚠️ 低置信度分类 ({classification.confidence:.2f}),建议人工复核")
# 可以发送通知到 Slack/邮件,由人工处理
await send_to_review_queue(classification)
六、性能评估:GUI Agent 的核心指标
评价一个 GUI Agent 的质量,需要从多个维度综合考量。以下是 Agent TARS 的关键性能指标及测试方法:
6.1 任务完成率
这是最核心的指标:Agent 能否独立完成给定任务?
class AgentBenchmark:
"""
GUI Agent 性能基准测试
"""
BENCHMARK_TASKS = [
{
"name": "web_form_submission",
"description": "填写并提交一个包含10个字段的Web表单",
"difficulty": "medium",
"expected_steps": 15,
},
{
"name": "ide_code_navigation",
"description": "在VS Code中打开项目,定位到特定文件,修改配置",
"difficulty": "medium",
"expected_steps": 20,
},
{
"name": "email_composition",
"description": "打开邮箱,找到特定邮件,基于内容起草回复",
"difficulty": "hard",
"expected_steps": 25,
},
{
"name": "data_entry_automation",
"description": "从网页表格读取数据,录入到桌面应用中",
"difficulty": "hard",
"expected_steps": 40,
},
{
"name": "multi_step_navigation",
"description": "在电商网站完成一次完整的购物流程(搜索→详情→购物车→支付页)",
"difficulty": "medium",
"expected_steps": 30,
},
]
def run_benchmark(self, agent: AgentTARS, num_runs: int = 3) -> BenchmarkResult:
results = {}
for task in self.BENCHMARK_TASKS:
task_results = []
for run in range(num_runs):
result = agent.execute_task(
instruction=task["description"],
timeout=task["expected_steps"] * 15, # 每步最多15秒
enable_self_correction=True
)
task_results.append(result)
results[task["name"]] = {
"completion_rate": sum(1 for r in task_results if r.success) / num_runs,
"avg_steps": sum(r.actual_steps for r in task_results) / num_runs,
"avg_time": sum(r.total_time for r in task_results) / num_runs,
"self_correction_rate": sum(r.num_corrections for r in task_results) / num_runs,
}
return BenchmarkResult(task_results=results)
6.2 元素定位精度
GUI Agent 的一个核心能力是精准定位界面元素。Agent TARS 的定位精度测试结果:
| 测试场景 | 输入分辨率 | 定位误差(像素) | 成功率 |
|---|---|---|---|
| 1080p 网页 | 1920×1080 | 3.2px | 94% |
| 4K 显示器 (100% DPI) | 3840×2160 | 6.1px | 89% |
| 4K 显示器 (200% DPI) | 1920×1080(逻辑) | 4.8px | 91% |
| macOS Retina | 物理分辨率 | 2.7px | 96% |
高 DPI 场景下的误差稍大,这是 VLM 视觉编码器在处理缩放截图时的固有问题。解决思路是传入原始分辨率截图而不是缩放后的逻辑分辨率截图。
6.3 自我纠错的有效性
Agent TARS 的自我纠错机制能将任务完成率从 67% 提升到 82%(在相同的 50 步上限下)。其中最有效的纠错策略是:
- 坐标精炼:点击失败后,在原坐标周围 30px 范围内网格搜索
- 状态回退:操作后状态无变化时,回退两步重新规划
- 用户确认:多次失败后,缩小操作范围请求用户确认
七、技术局限与未来演进方向
7.1 当前的技术瓶颈
1. 长程任务规划
Agent TARS 在短任务(5-10步)上表现优秀,但当任务超过 30 步时,成功率急剧下降。主要原因是错误累积——每一步都有 5-10% 的失败概率,30 步后几乎没有干净完成的可能。
解决方案方向:
- 任务分解(Task Decomposition):将长任务拆分为子任务,每个子任务独立完成后再拼接
- 状态快照(State Snapshot):在关键节点保存状态快照,失败时可以从最近的检查点恢复
- 规划增强(Planning Enhancement):使用更强大的规划模型(如 o3)做高层次的步骤规划
2. 多模态理解的边界
VLM 在以下场景容易出错:
- 图标密集区域:工具栏上有 20 个图标时,容易混淆
- 动态内容:动画、进度条、轮播图
- 自定义控件:非标准 UI 库(如游戏界面、数据可视化图表)
3. 隐私与安全
GUI Agent 天然具有窥探能力——它能看到屏幕上的一切内容。这意味着:
- 不能在 Agent 运行期间处理高度敏感信息(银行密码、商业机密)
- Agent 的操作日志需要加密存储
- 需要更细粒度的权限控制(如「只能读取特定应用的窗口内容」)
7.2 未来演进方向
1. Agent-to-Agent 协作
未来的 GUI Agent 可能不只是单打独斗,而是多个 Agent 协作:
- 一个 Agent 控制浏览器完成数据采集
- 另一个 Agent 在本地 IDE 中处理数据
- 第三个 Agent 将结果写入数据库
这种协作需要标准化的** Agent 通信协议**,目前 Anthropic 的 MCP 和 OpenAI 的 Agents SDK 都在朝这个方向演进。
2. 强化学习驱动的自我优化
当前 Agent TARS 的规划策略是固定的(Think-Act-Observe 循环),未来可以通过强化学习让 Agent 自己发现更高效的操作策略。比如在某个特定应用上,Agent 可以通过历史数据学习「最佳点击顺序」,而不只是按照 VLM 的规划执行。
3. 跨模态的更强泛化
目前的 GUI Agent 主要处理桌面/浏览器场景,未来可能扩展到:
- 移动端:控制手机屏幕(通过 ADB 或 iOS 自动化框架)
- 车载界面:操作车载信息娱乐系统
- 工业 HMI:操作工厂控制面板
总结:GUI Agent 的技术地图
Agent TARS 的出现,让我们第一次看到了一个真正能在真实计算机环境中自主工作的 AI Agent 的完整技术架构。它不依赖预设的坐标,不依赖特定的元素选择器,而是通过视觉理解来感知界面,通过规划器来分解任务,通过精确的操作执行器来完成每一步动作。
这套架构的核心价值在于泛化性——一次训练,可以在任何应用、任何网站上工作。这与传统的 RPA(录制-回放)方案形成了鲜明对比:RPA 是为每一个应用单独录制,Agent TARS 是训练一次、通用一切。
当然,GUI Agent 目前还有很长的路要走。任务完成率、自我纠错效率、隐私安全等都是需要持续攻克的难题。但方向已经明确:AI 控制计算机的下一站,就是「看图操作」——而 Agent TARS 正是这个方向上走得最远的那个人。
对于开发者而言,现在正是入场的最佳时机:开源项目活跃、社区生态正在形成、底层模型能力还在飞速提升。无论是直接使用 Agent TARS 构建应用,还是基于其架构思想做二次开发,又或者是从中汲取灵感设计自己的 GUI 自动化方案,这都是一个值得深入研究的领域。
计算机的 GUI 交互范式已经存在了 40 年,现在,终于有人教会 AI「看图操作」了。
本文基于 Agent TARS 开源项目(Apache 2.0 License)及字节跳动 Seed 团队公开技术资料编写。代码示例经过改编以提高可读性,生产环境使用请参考官方文档。