UI-TARS 源码解析 #6:动作空间设计:click、drag、hotkey、type、scroll 如何统一成模型输出?

在上一篇文章中,我们分析了 prompt.py 里的三种提示词模板:

复制代码
COMPUTER_USE
MOBILE_USE
GROUNDING

其中,COMPUTER_USE 面向桌面端 GUI 操作,MOBILE_USE 面向移动端 GUI 操作,GROUNDING 则更适合单步定位和评估。

这篇文章继续沿着 prompt.py 往下看,重点分析 UI-TARS 的 动作空间设计。

所谓动作空间,简单理解就是:

模型到底可以输出哪些动作?

对 GUI Agent 来说,这个问题非常关键。

因为模型不能随便说:

复制代码
我觉得你可以点一下右边那个按钮。

这句话人能理解,但程序不能执行。

程序真正需要的是:

复制代码
Action: click(point='<point>850 120</point>')

也就是说,UI-TARS 必须把自然语言任务转成一组标准动作:

复制代码
click
drag
hotkey
type
scroll
wait
finished

这篇文章就来分析:为什么 UI-TARS 要这样设计动作空间,以及这些动作如何统一成模型输出。


一、为什么 GUI Agent 必须定义动作空间?

如果没有动作空间,模型输出会非常自由。

例如,用户说:

复制代码
帮我在浏览器里搜索 UI-TARS。

模型可能输出:

复制代码
你需要先点击地址栏,然后输入 UI-TARS,再按回车。

也可能输出:

复制代码
我会打开搜索框,输入关键词,然后提交搜索。

还可能输出:

复制代码
Click the search bar and type UI-TARS.

这些表达对人来说都没问题,但对程序来说很麻烦。

因为程序需要知道:

复制代码
动作类型是什么?
目标坐标是多少?
输入内容是什么?
是否需要按快捷键?
是否需要等待?
任务是否结束?

所以 GUI Agent 不能只靠自然语言输出。

它必须把动作限制在一个标准集合里。

UI-TARS 论文中也明确提到,GUI Agent 需要跨移动端、桌面端和 Web 等不同平台工作,而不同平台交互方式不同,因此需要把语义相近的操作抽象成统一动作空间,例如 click、type、scroll、drag 等。论文还指出,将动作拆成原子操作,可以降低学习复杂度,并方便跨平台迁移。

这就是动作空间的意义:

把模型的自由表达,变成程序可理解、可解析、可执行的动作协议。


二、UI-TARS 的动作空间分成几类?

从 prompt.py 看,桌面端 COMPUTER_USE_DOUBAO 定义了这些动作:

复制代码
click(point='<point>x1 y1</point>')

left_double(point='<point>x1 y1</point>')

right_single(point='<point>x1 y1</point>')

drag(start_point='<point>x1 y1</point>', end_point='<point>x2 y2</point>')

hotkey(key='ctrl c')

type(content='xxx')

scroll(point='<point>x1 y1</point>', direction='down or up or right or left')

wait()

finished(content='xxx')

这些动作在源码中是直接写在 COMPUTER_USE_DOUBAO 的 Action Space 里的,同时 MOBILE_USE_DOUBAO 还增加了 long_press、open_app、press_home、press_back 等移动端特有动作。

从功能上看,桌面端动作可以分成五类:

复制代码
鼠标类:click、left_double、right_single、drag

键盘类:hotkey、type

页面导航类:scroll、wait

任务控制类:finished

平台扩展类:移动端的 long_press、open_app、press_home、press_back

也就是说,UI-TARS 并不是只会"点击"。

它试图用一套较小但完整的动作集合,覆盖绝大多数 GUI 操作场景。


三、click:最基础的原子动作

click 是 GUI Agent 最基础的动作。

格式是:

复制代码
click(point='<point>x1 y1</point>')

例如:

复制代码
Thought: 我需要点击搜索框,准备输入关键词。
Action: click(point='<point>850 120</point>')

这个动作包含两个信息:

复制代码
动作类型:click
动作参数:point 坐标

这里要注意一点:

click 不是传统 RPA 里的固定坐标点击。

传统脚本可能是录制时点过某个位置,所以回放时继续点这个位置。

而 UI-TARS 的 click 是模型根据当前截图动态生成的。

也就是说,模型需要先看懂屏幕:

复制代码
搜索框在哪里?
按钮在哪里?
目标元素是否被遮挡?
当前界面是不是预期页面?

然后才输出坐标。

所以,click 的真正难点不是"调用鼠标点击 API",而是:

如何把自然语言目标定位到截图上的具体元素。

这就是 GUI grounding。

论文也提到,点击、拖拽、滑动等动作的坐标 grounding 非常困难,因为 GUI 布局、设备比例和动态内容都会影响元素位置。


四、为什么 point 要用 <point>x y</point>?

UI-TARS 没有把点击写成:

复制代码
click(x=850, y=120)

而是写成:

复制代码
click(point='<point>850 120</point>')

这个设计看起来有点绕,但很有工程意义。

第一,<point>...</point> 明确标记了坐标区域。

模型输出中可能出现很多数字:

复制代码
日期
金额
页码
文件名
表格数据

如果没有 point 标签,parser 很难判断哪些数字是坐标。

第二,它让坐标参数变成一个统一字符串。

后面的 action_parser.py 可以专门识别 <point> 标签,再把它转换成内部坐标格式。

第三,它适合多模态模型输出。

多模态 grounding 任务中,模型经常需要把视觉目标映射成坐标。用 <point> 标签可以形成一个清晰的输出协议。

所以,point='<point>x y</point>' 不只是格式问题,而是模型输出和后处理解析之间的接口协议。


五、left_double 与 right_single:桌面端交互的必要补充

桌面端和移动端最大的区别之一是:

桌面端有双击和右键。

例如:

复制代码
双击桌面图标打开程序
双击文件打开文档
右键文件弹出菜单
右键图片选择另存为
右键窗口调用上下文菜单

所以 COMPUTER_USE_DOUBAO 中额外定义了:

复制代码
left_double(point='<point>x1 y1</point>')

right_single(point='<point>x1 y1</point>')

这两个动作本质上仍然是"基于坐标的鼠标操作",但语义和 click 不同。

同一个点,单击、双击、右键可能触发完全不同的行为。

例如:

复制代码
单击文件:选中文件
双击文件:打开文件
右键文件:弹出菜单

如果动作空间里只有 click,模型就很难表达这些桌面端常见操作。

这说明 UI-TARS 的动作空间设计不是随便列几个 API,而是根据真实 GUI 使用习惯抽象出来的。


六、drag:从单点定位到双点轨迹

drag 的格式是:

复制代码
drag(
  start_point='<point>x1 y1</point>',
  end_point='<point>x2 y2</point>'
)

它比 click 更复杂。

click 只需要一个点。

drag 需要两个点:

复制代码
起点
终点

这意味着模型不仅要知道"操作哪个元素",还要知道"把它拖到哪里"。

drag 可以覆盖很多场景:

复制代码
拖动窗口
拖动滑块
拖拽文件
框选文本
框选区域
调整分隔栏
移动地图
拖动进度条

论文中的统一动作空间也把 Drag 定义为共享动作之一,即从一个坐标拖拽到另一个坐标。

这说明 GUI Agent 不能只关注点击任务。

真实桌面操作里,拖拽非常常见,而且比点击更难。

原因是它要求模型理解:

复制代码
起点元素是什么?
终点位置在哪里?
拖拽路径是否合理?
拖拽后界面会发生什么变化?

所以,drag 是检验 GUI Agent 是否具备复杂交互能力的重要动作。


七、hotkey:把键盘快捷键纳入动作空间

hotkey 的格式是:

复制代码
hotkey(key='ctrl c')

prompt.py 中还特别约束:快捷键按空格分隔,使用小写,并且一个 hotkey 动作不要超过 3 个按键。

例如:

复制代码
hotkey(key='ctrl c')
hotkey(key='ctrl v')
hotkey(key='ctrl s')
hotkey(key='alt tab')
hotkey(key='ctrl shift s')

这个设计非常实用。

因为很多桌面操作用快捷键比鼠标更稳定:

复制代码
复制:ctrl c
粘贴:ctrl v
保存:ctrl s
全选:ctrl a
切换窗口:alt tab
打开搜索:ctrl f

如果没有 hotkey,模型可能需要通过复杂的菜单点击完成同样操作。

例如保存文件,可以走菜单:

复制代码
点击 File
点击 Save

也可以直接:

复制代码
hotkey(key='ctrl s')

后者更短、更稳定,也更接近熟练用户的操作方式。

所以,hotkey 的加入让 UI-TARS 不只是"视觉点击模型",而是具备桌面操作习惯的 GUI Agent。


八、type:输入文本不是简单键盘敲字

type 的格式是:

复制代码
type(content='xxx')

例如:

复制代码
Thought: 搜索框已经获得焦点,下一步输入关键词并提交。
Action: type(content='UI-TARS\n')

prompt.py 对 type 有一个很细的要求:

复制代码
Use escape characters \', \", and \n in content part

也就是说,输入内容中如果包含单引号、双引号、换行符,要使用转义字符,保证它能被正常 Python 字符串解析。

这个细节很重要。

因为模型输出最终不是给人看的,而是要被 parser 解析。

如果模型输出:

复制代码
type(content='it's good')

中间的单引号就可能破坏字符串结构。

正确做法应该是:

复制代码
type(content='it\'s good')

另外,如果想提交输入,可以在结尾加:

复制代码
\n

例如:

复制代码
type(content='UI-TARS\n')

这表示输入关键词后按回车。

所以,type 并不只是"输入文字"。

它还承担了:

复制代码
文本内容传递
字符串转义
输入提交
和 parser 的格式兼容

这些工程约束。


九、scroll:为什么滚动也需要 point?

scroll 的格式是:

复制代码
scroll(point='<point>x1 y1</point>', direction='down or up or right or left')

它有两个参数:

复制代码
point
direction

很多人可能会疑惑:

滚动为什么还要 point?直接 scroll down 不就行了吗?

原因是 GUI 里可能同时存在多个可滚动区域。

例如:

复制代码
左侧菜单可以滚动
主内容区可以滚动
表格内部可以滚动
弹窗内部可以滚动
文件列表可以滚动

鼠标停在哪里,往往决定滚动哪个区域。

所以,scroll 需要 point 来指定滚动发生的位置。

例如:

复制代码
Thought: 当前列表没有显示目标文件,需要在文件列表区域向下滚动。
Action: scroll(point='<point>600 720</point>', direction='down')

这里的 point 不是要点击,而是告诉执行层:

复制代码
把鼠标移动到这个区域附近,再执行滚轮操作。

这体现了 UI-TARS 对真实 GUI 交互的理解:滚动不是全局动作,而是区域相关动作。


十、wait:等待也是一种动作

wait() 看起来不像操作。

但在 GUI Agent 里,它非常重要。

prompt.py 中的注释说明,wait() 会等待 5 秒并重新截图检查变化。

为什么等待也要作为动作?

因为真实软件里很多变化不是立即发生的:

复制代码
网页加载
搜索结果刷新
文件上传
下载完成
弹窗出现
登录跳转
动画过渡
服务端处理

如果模型不知道等待,它可能会连续点击,导致错误。

例如:

复制代码
点击登录
页面还没跳转
模型又点击了一次登录
产生重复请求或报错

所以 wait 的意义是告诉 Agent:

当前不是缺少操作,而是需要等待界面变化。

这和传统脚本中的 sleep(2) 有点像,但又不同。

传统脚本通常固定等待。

UI-TARS 则可以根据当前截图决定是否等待。

这让等待动作也变成了模型决策的一部分。


十一、finished:动作空间里的终止符

finished 的格式是:

复制代码
finished(content='xxx')

例如:

复制代码
Thought: 文件已经下载完成,任务目标已经达成。
Action: finished(content='文件已经下载完成。')

它表示任务结束。

如果没有 finished,外层 Agent Loop 就不知道什么时候停止。

完整 GUI Agent 循环通常是:

复制代码
截图
  ↓
模型输出 Thought + Action
  ↓
解析 Action
  ↓
执行动作
  ↓
再次截图
  ↓
继续循环

什么时候退出?

就靠:

复制代码
finished(...)

论文里的统一动作空间也把 Finished 定义为终止动作,用来标记任务完成。

所以 finished 不是普通操作,而是控制整个任务生命周期的信号。


十二、为什么动作要设计成"函数调用"形式?

UI-TARS 的动作格式都像函数调用:

复制代码
click(point='...')

drag(start_point='...', end_point='...')

hotkey(key='...')

type(content='...')

scroll(point='...', direction='...')

这样设计有几个好处。

第一,动作类型清晰。

函数名就是动作类型:

复制代码
click
drag
type
scroll

第二,参数结构清晰。

每个动作需要什么参数,一眼就能看出来。

第三,方便 parser 解析。

action_parser.py 中有 parse_action、parse_action_to_structure_output、parsing_response_to_pyautogui_code 等函数,用来把模型输出解析成结构化动作,并进一步转成 pyautogui 代码。项目 README 的 Quick Start 也展示了这条后处理链路:先解析模型输出,再生成 pyautogui 自动化代码。

第四,方便扩展。

以后如果要增加动作,可以继续沿用函数调用格式:

复制代码
switch_window(title='xxx')
open_file(path='xxx')
select_menu(name='xxx')
confirm_dialog()

只要 Prompt、Parser 和 Executor 同步扩展,就能形成新的动作协议。


十三、动作空间和 action_parser.py 的关系

动作空间不是只写在 Prompt 里给模型看的。

它必须和后面的解析器配合。

整体链路是:

复制代码
prompt.py
定义模型可以输出哪些动作
    ↓

模型输出
Thought + Action
    ↓

action_parser.py
解析动作名称和参数
    ↓

结构化动作
{
  "action_type": "...",
  "action_inputs": {...}
}
    ↓

执行层
生成 pyautogui 代码并执行

比如模型输出:

复制代码
Action: scroll(point='<point>600 720</point>', direction='down')

parser 需要识别出:

复制代码
action_type = scroll
point = 600 720
direction = down

然后执行层才知道应该把鼠标移动到指定区域,并执行向下滚动。

所以动作空间本质上是三方协议:

复制代码
Prompt 和模型之间的协议
模型和 Parser 之间的协议
Parser 和执行层之间的协议

这也是为什么动作格式不能随便写。


十四、动作空间为什么要小而精?

理论上,动作空间可以设计得非常复杂。

例如:

复制代码
open_browser()
search_google(query='')
download_file(url='')
save_as(path='')
send_email(to='', content='')

这些动作看起来很高级。

但 UI-TARS 没有这样设计,而是主要保留原子动作:

复制代码
click
drag
scroll
type
hotkey
wait
finished

这是有原因的。

如果动作太高级,模型虽然可以一步完成更多事,但执行层会变得复杂,而且跨平台适配困难。

比如 download_file 在不同浏览器、不同网站、不同权限状态下行为都不一样。

但如果拆成原子动作,模型只需要学会:

复制代码
点击下载按钮
等待弹窗
选择路径
点击保存
等待完成

这样更接近真人操作,也更容易迁移到不同软件。

论文中也提到,动作可以分为原子动作和组合动作,拆成原子操作有助于降低学习复杂度,并促进跨平台动作迁移。

所以,UI-TARS 的动作空间不是追求"一个动作做很多事",而是追求:

少量动作,覆盖广泛 GUI 场景。


十五、跨平台统一:click 与 tap 的抽象

在桌面端,我们说 click。

在移动端,我们说 tap。

但从模型角度看,它们语义相似:

复制代码
在某个屏幕坐标触发一次选择操作

所以 UI-TARS 论文把不同平台中语义等价的动作标准化到统一动作空间中,例如 Windows 上的 click 和移动端的 tap 可以统一建模。

这就是 Unified Action Modeling 的核心思想。

它不是说所有平台动作完全一样。

而是说:

复制代码
共享动作尽量统一
平台特有动作单独扩展

例如共享动作:

复制代码
Click
Drag
Scroll
Type
Wait
Finished

桌面端特有动作:

复制代码
Hotkey
LeftDouble
RightSingle

移动端特有动作:

复制代码
LongPress
PressBack
PressHome
PressEnter

这种设计既保证了统一性,也保留了平台差异。


十六、从开发者角度看,动作空间应该怎么扩展?

如果你想基于 UI-TARS 做自己的自动化产品,动作空间很可能需要扩展。

比如桌面自动化 App 可能需要:

复制代码
switch_window(title='xxx')
activate_window(title='xxx')
copy_selected_text()
paste_text(content='xxx')
open_file(path='xxx')
run_command(command='xxx')

但扩展动作时一定要注意:

不能只改 Prompt。

完整扩展链路应该是:

复制代码
第一步:在 prompt.py 里增加动作说明

第二步:在 action_parser.py 里增加解析逻辑

第三步:在执行层实现动作

第四步:增加测试用例

第五步:增加安全限制

例如你增加:

复制代码
delete_file(path='xxx')

模型可能会在错误场景下误输出这个动作。

所以这类高风险动作必须加确认机制。

GUI Agent 的动作空间不是越大越好。

动作越多,模型越容易混淆,执行层越复杂,安全风险也越高。

更合理的方式是:

复制代码
基础动作保持稳定
高风险动作谨慎加入
复杂动作尽量由原子动作组合完成

十七、UI-TARS 动作空间的设计启发

从 UI-TARS 的动作空间设计里,可以总结出几个通用原则。

第一,动作要足够少。

动作太多会增加模型学习和解析难度。

第二,动作要足够通用。

click、type、scroll、drag 这些动作可以覆盖大量 GUI 场景。

第三,动作要有明确参数。

例如 click 必须有 point,scroll 必须有 point 和 direction,type 必须有 content。

第四,动作要容易落地执行。

最终要能映射到 pyautogui、系统 API 或移动端自动化接口。

第五,动作要支持任务闭环。

wait 和 finished 就不是普通界面操作,但对 Agent Loop 非常重要。

第六,动作要保留平台差异。

桌面端需要 hotkey、right click、double click,移动端需要 back、home、long press。


总结

这篇文章我们分析了 UI-TARS 的动作空间设计。

UI-TARS 没有让模型自由输出自然语言操作,而是把 GUI 任务统一成一组标准动作:

复制代码
click
left_double
right_single
drag
hotkey
type
scroll
wait
finished

这些动作背后体现的是 GUI Agent 工程化的核心思想:

复制代码
用 click 解决单点交互
用 drag 解决连续轨迹操作
用 hotkey 复用熟练用户的键盘习惯
用 type 处理文本输入和提交
用 scroll 处理区域滚动
用 wait 处理界面延迟
用 finished 控制任务结束

从论文角度看,这对应 Unified Action Modeling:把不同平台、不同 GUI 环境中的操作统一到一个可学习、可解析、可迁移的动作空间中。

从源码角度看,这些动作首先在 prompt.py 中定义,然后由模型输出,再由 action_parser.py 解析,最后转换成 pyautogui 等底层执行代码。

所以,动作空间不是简单的 API 列表。

它是连接:

复制代码
用户任务
模型推理
屏幕坐标
动作解析
系统执行

之间的核心协议。

相关推荐
传奇开心果编程2 小时前
【声明式UI开发实用技术学与练】第9课 派生状态与计算
ui·移动开发·ux·编程框架·声明式ui
ZARD9964 小时前
Codex 连接 Figma:让 AI 读取 Figma UI 设计稿数据
ui·figma·codex
事圆则缓5 小时前
Android 一次性事件怎么设计:Channel、SharedFlow 还是 UI State?
android·ui
我命由我123455 小时前
Photoshop - Photoshop 使用魔棒工具选择单独的区域
学习·ui·职场和发展·求职招聘·职场发展·学习方法·photoshop
特立独行的猫a5 小时前
用仓颉语言给娃写了个打字练习游戏:cj-tauri 项目实战
游戏·ui·框架·tauri·仓颉·cangjie·cj-tauri
传奇开心果编程6 小时前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第7课 平台适配与互操作
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程1 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
传奇开心果编程1 天前
【Compose Multiplatform 跨端开发学与练】第5课 网络与数据层
android·网络·学习·ui·ios·kotlin·composer
传奇开心果编程2 天前
【SwiftUI提高练中学】第8课 网络层架构:拦截器、重试、缓存与离线优先
学习·macos·ui·ios·swiftui