在上一篇文章中,我们对 action_parser.py 做了整体总览。
它的核心作用是把模型输出的文本:
text
Thought: 我需要点击搜索框。
Action: click(point='<point>850 120</point>')
转换成程序可以理解的结构化动作:
json
{
"action_type": "click",
"action_inputs": {
"start_box": "[0.44, 0.11, 0.44, 0.11]"
}
}
然后再进一步生成 pyautogui 自动化代码。
这一篇,我们深入 action_parser.py 里的第一个关键函数:
python
parse_action(action_str)
这个函数非常小,但很关键。
因为它负责把模型生成的:
text
click(start_box='(850,120)')
解析成:
json
{
"function": "click",
"args": {
"start_box": "(850,120)"
}
}
也就是说,parse_action 是模型文本动作进入结构化世界的第一道门。
一、为什么需要 parse_action?
在 UI-TARS 中,Prompt 会要求模型按照函数调用形式输出动作。
例如:
text
click(point='<point>850 120</point>')
type(content='UI-TARS\n')
hotkey(key='ctrl c')
scroll(point='<point>600 720</point>', direction='down')
drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')
这些 Action 看起来像 Python 函数调用,但它们本质上仍然是模型生成的字符串。
程序不能直接执行它们。
因为模型输出可能来自不稳定的自然语言生成过程,里面可能有格式错误、参数缺失、引号错误,甚至出现不应该执行的内容。
所以系统必须先做一件事:
只解析它,不执行它。
parse_action 就是做这个事情的。
它不负责点击鼠标,也不负责坐标换算,只负责回答三个问题:
text
这是一个合法的函数调用吗?
函数名是什么?
关键字参数有哪些?
这一步成功以后,后续代码才能继续处理坐标、动作类型和 pyautogui 代码生成。
二、parse_action 在源码中的位置
在 action_parser.py 中,parse_action 位于较靠前的位置。
它后面会被 parse_action_to_structure_output 调用。
整体链路大概是:
text
模型输出文本
↓
parse_action_to_structure_output
↓
提取 Action 字符串
↓
parse_action
↓
得到 function + args
↓
生成结构化 action dict
官方 codes/README.md 对 ui-tars 包的定位也很清楚:它用于解析 VLM 生成的 GUI 动作指令,自动生成 pyautogui 脚本,并支持坐标转换和智能图像缩放。也就是说,parse_action 属于整个"模型输出后处理链路"的底层函数。
三、parse_action 的核心代码逻辑
parse_action 的源码逻辑可以简化成下面这样:
python
def parse_action(action_str):
try:
node = ast.parse(action_str, mode='eval')
if not isinstance(node, ast.Expression):
raise ValueError("Not an expression")
call = node.body
if not isinstance(call, ast.Call):
raise ValueError("Not a function call")
if isinstance(call.func, ast.Name):
func_name = call.func.id
elif isinstance(call.func, ast.Attribute):
func_name = call.func.attr
else:
func_name = None
kwargs = {}
for kw in call.keywords:
key = kw.arg
if isinstance(kw.value, ast.Constant):
value = kw.value.value
elif isinstance(kw.value, ast.Str):
value = kw.value.s
else:
value = None
kwargs[key] = value
return {
"function": func_name,
"args": kwargs
}
except Exception as e:
print(f"Failed to parse action '{action_str}': {e}")
return None
真实源码中可以看到,它确实使用 ast.parse(action_str, mode='eval') 解析字符串,然后检查结果是否是 ast.Expression,再检查主体是否是 ast.Call,最后提取函数名和关键字参数。
这个函数不长,但设计很明确:
它只接受"一个表达式形式的函数调用"。
例如:
text
click(start_box='(850,120)')
可以解析。
但下面这些就不应该被当作正常 Action:
text
x = click(...)
import os
click(...)
os.system('rm -rf /')
因为它们不是 UI-TARS 期望的"单个动作函数调用"。
四、为什么使用 ast.parse,而不是 eval?
这是这篇文章最重要的点。
如果只是想从字符串里得到函数名和参数,最危险的方式是:
python
eval(action_str)
例如:
python
eval("click(start_box='(850,120)')")
问题是,eval 会执行表达式。
如果模型输出了恶意内容,直接 eval 就可能执行不该执行的代码。
而 ast.parse 不会直接执行代码,它只是把源码字符串解析成抽象语法树。Python 官方文档也说明,ast 模块用于处理 Python 抽象语法树,ast.parse() 会把 source 解析成 AST 节点。
也就是说:
text
eval:
解析并执行。
ast.parse:
只解析成语法树,不执行。
这就是为什么 parse_action 使用 AST。
它把模型输出当成"语法结构"来分析,而不是当成"代码"来运行。
这比 eval 安全得多。
不过也要注意:
ast.parse本身只是解析,不等于整个执行链路绝对安全。
因为 UI-TARS 后面的 parsing_response_to_pyautogui_code 里还会对结构化坐标字符串使用 eval(start_box) 之类的逻辑来还原列表或元组。这个地方如果做产品化,需要额外加安全替代方案,比如使用 ast.literal_eval 或严格的数值解析。
所以更准确地说:
parse_action用 AST 避免了直接执行模型生成的函数调用,但整个系统仍然需要额外安全校验。
五、为什么不用正则表达式?
另一种常见做法是用正则。
例如:
python
pattern = r"(\w+)\((.*)\)"
它可以解析:
text
click(start_box='(850,120)')
得到:
text
函数名:click
参数部分:start_box='(850,120)'
但问题是,Action 一复杂,正则就很容易失控。
例如:
text
type(content='hello, world')
参数里有逗号。
再比如:
text
type(content='it\'s good')
参数里有转义单引号。
再比如:
text
drag(start_box='(300,500)', end_box='(700,500)')
有多个关键字参数。
再比如:
text
scroll(start_box='(600,720)', direction='down')
参数类型不同。
如果用正则,需要不断处理:
text
引号
逗号
转义字符
多个参数
空格
换行
代码会越来越复杂。
而 AST 的优势是,它天然理解 Python 函数调用结构。
例如 Python 文档中展示,ast.parse('func(a, b=c, *d, **e)', mode='eval') 会得到一个 Expression,其主体是 Call 节点,里面包含函数、位置参数和关键字参数。
所以对 UI-TARS 这种"函数调用式 Action"来说,AST 比正则更合适。
六、parse_action 的第一步:mode='eval'
源码里第一句关键代码是:
python
node = ast.parse(action_str, mode='eval')
这里的 mode='eval' 很重要。
Python 的 ast.parse 支持不同模式。
如果是默认模式,通常解析的是完整模块或语句。
但 UI-TARS 的 Action 不是完整 Python 文件,也不是赋值语句,而是单个表达式:
text
click(start_box='(850,120)')
所以它使用 mode='eval'。
在这个模式下,解析结果应该是:
python
ast.Expression
也就是一个表达式节点。
源码紧接着就检查:
python
if not isinstance(node, ast.Expression):
raise ValueError("Not an expression")
这一步的意思是:
如果模型输出的不是表达式,就直接拒绝。
例如:
text
x = click(start_box='(850,120)')
这不是单个表达式,而是赋值语句,不符合 UI-TARS 的 Action 协议。
七、第二步:检查是不是函数调用 ast.Call
通过第一步后,源码继续:
python
call = node.body
if not isinstance(call, ast.Call):
raise ValueError("Not a function call")
这一步非常关键。
即使一个字符串是合法表达式,也不一定是函数调用。
例如:
text
123
"hello"
start_box
1 + 2
这些都可能是合法表达式,但不是 UI-TARS 的动作。
UI-TARS 期望的是:
text
click(...)
type(...)
scroll(...)
所以必须检查:
python
ast.Call
只有 AST 主体是函数调用时,才继续提取函数名和参数。
这一步相当于给 Action 做了一层结构校验:
text
合法表达式?
↓
是函数调用?
↓
提取函数名和参数
八、第三步:提取函数名
源码中函数名提取逻辑是:
python
if isinstance(call.func, ast.Name):
func_name = call.func.id
elif isinstance(call.func, ast.Attribute):
func_name = call.func.attr
else:
func_name = None
这里考虑了两种形式。
第一种是普通函数调用:
text
click(start_box='(850,120)')
这种情况下,call.func 是 ast.Name,函数名是:
text
click
第二种是属性调用:
text
pyautogui.click(start_box='(850,120)')
这种情况下,call.func 是 ast.Attribute,源码取的是属性名:
text
click
所以,parse_action 理论上可以兼容:
text
click(...)
和:
text
xxx.click(...)
但从 Prompt 设计来看,UI-TARS 更希望模型直接输出:
text
click(...)
而不是:
text
pyautogui.click(...)
因为模型输出的是抽象动作,不应该直接绑定到底层执行库。
这也体现了 UI-TARS 的分层设计:
text
模型输出:
click(...)
Parser:
解析成结构化动作
Executor:
再决定是否映射到 pyautogui.click(...)
模型不直接写 pyautogui,这样后续也可以换成别的执行层。
九、第四步:提取关键字参数
函数名提取完后,源码会遍历:
python
for kw in call.keywords:
key = kw.arg
也就是说,它只处理关键字参数。
例如:
text
click(start_box='(850,120)')
会得到:
json
{
"start_box": "(850,120)"
}
再例如:
text
scroll(start_box='(600,720)', direction='down')
会得到:
json
{
"start_box": "(600,720)",
"direction": "down"
}
这也是为什么 UI-TARS 的 Prompt 里动作参数都写成关键字形式:
text
point='...'
content='...'
key='...'
direction='...'
而不是:
text
click('(850,120)')
关键字参数的好处是:
text
参数语义清楚
顺序不敏感
方便解析
方便后续扩展
例如:
text
scroll(start_box='(600,720)', direction='down')
即使参数顺序换成:
text
scroll(direction='down', start_box='(600,720)')
结构化结果仍然可以正确表达含义。
十、第五步:只接受常量值
源码中对参数值的处理是:
python
if isinstance(kw.value, ast.Constant):
value = kw.value.value
elif isinstance(kw.value, ast.Str):
value = kw.value.s
else:
value = None
也就是说,它主要接受字符串、数字等常量值。
例如:
text
click(start_box='(850,120)')
start_box 是字符串常量,可以解析。
text
hotkey(key='ctrl c')
key 是字符串常量,可以解析。
text
type(content='UI-TARS\n')
content 是字符串常量,可以解析。
但如果模型输出:
text
click(start_box=get_position())
或者:
text
type(content='hello' + 'world')
这些参数值不是简单常量,parse_action 会把它们处理成 None。
这个设计很重要。
因为 UI-TARS 的 Action 参数应该是静态数据,不应该是表达式计算。
也就是说,模型只能告诉系统:
text
我要点击哪里
我要输入什么
我要按什么键
而不能让模型生成一段逻辑表达式给系统执行。
这能减少风险,也能让动作结构更稳定。
十一、parse_action 的返回值
如果解析成功,parse_action 返回:
python
{
"function": func_name,
"args": kwargs
}
例如输入:
text
click(start_box='(850,120)')
返回:
json
{
"function": "click",
"args": {
"start_box": "(850,120)"
}
}
输入:
text
scroll(start_box='(600,720)', direction='down')
返回:
json
{
"function": "scroll",
"args": {
"start_box": "(600,720)",
"direction": "down"
}
}
输入:
text
hotkey(key='ctrl c')
返回:
json
{
"function": "hotkey",
"args": {
"key": "ctrl c"
}
}
这个结构正好对应后续 parse_action_to_structure_output 需要的字段:
text
function → action_type
args → action_inputs
所以,parse_action 的职责非常纯粹:
把函数调用字符串拆成函数名和参数字典。
十二、解析失败时返回 None
如果解析失败,源码会捕获异常:
python
except Exception as e:
print(f"Failed to parse action '{action_str}': {e}")
return None
例如:
text
click start_box='(850,120)'
不是合法函数调用。
或者:
text
click(start_box='(850,120)'
缺少右括号。
或者:
text
I will click the button.
不是函数调用。
这些都会解析失败,返回 None。
后续 parse_action_to_structure_output 会检查解析结果,如果是 None,就抛出错误:
python
raise ValueError(f"Action can't parse: {raw_str}")
这说明 UI-TARS 对 Action 格式是比较严格的。
模型可以在 Thought 中自由表达,但 Action 必须符合约定格式。
十三、parse_action 和 prompt.py 的配合
现在回头看 prompt.py,就会发现很多格式约束都是为了服务 parse_action。
例如 Prompt 要求:
text
click(point='<point>x1 y1</point>')
而不是:
text
click at x1 y1
因为前者可以被 AST 当成函数调用解析。
又比如 Prompt 要求:
text
hotkey(key='ctrl c')
而不是:
text
press control and c
因为前者有明确的函数名和关键字参数。
再比如 Prompt 要求 type(content='xxx') 中的引号、换行符要正确转义。
因为如果字符串不合法,ast.parse 就会失败。
所以,prompt.py 和 parse_action 本质上是一套协议的两端:
text
prompt.py:
约束模型输出"像函数调用一样"的 Action。
parse_action:
按照函数调用结构解析模型输出。
没有 Prompt 约束,Parser 会很复杂。
没有 Parser,Prompt 输出也无法进入执行层。
十四、为什么这种设计比 JSON 更适合当前场景?
有人可能会问:
为什么不用 JSON?让模型直接输出
{ "action": "click", "x": 850, "y": 120 }不行吗?
当然可以。
很多 Agent 框架确实使用 JSON。
但是在 UI-TARS 这里,函数调用式 Action 也有它的优势。
第一,它更短。
text
click(point='<point>850 120</point>')
比 JSON 更紧凑。
第二,它更接近动作语义。
text
click(...)
type(...)
scroll(...)
一眼就能看出动作类型。
第三,它更方便和自然语言 Thought 放在一起。
text
Thought: ...
Action: click(...)
第四,它方便用 AST 解析。
因为它刚好符合 Python 表达式形式。
当然,JSON 也有优势,比如更标准、更容易做 schema 校验。
如果做更严格的生产系统,JSON Schema 或 function calling 也可以考虑。
但就 UI-TARS 当前开源代码来说,函数调用式 Action 加 AST 解析,是一种轻量、直接、工程成本低的方案。
十五、parse_action 的安全边界
虽然本文标题里有"安全解析",但这里必须说清楚:
parse_action 的安全性主要来自三点:
text
第一,它使用 ast.parse,只解析语法树,不直接执行模型输出。
第二,它检查结果必须是 ast.Expression 和 ast.Call。
第三,它只提取常量参数,不执行参数表达式。
这比直接 eval(action_str) 安全得多。
但它还不是完整安全沙箱。
原因有几个。
第一,函数名没有白名单。
例如模型输出:
text
delete_file(path='/important')
parse_action 也能解析出:
json
{
"function": "delete_file",
"args": {
"path": "/important"
}
}
虽然后续执行层未必支持这个动作,但 Parser 本身不会拒绝它。
第二,参数值缺少类型和范围校验。
例如:
text
click(start_box='(999999,999999)')
可以被解析,但坐标是否有效,要靠后续处理。
第三,后续代码生成阶段仍然需要安全检查。
特别是坐标还原、字符串输入、文件操作、高风险点击等,都需要额外限制。
所以,如果要产品化,我建议在 parse_action 后增加一个 action validation 层。
例如:
python
ALLOWED_ACTIONS = {
"click",
"left_double",
"right_single",
"drag",
"hotkey",
"type",
"scroll",
"wait",
"finished",
}
if action_type not in ALLOWED_ACTIONS:
raise ValueError(f"Unsupported action type: {action_type}")
再进一步,可以为每种动作定义参数 schema:
python
ACTION_SCHEMAS = {
"click": ["start_box"],
"drag": ["start_box", "end_box"],
"hotkey": ["key"],
"type": ["content"],
"scroll": ["start_box", "direction"],
"finished": ["content"],
}
这样才能真正把模型输出控制在安全范围内。
十六、parse_action 的局限
parse_action 简洁,但也有几个明显局限。
1. 只支持关键字参数
它主要读取 call.keywords。
如果模型输出:
text
click('(850,120)')
这是位置参数,当前逻辑不会把它解析到 args 里。
所以 Prompt 必须要求模型使用关键字参数:
text
click(start_box='(850,120)')
2. 非常依赖合法 Python 字符串
如果输入文本包含未转义引号:
text
type(content='it's good')
AST 会解析失败。
所以 parse_action_to_structure_output 里专门对 type(content=...) 做了额外处理。
3. 不处理复杂参数
如果参数是列表、字典、表达式,当前逻辑会变成 None。
例如:
text
click(start_box=[0.1, 0.2, 0.1, 0.2])
这里参数是 list 节点,不是简单 ast.Constant,当前 parse_action 不会完整处理。
4. 没有动作白名单
它负责解析,不负责判断动作是否允许。
这在工程上可以接受,但生产系统最好补一个校验层。
十七、可以怎样改进 parse_action?
如果要把 UI-TARS 用到更严肃的桌面自动化产品里,可以考虑增强 parse_action。
1. 增加动作白名单
python
ALLOWED_ACTIONS = {
"click",
"left_double",
"right_single",
"drag",
"hotkey",
"type",
"scroll",
"wait",
"finished",
}
解析出函数名后立即校验。
2. 使用 ast.literal_eval 解析参数值
对于列表、元组、数字、字符串等字面量,可以考虑使用 ast.literal_eval。
这样能支持:
text
click(start_box=[0.1, 0.2, 0.1, 0.2])
但仍避免执行任意代码。
3. 增加参数 schema 校验
例如:
text
click 必须有 start_box
drag 必须有 start_box 和 end_box
scroll 必须有 direction
type 必须有 content
4. 增加坐标格式校验
例如检查:
text
坐标必须是 2 个或 4 个数字
归一化坐标必须在 0 到 1 之间
绝对坐标不能超过图片尺寸
5. 明确拒绝 ast.Attribute
当前源码支持 ast.Attribute,也就是:
text
xxx.click(...)
如果希望模型只输出抽象动作,可以拒绝 attribute 调用,只允许:
text
click(...)
这样能减少误解析和执行层混淆。
十八、一个完整解析例子
假设模型输出:
text
Action: scroll(start_box='(600,720)', direction='down')
parse_action 实际处理的是:
text
scroll(start_box='(600,720)', direction='down')
解析流程如下:
text
1. ast.parse(..., mode='eval')
得到 Expression 节点
2. node.body
得到 Call 节点
3. call.func
是 ast.Name,函数名为 scroll
4. call.keywords
包含两个 keyword:
start_box='(600,720)'
direction='down'
5. 每个 keyword 的 value 是 ast.Constant
6. 返回:
{
"function": "scroll",
"args": {
"start_box": "(600,720)",
"direction": "down"
}
}
后续 parse_action_to_structure_output 再把这个结果进一步转成:
json
{
"action_type": "scroll",
"action_inputs": {
"start_box": "[0.3125, 0.6667, 0.3125, 0.6667]",
"direction": "down"
}
}
最后 parsing_response_to_pyautogui_code 才会生成滚动代码。
十九、parse_action 的设计启发
parse_action 给我们做 Agent 工程化提供了几个启发。
第一,不要直接执行模型输出。
模型输出必须先解析、校验、结构化,再进入执行层。
第二,Prompt 和 Parser 要成对设计。
你希望 Parser 怎么解析,就要在 Prompt 中约束模型怎么输出。
第三,动作格式要足够简单。
函数调用式 Action 很适合表达"动作类型 + 参数"。
第四,解析层和执行层要分离。
parse_action 只负责解析,不负责执行,这样更清晰,也更容易调试。
第五,安全不能只靠 AST。
AST 只是第一道门,后面还需要动作白名单、参数校验、坐标校验和高风险操作确认。
总结
这篇文章我们深入分析了 UI-TARS 的 parse_action 函数。
它的核心作用是:
text
把模型生成的函数调用式 Action 字符串,
解析成 function + args 的结构化结果。
它的关键设计包括:
text
使用 ast.parse(..., mode='eval') 解析表达式;
要求解析结果必须是 ast.Expression;
要求表达式主体必须是 ast.Call;
支持 ast.Name 和 ast.Attribute 提取函数名;
只提取关键字参数;
主要接受 ast.Constant / ast.Str 这类常量值;
解析失败时返回 None。
相比正则,AST 更适合解析函数调用结构。
相比 eval,AST 只解析不执行,更适合处理模型生成的动作文本。
但 parse_action 也不是完整安全方案。
如果要用于真实产品,还应该增加:
text
动作白名单
参数 schema 校验
坐标范围检查
高风险动作拦截
更安全的字面量解析
从 UI-TARS 的整体架构看,parse_action 是一个很小但很关键的函数。
它把模型输出从"自然语言文本"推进到了"结构化动作"的第一步。