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