UI-TARS 源码解析 #14:parse_action_to_structure_output:从 Thought/Action 文本到动作字典

在前几篇文章中,我们已经分别分析了 UI-TARS 的几个关键点:

text 复制代码
prompt.py 如何定义 Thought + Action 输出格式;
parse_action 如何用 AST 解析函数调用式 Action;
point / start_box / end_box 如何统一;
Qwen2.5-VL 绝对坐标如何映射回真实屏幕;
smart_resize 为什么影响坐标还原。

这些内容其实都汇聚到 action_parser.py 里的一个核心函数:

python 复制代码
parse_action_to_structure_output(...)

如果说 parse_action 是"解析一个动作函数调用",那么 parse_action_to_structure_output 就是更上层的总控函数。

它负责把模型完整输出:

text 复制代码
Thought: 我需要点击搜索框。
Action: click(point='<point>850 120</point>')

转换成结构化动作字典:

json 复制代码
[
  {
    "reflection": null,
    "thought": "我需要点击搜索框。",
    "action_type": "click",
    "action_inputs": {
      "start_box": "[0.4427, 0.1111, 0.4427, 0.1111]"
    },
    "text": "Thought: 我需要点击搜索框。\nAction: click(start_box='(850,120)')"
  }
]

这篇文章就来完整拆解这个函数的处理流程。


一、parse_action_to_structure_output 的定位

先看 codes/README.md 对这个函数的说明。

官方文档中写到,parse_action_to_structure_output 的作用是把模型输出的 action instruction 解析成结构化字典,并自动处理坐标缩放以及 box/point 格式转换;它的返回值是一个 structured actions 列表,每个 dict 包含 action_typeaction_inputsthought 等字段。

这句话其实已经说明了它的三个职责:

text 复制代码
第一,解析 Thought / Action 文本;

第二,解析 Action 里的函数名和参数;

第三,处理坐标格式转换和缩放。

所以它不是普通的字符串解析函数。

它是 UI-TARS 从"模型文本输出"进入"程序动作字典"的关键入口。

完整链路可以理解为:

text 复制代码
模型输出文本
    ↓
parse_action_to_structure_output
    ↓
结构化 actions
    ↓
parsing_response_to_pyautogui_code
    ↓
pyautogui 自动化脚本

二、函数签名说明

源码中函数签名如下:

python 复制代码
def parse_action_to_structure_output(text,
                                     factor,
                                     origin_resized_height,
                                     origin_resized_width,
                                     model_type="qwen25vl",
                                     max_pixels=16384 * 28 * 28,
                                     min_pixels=100 * 28 * 28):

官方 README 中也列出了同样的 API 参数:text 是模型输出字符串,factor 是缩放因子,origin_resized_height / origin_resized_width 是原始图像高宽,model_type 用于区分模型坐标协议,max_pixels / min_pixels 用于图像像素上下限控制。

这些参数可以分成三类。

第一类是文本输入:

text 复制代码
text

也就是模型完整输出。

第二类是坐标缩放参数:

text 复制代码
factor
origin_resized_height
origin_resized_width
model_type

它们决定模型输出坐标如何归一化。

第三类是 Qwen2.5-VL 图像预处理参数:

text 复制代码
max_pixels
min_pixels

它们会影响 smart_resize 的结果,从而影响 Qwen2.5-VL 绝对坐标的还原。


三、第一步:清理文本

函数开头很简单:

python 复制代码
text = text.strip()

这一步去掉模型输出前后的空白字符。

看起来很普通,但对解析很重要。

因为后面会用:

python 复制代码
text.startswith("Thought:")

来判断文本结构。

如果模型输出前面多了换行或空格,strip() 可以先把它清理掉。

这也是处理大模型输出时常见的小技巧:

在正式解析之前,先做最基础的文本归一化。


四、第二步:把 <point>x y</point> 转成坐标

接下来源码判断:

python 复制代码
if "<point>" in text:
    text = convert_point_to_coordinates(text)

convert_point_to_coordinates 会匹配:

text 复制代码
<point>850 120</point>

并转换成:

text 复制代码
(850,120)

它还会去掉 [EOS]。源码中可以看到,该函数使用正则匹配 <point>(\d+)\s+(\d+)</point>,然后返回 (x,y) 格式。

例如模型原始输出是:

text 复制代码
Action: click(point='<point>850 120</point>')

这一步之后变成:

text 复制代码
Action: click(point='(850,120)')

这一步的意义是:

先把模型友好的 point 标签格式,转换成后续 parser 更容易处理的坐标字符串。


五、第三步:统一参数名

接下来是三个替换:

python 复制代码
if "start_point=" in text:
    text = text.replace("start_point=", "start_box=")

if "end_point=" in text:
    text = text.replace("end_point=", "end_box=")

if "point=" in text:
    text = text.replace("point=", "start_box=")

这一步非常关键。

模型可能输出:

text 复制代码
click(point='(850,120)')

drag(start_point='(300,500)', end_point='(700,500)')

但内部统一成:

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

drag(start_box='(300,500)', end_box='(700,500)')

源码中 parse_action_to_structure_output 确实会把 start_point 替换成 start_box,把 end_point 替换成 end_box,把 point 替换成 start_box

为什么要这样做?

因为后续执行层主要处理的是:

text 复制代码
start_box
end_box

而不是各种不同字段。

这就是前几篇文章讲过的设计:

text 复制代码
模型输出层可以比较友好;
Parser 内部必须统一稳定。

六、第四步:如果是 qwen25vl,先计算 smart_resize 尺寸

接下来源码判断:

python 复制代码
if model_type == "qwen25vl":
    smart_resize_height, smart_resize_width = smart_resize(
        origin_resized_height,
        origin_resized_width,
        factor=IMAGE_FACTOR,
        min_pixels=min_pixels,
        max_pixels=max_pixels)

这一步只在 model_type == "qwen25vl" 时执行。

原因是 Qwen2.5-VL 输出的是基于模型输入图像的绝对坐标,而模型输入图像可能经过 smart_resize。源码注释也写明:Qwen2.5-VL 输出 absolute coordinates,而 qwen2vl 输出 relative coordinates。

所以后面处理坐标时,需要知道:

text 复制代码
smart_resize_width
smart_resize_height

否则就无法把 Qwen2.5-VL 输出的绝对坐标归一化。

这一点也说明,parse_action_to_structure_output 不只是文本解析函数,它还承担模型坐标协议适配职责。


七、第五步:识别 Thought / Reflection / Action_Summary 格式

接下来开始提取思考内容。

源码根据文本开头选择不同正则:

python 复制代码
if text.startswith("Thought:"):
    thought_pattern = r"Thought: (.+?)(?=\s*Action: |$)"
    thought_hint = "Thought: "

elif text.startswith("Reflection:"):
    thought_pattern = r"Reflection: (.+?)Action_Summary: (.+?)(?=\s*Action: |$)"
    thought_hint = "Reflection: "

elif text.startswith("Action_Summary:"):
    thought_pattern = r"Action_Summary: (.+?)(?=\s*Action: |$)"
    thought_hint = "Action_Summary: "

else:
    thought_pattern = r"Thought: (.+?)(?=\s*Action: |$)"
    thought_hint = "Thought: "

从这里可以看出,UI-TARS 不只支持最普通的:

text 复制代码
Thought: ...
Action: ...

还支持:

text 复制代码
Reflection: ...
Action_Summary: ...
Action: ...

以及:

text 复制代码
Action_Summary: ...
Action: ...

源码中确实会根据文本是否以 Thought:Reflection:Action_Summary: 开头来选择不同解析模式。

这说明 UI-TARS 的 parser 兼容了更复杂的 System-2 输出格式。


八、Thought、Reflection、Action_Summary 分别表示什么?

虽然源码只是做正则解析,但从 Agent 语义上看,这三个字段可以这样理解。

Thought 表示当前一步的推理:

text 复制代码
我现在看到什么?
我下一步为什么要这么做?

Reflection 表示对之前动作的反思:

text 复制代码
刚才的动作有没有成功?
如果失败,失败原因可能是什么?

Action_Summary 表示接下来动作的摘要:

text 复制代码
下一步动作大概是什么?
目标元素是什么?

例如:

text 复制代码
Reflection: 刚才点击后页面没有变化,可能没有点中搜索框。
Action_Summary: 重新点击搜索框。
Action: click(point='<point>850 120</point>')

parse_action_to_structure_output 会把 ReflectionAction_Summary 拆出来,其中 Reflection 放入 reflection 字段,Action_Summary 作为 thought 字段保存。源码中如果正则匹配到两个 group,就会把第一个 group 存为 reflection,第二个 group 存为 thought

这让结构化动作不仅包含"做什么",还保留"为什么做"和"上一步反思"。


九、第六步:提取 Action 字符串

提取 Thought 后,源码会强制要求文本里包含:

python 复制代码
assert "Action:" in text

然后取最后一个 Action: 后面的内容:

python 复制代码
action_str = text.split("Action: ")[-1]

这一步说明:

parse_action_to_structure_output 来说,Action 是必须存在的。

没有 Action,就无法生成结构化动作。

而且这里使用的是:

text 复制代码
split("Action: ")[-1]

也就是取最后一个 Action: 后面的内容。

这对普通输出没问题:

text 复制代码
Thought: ...
Action: click(...)

但如果 Thought 里也包含了字符串 Action:,可能会产生误切分。

从产品化角度看,这里可以增强为更严格的正则匹配或行级解析。


十、第七步:支持多动作拆分

拿到 action_str 后,源码会执行:

python 复制代码
tmp_all_action = action_str.split(")\n\n")
all_action = []

也就是说,它尝试按:

text 复制代码
)\n\n

拆分多个动作。

例如模型可能输出:

text 复制代码
Action: click(start_box='(850,120)')

type(content='UI-TARS\n')

理论上可以被拆成多个 action。

然后每个 action 会进入循环:

python 复制代码
for action_str in tmp_all_action:
    ...
    if not action_str.strip().endswith(")"):
        action_str = action_str.strip() + ")"
    all_action.append(action_str)

如果拆分后动作末尾没有右括号,代码会补上。

源码中确实有这段多动作拆分和补括号逻辑。

不过从 GUI Agent 产品设计看,一般更推荐:

text 复制代码
一轮一个主要 Action;
执行后重新截图;
再决定下一步。

因为 GUI 状态会在每个动作后变化,多动作连续执行更容易因为页面变化而失败。


十一、第八步:特殊处理 type(content='...')

type 是最容易出解析问题的动作。

因为 content 可能包含:

text 复制代码
单引号
双引号
换行符
逗号
代码片段
路径
JSON

源码中如果发现:

python 复制代码
if "type(content" in action_str:

就会进入特殊分支。

主要做几件事:

text 复制代码
1. 如果末尾没有右括号,就补上;
2. 用正则匹配 type(content='...');
3. 提取 content;
4. 调用 escape_single_quotes 处理单引号;
5. 重新构造 type(content='...')。

源码中可以看到它使用 pattern = r"type\(content='(.*?)'\)" 匹配 type(content='...'),随后用 escape_single_quotes 处理内容,再重新拼成 type(content='...')

这个分支说明,文本输入动作比点击动作复杂得多。

点击动作只要处理坐标。

输入动作则要处理任意字符串。


十二、第九步:调用 parse_action 解析每个 Action

完成清洗和修正后,源码调用:

python 复制代码
parsed_actions = [
    parse_action(action.replace("\n", "\\n").lstrip())
    for action in all_action
]

这里有两个细节。

第一,action.replace("\n", "\\n")

这会把真实换行替换成转义形式,避免 AST 解析时被换行破坏。

第二,lstrip()

去掉动作前面的空白字符。

然后交给上一篇文章分析过的 parse_action

parse_action 会用 AST 把:

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

解析成:

json 复制代码
{
  "function": "click",
  "args": {
    "start_box": "(850,120)"
  }
}

源码中 parse_action 使用 ast.parse(action_str, mode='eval'),检查是否为函数调用,然后提取函数名和关键字参数。


十三、第十步:解析失败则抛出异常

接下来:

python 复制代码
for action_instance, raw_str in zip(parsed_actions, all_action):
    if action_instance == None:
        print(f"Action can't parse: {raw_str}")
        raise ValueError(f"Action can't parse: {raw_str}")

也就是说,只要某个 Action 解析失败,就会直接抛异常。

这说明 UI-TARS 对 Action 格式要求比较严格。

模型可以在 Thought 中自由表达,但 Action 必须是 parser 可以处理的函数调用格式。

这也是 Prompt 里要求模型按固定格式输出动作的原因。

如果模型输出:

text 复制代码
Action: click the search box

就不是合法函数调用,无法进入后续结构化处理。


十四、第十一步:生成 action_type 和 params

解析成功后,源码取出:

python 复制代码
action_type = action_instance["function"]
params = action_instance["args"]

例如:

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

会得到:

text 复制代码
action_type = "click"
params = {
    "start_box": "(850,120)"
}

再比如:

text 复制代码
scroll(start_box='(600,720)', direction='down')

会得到:

text 复制代码
action_type = "scroll"
params = {
    "start_box": "(600,720)",
    "direction": "down"
}

从这里开始,模型输出已经不再是纯文本,而开始进入结构化动作字典。


十五、第十二步:处理普通参数

源码中先初始化:

python 复制代码
action_inputs = {}

然后遍历参数:

python 复制代码
for param_name, param in params.items():
    if param == "": continue
    param = param.lstrip()
    action_inputs[param_name.strip()] = param

这一步会把普通参数直接放进 action_inputs

例如:

text 复制代码
hotkey(key='ctrl c')

会得到:

json 复制代码
{
  "key": "ctrl c"
}

再比如:

text 复制代码
type(content='UI-TARS\n')

会得到:

json 复制代码
{
  "content": "UI-TARS\\n"
}

对于不涉及坐标的动作,这一步基本就够了。

例如:

text 复制代码
hotkey
type
finished

它们主要依赖普通字符串参数。


十六、第十三步:识别 start_box / end_box 坐标参数

如果参数名中包含:

text 复制代码
start_box
end_box

源码会进入坐标处理逻辑:

python 复制代码
if "start_box" in param_name or "end_box" in param_name:
    ori_box = param
    numbers = ori_box.replace("(", "").replace(")", "").split(",")

这里会把:

text 复制代码
"(850,120)"

处理成:

text 复制代码
["850", "120"]

如果是四个数:

text 复制代码
"(100,200,300,400)"

就会处理成:

text 复制代码
["100", "200", "300", "400"]

这一步之后,才会进入坐标归一化。


十七、第十四步:按模型类型做坐标归一化

源码对坐标有两种处理方式。

1. qwen25vl 分支

如果:

text 复制代码
model_type == "qwen25vl"

则按 smart resize 后的尺寸归一化:

python 复制代码
if (num_idx + 1) % 2 == 0:
    float_numbers.append(float(num / smart_resize_height))
else:
    float_numbers.append(float(num / smart_resize_width))

也就是说:

text 复制代码
x / smart_resize_width
y / smart_resize_height

源码中这一段旁边的注释写明:Qwen2.5-VL 输出 absolute coordinates,而 qwen2vl 输出 relative coordinates。

2. 非 qwen25vl 分支

如果不是 Qwen2.5-VL,则:

python 复制代码
float_numbers = [float(num) / factor for num in numbers]

也就是直接除以 factor

例如 factor=1000 时:

text 复制代码
500 → 0.5
300 → 0.3

README 的 Quick Start 示例中,调用 parse_action_to_structure_output 时传入 factor=1000model_type="doubao",这正是非 qwen25vl 分支的典型用法。


十八、第十五步:二维点扩展成四维 box

如果归一化后只有两个数:

text 复制代码
[x, y]

源码会扩展成四个数:

python 复制代码
if len(float_numbers) == 2:
    float_numbers = [
        float_numbers[0], float_numbers[1],
        float_numbers[0], float_numbers[1]
    ]

也就是:

text 复制代码
[x, y] → [x, y, x, y]

源码中确实在坐标长度为 2 时做了这个扩展。

这一步的意义是统一结构。

无论模型输出的是点还是区域,最终都变成:

text 复制代码
[x1, y1, x2, y2]

后续 pyautogui 阶段只要取中心点即可:

text 复制代码
center_x = (x1 + x2) / 2
center_y = (y1 + y2) / 2

十九、第十六步:写入 action_inputs

坐标归一化完成后,源码会写回:

python 复制代码
action_inputs[param_name.strip()] = str(float_numbers)

例如:

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

factor=1000 的情况下,会变成:

json 复制代码
{
  "start_box": "[0.85, 0.12, 0.85, 0.12]"
}

如果是 Qwen2.5-VL,则会除以 smart resize 后的宽高。

最终 action_inputs 保存的不是原始坐标,而是归一化后的字符串形式 list。

这也是为什么后续 parsing_response_to_pyautogui_code 需要再把它解析出来,并乘以真实 image_width / image_height


二十、第十七步:组装最终 action dict

最后,源码把每个动作组装成:

python 复制代码
actions.append({
    "reflection": reflection,
    "thought": thought,
    "action_type": action_type,
    "action_inputs": action_inputs,
    "text": text
})

返回的是:

python 复制代码
return actions

也就是一个列表。

每个动作包含五个核心字段:

text 复制代码
reflection:
模型对前一步或当前状态的反思。

thought:
模型当前动作摘要或推理内容。

action_type:
动作类型,例如 click、type、scroll、drag。

action_inputs:
动作参数,例如 start_box、content、direction。

text:
清洗和转换后的完整模型文本。

源码中最终返回的 dict 字段正是 reflectionthoughtaction_typeaction_inputstext

这就是 UI-TARS 后续执行层真正消费的数据结构。


二十一、完整示例:click 动作

假设模型输出:

text 复制代码
Thought: 我需要点击搜索框。
Action: click(point='<point>850 120</point>')

调用:

python 复制代码
actions = parse_action_to_structure_output(
    text=response,
    factor=1000,
    origin_resized_height=1080,
    origin_resized_width=1920,
    model_type="doubao"
)

处理流程是:

text 复制代码
1. strip 清理文本;
2. <point>850 120</point> 转为 (850,120);
3. point= 替换成 start_box=;
4. 提取 Thought;
5. 提取 Action;
6. parse_action 解析 click 和 start_box;
7. 坐标除以 factor=1000;
8. [0.85, 0.12] 扩展成 [0.85, 0.12, 0.85, 0.12];
9. 组装 action dict。

最终结构大致是:

json 复制代码
[
  {
    "reflection": null,
    "thought": "我需要点击搜索框。",
    "action_type": "click",
    "action_inputs": {
      "start_box": "[0.85, 0.12, 0.85, 0.12]"
    },
    "text": "Thought: 我需要点击搜索框。\nAction: click(start_box='(850,120)')"
  }
]

这就是从模型文本到动作字典的完整转换。


二十二、完整示例:Reflection + Action_Summary

再看一个更复杂的输出:

text 复制代码
Reflection: 刚才点击后页面没有变化,可能没有点中搜索框。
Action_Summary: 重新点击搜索框。
Action: click(point='<point>850 120</point>')

解析后:

json 复制代码
[
  {
    "reflection": "刚才点击后页面没有变化,可能没有点中搜索框。",
    "thought": "重新点击搜索框。",
    "action_type": "click",
    "action_inputs": {
      "start_box": "[0.85, 0.12, 0.85, 0.12]"
    },
    "text": "..."
  }
]

这里可以看到:

text 复制代码
Reflection 被单独保存;
Action_Summary 被保存到 thought;
Action 被解析成结构化动作。

这让系统既能执行动作,也能保存模型的反思记录。


二十三、完整示例:drag 动作

假设模型输出:

text 复制代码
Thought: 我需要把滑块拖到右侧。
Action: drag(start_point='<point>300 500</point>', end_point='<point>700 500</point>')

处理后:

text 复制代码
start_point → start_box
end_point   → end_box

再解析:

json 复制代码
{
  "function": "drag",
  "args": {
    "start_box": "(300,500)",
    "end_box": "(700,500)"
  }
}

如果 factor=1000,最终:

json 复制代码
[
  {
    "reflection": null,
    "thought": "我需要把滑块拖到右侧。",
    "action_type": "drag",
    "action_inputs": {
      "start_box": "[0.3, 0.5, 0.3, 0.5]",
      "end_box": "[0.7, 0.5, 0.7, 0.5]"
    },
    "text": "..."
  }
]

后续 parsing_response_to_pyautogui_code 会把 start_boxend_box 还原成真实坐标,生成 pyautogui.moveTo(...)pyautogui.dragTo(...)。源码中 drag/select 分支正是读取两个 box,计算中心点后生成拖拽代码。


二十四、parse_action_to_structure_output 和 pyautogui 生成的关系

parse_action_to_structure_output 的输出不是最终执行代码,而是结构化中间层。

README 中的 Quick Start 也展示了这一点:先调用 parse_action_to_structure_output 得到 parsed_dict,再调用 parsing_response_to_pyautogui_code 生成 pyautogui 脚本。

也就是说:

text 复制代码
parse_action_to_structure_output:
模型文本 → 结构化动作

parsing_response_to_pyautogui_code:
结构化动作 → pyautogui 代码

这两个函数分别解决不同问题。

前者处理:

text 复制代码
文本格式
Thought / Reflection / Action 提取
函数调用解析
坐标归一化
动作字典生成

后者处理:

text 复制代码
真实坐标还原
鼠标点击
键盘快捷键
文本输入
拖拽滚动
任务结束

分层很清楚。


二十五、这个函数的核心价值

parse_action_to_structure_output 的核心价值可以总结成一句话:

把大模型不稳定的文本输出,收敛成稳定的动作字典。

模型输出天然有很多不稳定因素:

text 复制代码
可能使用 point,也可能使用 start_point;
可能有 Thought,也可能有 Reflection;
可能输出一个 Action,也可能输出多个 Action;
坐标可能是 Qwen2.5-VL 绝对坐标,也可能是 factor 相对坐标;
type 内容可能包含引号和换行。

而这个函数把这些差异统一成:

text 复制代码
[
  {
    "reflection": ...,
    "thought": ...,
    "action_type": ...,
    "action_inputs": ...,
    "text": ...
  }
]

这就是 GUI Agent 工程化最重要的一步。


二十六、源码里的几个潜在风险

虽然这个函数设计很实用,但如果要用于真实产品,还需要注意几个问题。

1. assert "Action:" in text 不适合生产环境

生产环境里最好不要用 assert 处理用户或模型输入。

更建议改成:

python 复制代码
if "Action:" not in text:
    raise ValueError("Missing Action field")

因为 Python 在优化模式下可能禁用 assert。

2. text.split("Action: ")[-1] 可能误切

如果 Thought 内容中出现 Action: 字样,可能导致误解析。

更稳的方式是按行查找最后一个真正的 Action 字段,或者用正则明确匹配。

3. 多动作拆分比较脆弱

当前用:

python 复制代码
action_str.split(")\n\n")

来拆多动作。

如果模型输出格式稍有变化,就可能拆错。

4. type(content='...') 处理不够通用

当前正则:

python 复制代码
r"type\(content='(.*?)'\)"

对复杂文本可能不够稳,比如内容中包含特殊引号、多行代码、括号等。

5. 坐标解析假设比较强

代码假设 box 字符串可以通过:

python 复制代码
replace("(", "").replace(")", "").split(",")

拆成数字。

如果模型输出空格分隔、括号不完整、带额外 token,就可能失败。


二十七、二次开发建议

如果你基于 UI-TARS 做自己的桌面自动化项目,可以在这个函数基础上增强几层。

第一,增加动作白名单:

python 复制代码
ALLOWED_ACTIONS = {
    "click", "left_double", "right_single",
    "drag", "hotkey", "type",
    "scroll", "wait", "finished"
}

第二,增加参数 schema:

python 复制代码
click 必须有 start_box
drag 必须有 start_box 和 end_box
scroll 必须有 direction
type 必须有 content

第三,增加坐标校验:

text 复制代码
长度必须是 2 或 4;
坐标必须是数字;
归一化坐标必须在 0 到 1 之间;
最终坐标不能超出屏幕。

第四,替换危险解析方式。

后续 pyautogui 代码生成里会用 eval(start_box) 还原 box;如果产品化,建议改成 ast.literal_eval 或自写安全解析器。源码中的 click、drag、scroll 等分支确实会通过 eval 解析 box 字符串。

第五,保存完整调试日志。

例如:

json 复制代码
{
  "raw_text": "...",
  "normalized_text": "...",
  "thought": "...",
  "reflection": "...",
  "action_type": "click",
  "raw_coordinates": "(850,120)",
  "normalized_coordinates": "[0.85,0.12,0.85,0.12]"
}

这样后续排查点击错误会方便很多。


总结

这篇文章我们完整拆解了 parse_action_to_structure_output

它的处理流程可以概括为:

text 复制代码
1. 清理模型输出文本;
2. 将 <point>x y</point> 转成 (x,y);
3. 将 point / start_point / end_point 统一成 start_box / end_box;
4. 如果是 qwen25vl,计算 smart_resize 后的宽高;
5. 提取 Thought / Reflection / Action_Summary;
6. 提取 Action 字符串;
7. 拆分多动作;
8. 特殊处理 type(content='...');
9. 调用 parse_action 用 AST 解析函数调用;
10. 提取 action_type 和参数;
11. 对 start_box / end_box 做坐标归一化;
12. 将二维点扩展成四维 box;
13. 返回结构化 actions 列表。

从 UI-TARS 的整体架构看,这个函数位于非常关键的位置:

text 复制代码
prompt.py
约束模型输出 Thought + Action

parse_action_to_structure_output
把模型文本转成结构化 action dict

parsing_response_to_pyautogui_code
把 action dict 转成 pyautogui 执行代码

所以,parse_action_to_structure_output 是 UI-TARS 从"模型会说动作"走向"程序能执行动作"的关键桥梁。

相关推荐
_ZHOURUI_H_11 小时前
Unity MyFramework 用法说明(二十五):使用 AtlasManager 统一管理图集与 Sprite 引用
ui·unity·游戏引擎·unity3d·游戏开发
不如摸鱼去12 小时前
Wot UI 2.3.0 发布:二维码组件来了,Open Wot 与 wot-starter 同步更新
前端·ui·微信小程序·前端框架·uni-app
测试开发技术2 天前
AI 测试提效 | 告别手工写脚本,分享我的 Playwright + Skill 批量生成 UI 自动化脚本方案
自动化测试·人工智能·ui·自动化·agent·skill·ai测试
吾儿良辰2 天前
Flutter 迎来重大变革:Material 设计系统拆分,迈向更开放的 UI 框架
javascript·flutter·ui
兰亭妙微UI设计公司3 天前
兰亭妙微精益 UX 设计实战指南:以轻量化迭代消除设计资源浪费
ui
打妖妖灵滴哪吒3 天前
IOS模态窗口的作用与设计原则
前端·ui
打妖妖灵滴哪吒3 天前
前端开发/UI设计师-APP中如何进行可发现性设计
前端·ui
玛艾露贝3 天前
Browser-Use在UI自动化测试中的应用
ui
brave_zhao3 天前
Axure RP 9模板的使用
ui·axure·photoshop