UI-TARS 源码解析 #10:parse_action 源码解析:如何用 AST 安全解析模型生成的函数调用?

在上一篇文章中,我们对 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.mdui-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.funcast.Name,函数名是:

text 复制代码
click

第二种是属性调用:

text 复制代码
pyautogui.click(start_box='(850,120)')

这种情况下,call.funcast.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.pyparse_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 是一个很小但很关键的函数。

它把模型输出从"自然语言文本"推进到了"结构化动作"的第一步。

相关推荐
大耳朵-小飞象3 小时前
电力安全运维的智能密码:BACS如何破解设备全生命周期管理难题,让电网安全“看得见、管得住”?
运维·安全·智慧城市·能耗系统·楼宇智控·未来生活
wuhanzhanhui5 小时前
视觉革命与主动安全:2026武汉汽车电子展会定档,看芯片如何定义驾驶
安全·汽车
王维同学7 小时前
[原创][Windows C++]LSA 认证、安全与通知包的注册表枚举
c++·windows·安全
国科安芯9 小时前
星间光链路:AS32S601型抗辐射MCU在空间激光通信终端控制中的技术实现
服务器·网络·单片机·嵌入式硬件·物联网·安全·信息与通信
栩栩云生10 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
FuckPatience10 小时前
Telerik UI for WPF 值不能为null。参数名:key
ui·wpf
huijingjituan11 小时前
新一代IM聊天软件|构建企业专属智能通讯生态
安全·实时互动·即时通讯·极光鸟·灰鲸
其实防守也摸鱼11 小时前
补天SRC新手入门指南:从0到1的漏洞挖掘之路
网络·python·学习·安全·web安全·数据挖掘·挖洞
breeze jiang11 小时前
从原生事件到 React 组件化:用状态驱动模型加载进度 UI
前端·react.js·ui