当前位置: 首页 > news >正文

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

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

click

第二种是属性调用:

pyautogui.click(start_box='(850,120)')

这种情况下,call.funcast.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.pyparse_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 必须有 content

4. 增加坐标格式校验

例如检查:

坐标必须是 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是一个很小但很关键的函数。

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

http://www.jsqmd.com/news/1243461/

相关文章:

  • C 语言工业级通用组件手写 13:事件队列
  • 2026 年新消息:满洲里有实力的矽钢企业怎么联系,揭秘高频电路的秘密武器:它如何重塑你的电子设计?-瑞产钢板 - 实业推荐官【官方】
  • 2026北京上海初高中托福择校指南|托福机构「青少年适配度」梯队排名 - 信息热点
  • 在线视频去水印工具推荐:2026免费网站优缺点与风险提醒 - 办公小帮手
  • Belt函数缓存技巧:提升PHP应用性能的实用方法
  • 佛山三水区除甲醛公司深度对比测评|本地靠谱除甲醛机构怎么挑选不踩坑 - 专注室内空气检测治理
  • 2026在线一键去水印工具推荐:去视频和图片水印,这几个免安装方法很省心 - 软件小管家
  • 猫和老鼠 同花顺期货通指标
  • TinyVec高级特性:serde序列化、borsh支持与schemars集成全解析
  • 北方适合用的空气净化产品权威横评|六大品牌实测打分,选购避坑全攻略 - 互联网科技品牌测评
  • GEO 优化傻瓜指南
  • 佛山搬厂公司 机房数据设备服务器搬运收费标准、服务流程、正规服务商联系方式参考 - 厚道搬家
  • 如何实现SEO优化
  • 2026实测,去水印在线工具有哪些?分享几个一直在用的解析工具 - 免费软件工具方法教程
  • 前端监控系统构建指南:从指标采集到架构设计
  • 太原本地口碑银元回收
  • Node API for .NET架构解析:深入理解.NET与JavaScript互操作核心原理
  • 水库库容计算(Global Mapper)
  • 2026实测:微信小程序免费视频去水印哪个好用,靠谱无套路指南 - 办公小帮手
  • 2026探望老人的优质零食品牌盘点 行业选型标准详解 核心避坑FAQ与合规机构推荐 - 商业大观
  • 报销前查发票真伪如何做?2026年线上查验流程完整指南 - 跑政通
  • 皇族宠物家云杉纤维排毛冻干:天然排毛,守护爱宠肠道健康
  • 惠州搬家公司仓库整仓打包转仓收费标准:2026 分项报价、计费方式与避坑要点一览 - 厚道搬家
  • 2026年海关知识产权侵权国际货代律师推荐:专业团队选择指南 - 全域品牌推荐
  • 抖音去水印在线怎么操作?附2026无水印提取合规提醒与网站风险说明 - 办公小帮手
  • 主流双端面磨床厂商工况适配盘点
  • 2026 年现阶段,菏泽知名的高密度水泥压力板制造商选哪家,打破传统!这块板子如何颠覆你的工程效率? - 行业甄选官
  • 2026年7月青岛装修公司哪家靠谱|本土老牌装企焰火装饰领衔 半包全包报价一览 - 品牌优企推荐
  • K3 vs Qwen3.8
  • 2026在线去水印教程:图片视频免费去水印工具实测与避坑指南 - 办公小帮手