在上一篇文章中,我们分析了 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 列表。
它是连接:
用户任务
模型推理
屏幕坐标
动作解析
系统执行
之间的核心协议。