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_pressopen_apppress_homepress_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.pytype 有一个很细的要求:

复制代码
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_actionparse_action_to_structure_outputparsing_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 列表。

它是连接:

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

之间的核心协议。

相关推荐
小堂子这厢有礼了5 小时前
Chet.QuartzNet.UI v2.3.0 大更新!表格重构 + 系统配置 + 分析页全面升级!
前端·后端·ui·重构·c#·vue
youtootech14 小时前
HarmonyOS《柚兔学伴》项目实战09-待办列表 UI——List+ForEach+滑动删除
数据结构·ui·华为·list·harmonyos
ndsc_d1 天前
前端实战复盘:用AI直接生成响应式官网落地页与React源码
前端·人工智能·react.js·ui·前端框架·aigc·paico
monsion1 天前
AG-UI 协议详解:Agent 与用户之间缺失的那一层
人工智能·ui·个人开发
Full Stack Developme1 天前
flowable-spring-boot-starter 与 flowable-ui
ui
天天进步20151 天前
UI-TARS 源码解析 #5:prompt.py 解析:COMPUTER_USE、MOBILE_USE、GROUNDING 三种提示词模板
ui·prompt
一键生成网站1 天前
AI生成UI原型支持复杂逻辑交互工具对比与场景适配分析
人工智能·ui·交互·
糯米导航2 天前
AI 视觉回归实战:截图对比不是“找不同”,如何让智能差异分析真正服务 UI 质量
人工智能·ui·回归