UI-TARS 源码解析 #21:从 UI-TARS 到本地桌面自动化:如何接入截图、模型推理和动作执行?

前面二十篇文章,我们基本把 UI-TARS 主仓库里的核心代码拆了一遍。

主要包括:

text 复制代码
prompt.py
action_parser.py
README_deploy.md
README_coordinates.md
action_parser_test.py
inference_test.py

我们已经知道,UI-TARS 的官方 ui-tars 包主要负责把视觉语言模型生成的 GUI 动作指令解析成 pyautogui 脚本,同时支持坐标转换和 smart image resizing。官方 README 也明确给出了 parse_action_to_structure_outputparsing_response_to_pyautogui_code 的快速使用示例。

但如果要真正做一个本地桌面自动化 Agent,仅仅会调用这两个函数还不够。

因为真实运行时还需要解决:

text 复制代码
如何截图?
如何把截图发给模型?
如何组织 Prompt?
如何接收 Thought + Action?
如何解析 Action?
如何执行鼠标键盘?
如何判断执行是否成功?
如何防止误点、误删、误提交?

这一篇我们把前面分析过的代码串起来,设计一个最小可运行的本地桌面自动化架构。


一、UI-TARS 主仓库提供了什么?

先明确一点:UI-TARS 主仓库并不是一个完整的桌面自动化 App。

它更像是给出了几类关键组件:

text 复制代码
1. Prompt 模板;
2. 模型部署和调用示例;
3. Action Parser;
4. 坐标转换逻辑;
5. pyautogui 代码生成;
6. 坐标可视化示例;
7. 基础测试文件。

官方 README 中也提到,主仓库提供 UI-TARS-desktop 的入口,如果要在本地个人设备上操作,可以参考对应桌面版本;而主仓库本身的 Quick Start 则把流程拆成 Deployment & Inference 与 Post Processing 两步。

所以,我们基于 UI-TARS 做本地自动化时,要自己补上完整 Agent Loop:

text 复制代码
截图
  ↓
模型推理
  ↓
动作解析
  ↓
安全检查
  ↓
本地执行
  ↓
重新截图
  ↓
下一轮

二、最小桌面 Agent 架构

一个最小可运行的 UI-TARS 本地桌面 Agent,可以拆成七个模块:

text 复制代码
Local UI-TARS Agent
├── Screenshot 模块
│   └── 获取当前屏幕或指定窗口截图
│
├── Prompt 模块
│   └── 组装 COMPUTER_USE Prompt、用户任务、历史动作、截图
│
├── Model Client 模块
│   └── 调用 UI-TARS-1.5-7B Endpoint 或本地模型服务
│
├── Parser 模块
│   └── parse_action_to_structure_output
│
├── Executor Codegen 模块
│   └── parsing_response_to_pyautogui_code
│
├── Safety 模块
│   └── 校验动作、坐标、风险操作
│
└── Feedback Loop 模块
    └── 执行后重新截图,进入下一轮

对应的数据流是:

text 复制代码
用户任务
  ↓
截图
  ↓
Prompt + Screenshot
  ↓
UI-TARS 模型
  ↓
Thought + Action
  ↓
结构化 action dict
  ↓
pyautogui 代码
  ↓
本地鼠标键盘执行
  ↓
新截图

这就是 GUI Agent 的最小闭环。


三、第一步:截图模块

本地桌面自动化的第一步一定是截图。

因为 UI-TARS 这类 GUI Agent 的输入不是 DOM、不是 API,也不是控件树,而是当前屏幕图像。

截图模块的职责是:

text 复制代码
1. 获取当前屏幕截图;
2. 保存截图文件;
3. 记录截图宽高;
4. 可选:只截取当前窗口;
5. 可选:处理 DPI 和缩放问题。

一个最小 Python 伪代码可以这样写:

python 复制代码
import pyautogui
from PIL import Image

def capture_screen(path="current_screen.png"):
    image = pyautogui.screenshot()
    image.save(path)

    width, height = image.size
    return {
        "path": path,
        "width": width,
        "height": height
    }

这里最重要的是返回:

text 复制代码
width
height

因为后面坐标还原一定会用到。

前面分析过,parsing_response_to_pyautogui_code 需要 image_heightimage_width,用来把归一化坐标还原成真实 pyautogui 坐标。官方 README 的示例也是先设置原始图像宽高,再调用动作解析和 pyautogui 代码生成。


四、第二步:Prompt 模块

截图只是图像,还需要给模型任务说明和动作协议。

UI-TARS 的 prompt.py 中定义了 COMPUTER_USE_DOUBAO,它要求模型输出:

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

并定义了桌面端 Action Space:

text 复制代码
click
left_double
right_single
drag
hotkey
type
scroll
wait
finished

其中 hotkey 要求按键用空格分隔,type 要求使用 \'\"\n 等转义字符,scroll 支持方向参数。

Prompt 模块的职责就是把用户任务填进去:

python 复制代码
from ui_tars.prompt import COMPUTER_USE_DOUBAO

def build_prompt(user_instruction, language="Chinese"):
    return COMPUTER_USE_DOUBAO.format(
        instruction=user_instruction,
        language=language
    )

例如用户任务是:

text 复制代码
打开浏览器,搜索 UI-TARS GitHub 仓库。

Prompt 会告诉模型:

text 复制代码
你是一个 GUI agent;
你会看到截图;
你需要根据任务和历史动作输出下一步;
输出格式必须是 Thought + Action;
Action 必须从动作空间中选择。

这一步非常关键。

因为模型输出格式是否稳定,直接决定后面 parser 能不能解析。


五、第三步:模型推理模块

模型推理模块负责把:

text 复制代码
Prompt
+
Screenshot
+
History

发送给 UI-TARS 模型服务。

如果使用官方 HuggingFace Endpoint 部署方式,官方 README_deploy.md 提供了基于 OpenAI SDK 的调用示例:创建 OpenAI client,设置 HuggingFace Endpoint 的 base_urlapi_key,然后调用 client.chat.completions.create(...)。示例中还使用了 temperature=0.0max_tokens=400stream=True

伪代码可以这样理解:

python 复制代码
from openai import OpenAI

class UITarsModelClient:
    def __init__(self, base_url, api_key):
        self.client = OpenAI(
            base_url=base_url,
            api_key=api_key
        )

    def infer(self, messages):
        stream = self.client.chat.completions.create(
            model="tgi",
            messages=messages,
            temperature=0.0,
            max_tokens=400,
            stream=True
        )

        response = ""
        for chunk in stream:
            delta = chunk.choices[0].delta.content
            if delta:
                response += delta

        return response

模型返回结果通常类似:

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

官方部署文档里的 Expected Output 也是这种 Thought + Action 格式。


六、第四步:Parser 模块

拿到模型输出后,下一步就是解析。

UI-TARS 官方 README 的 Post Processing 示例中,先调用:

python 复制代码
parse_action_to_structure_output(...)

再调用:

python 复制代码
parsing_response_to_pyautogui_code(...)

官方说明 parse_action_to_structure_output 会把模型动作解析成结构化字典,并自动处理坐标缩放和 box/point 格式转换。

Parser 模块可以这样封装:

python 复制代码
from ui_tars.action_parser import parse_action_to_structure_output

def parse_model_response(response, image_width, image_height, model_type="qwen25vl"):
    actions = parse_action_to_structure_output(
        response,
        factor=1000,
        origin_resized_height=image_height,
        origin_resized_width=image_width,
        model_type=model_type
    )
    return actions

输入:

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": "..."
  }
]

源码中 parse_action_to_structure_output 会先处理 <point>,再把 start_pointend_pointpoint 统一替换成 start_box / end_box,随后提取 Thought、Action,并对坐标做归一化。


七、第五步:坐标适配模块

如果使用的是 Qwen2.5-VL based model,坐标处理尤其重要。

UI-TARS 主 README 提醒,Qwen2.5-VL based models 使用 absolute coordinates 做 grounding,需要参考坐标处理指南。

README_coordinates.md 给出的核心思路是:

text 复制代码
模型输出坐标
  ↓
除以 smart_resize 后的宽高
  ↓
乘以原图宽高
  ↓
得到原图坐标

文档示例中,模型输出 click(start_box='(197,525)'),然后通过 smart_resize(height, width) 得到新尺寸,再用 model_output_width / new_width * widthmodel_output_height / new_height * height 计算真实坐标。

在实际项目中,最容易出错的就是这里:

text 复制代码
模型看的是 resize 后的图;
pyautogui 点的是真实屏幕;
中间必须把坐标系对齐。

所以,你要保证:

text 复制代码
1. 截图宽高记录正确;
2. model_type 设置正确;
3. image_width / image_height 传给执行层时没有写反;
4. Windows DPI、窗口偏移、多显示器问题另行处理。

八、第六步:生成 pyautogui 代码

结构化 action dict 还不能直接控制电脑。

它需要转换成 pyautogui 代码。

官方 ui-tars 包提供:

python 复制代码
parsing_response_to_pyautogui_code(...)

源码中,这个函数会先生成 import pyautoguiimport time,然后根据 action_type 分支生成不同代码:hotkeypyautogui.hotkey(...)type 默认走 pyperclip.copy(...)ctrl+vdrag/selectmoveTo + dragToscrollpyautogui.scroll(...)click/left_double/right_single/hover 则从 start_box 还原坐标后生成鼠标操作。

封装函数可以这样写:

python 复制代码
from ui_tars.action_parser import parsing_response_to_pyautogui_code

def build_pyautogui_code(actions, image_width, image_height):
    return parsing_response_to_pyautogui_code(
        responses=actions,
        image_height=image_height,
        image_width=image_width,
        input_swap=True
    )

例如结构化动作:

json 复制代码
{
  "action_type": "click",
  "action_inputs": {
    "start_box": "[0.5, 0.2, 0.5, 0.2]"
  }
}

会生成类似:

python 复制代码
pyautogui.click(960, 216, button='left')

如果图像尺寸是 1920×1080。


九、第七步:不要马上执行,先做安全检查

这里非常重要。

模型输出的动作不能直接执行。

因为 UI-TARS 这类 GUI Agent 真的会控制鼠标键盘,风险比普通聊天机器人高很多。

官方 README 的 Limitations 也提到,UI-TARS-1.5 可能出现误识别 GUI 元素、幻觉或采取次优动作,并且 GUI 自动化能力也可能被滥用。

所以本地 Agent 必须加入 Safety 模块。

至少检查:

text 复制代码
1. action_type 是否在白名单内;
2. 坐标是否越界;
3. 是否点击高风险区域;
4. 是否涉及删除、发送、付款、提交;
5. 是否输入敏感信息;
6. 是否连续失败;
7. 是否超过最大步数。

示例:

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

RISKY_ACTIONS = {
    "right_single",
    "hotkey",
    "type"
}

def validate_actions(actions):
    for action in actions:
        action_type = action.get("action_type")

        if action_type not in ALLOWED_ACTIONS:
            raise ValueError(f"Unsupported action type: {action_type}")

        if action_type in RISKY_ACTIONS:
            print(f"Risky action detected: {action_type}")

    return True

真实产品里,高风险动作应该弹出确认框,而不是自动执行。


十、执行层:执行代码还是直接调用?

UI-TARS 当前函数返回的是 pyautogui 代码字符串。

这有利于调试和审计,但真正执行时有两种方式。

第一种:执行生成的代码。

python 复制代码
def execute_code(code):
    exec(code)

这种方式简单,但风险高。

第二种:自己写安全执行器。

不要执行字符串,而是根据结构化 action dict 调用 pyautogui。

例如:

python 复制代码
def execute_action(action):
    action_type = action["action_type"]
    inputs = action["action_inputs"]

    if action_type == "click":
        # safe_parse_box + coordinate restore + pyautogui.click
        pass

    elif action_type == "type":
        # safe clipboard paste
        pass

如果是学习源码或 Demo,可以先打印 pyautogui 代码。

如果是产品化,我更建议:

text 复制代码
使用 UI-TARS 的 parser;
参考它的 pyautogui 生成逻辑;
但执行层自己写安全版本。

因为源码中 click、drag、scroll 等分支使用了 eval(start_box) / eval(end_box) 解析坐标字符串。这个写法在研究代码中方便,但模型输出属于不可信输入,产品化时最好换成 ast.literal_eval 或自定义安全解析。


十一、Feedback Loop:执行后必须重新截图

GUI Agent 和普通脚本最大的区别是:

每一步执行后,界面状态都会变化。

所以不能一次让模型输出十步,然后全部执行。

更稳的方式是:

text 复制代码
截图
  ↓
模型判断下一步
  ↓
执行一步
  ↓
重新截图
  ↓
继续判断

例如搜索任务:

text 复制代码
第 1 轮:
模型看到浏览器首页 → 点击搜索框

第 2 轮:
模型看到搜索框获得焦点 → 输入关键词

第 3 轮:
模型看到搜索结果页 → 点击目标链接

第 4 轮:
模型看到目标页面 → finished

这就是 Agent Loop。

伪代码如下:

python 复制代码
def run_agent(task, max_steps=15):
    history = []

    for step in range(max_steps):
        screen = capture_screen(f"step_{step}.png")
        prompt = build_prompt(task)

        messages = build_messages(
            prompt=prompt,
            screenshot_path=screen["path"],
            history=history
        )

        response = model_client.infer(messages)

        actions = parse_model_response(
            response=response,
            image_width=screen["width"],
            image_height=screen["height"],
            model_type="qwen25vl"
        )

        validate_actions(actions)

        code = build_pyautogui_code(
            actions=actions,
            image_width=screen["width"],
            image_height=screen["height"]
        )

        print(code)

        if "DONE" in code:
            return "finished"

        execute_code(code)

        history.append({
            "step": step,
            "screenshot": screen["path"],
            "response": response,
            "actions": actions,
            "code": code
        })

    return "max_steps_reached"

这个结构就是最小本地桌面 Agent。


十二、History 应该记录什么?

Prompt 里通常需要带上历史动作。

否则模型不知道前面做过什么。

建议每一步记录:

json 复制代码
{
  "step": 3,
  "user_task": "打开浏览器搜索 UI-TARS",
  "screenshot": "step_003.png",
  "model_response": "Thought: ... Action: ...",
  "parsed_actions": [
    {
      "action_type": "click",
      "action_inputs": {
        "start_box": "[0.5,0.2,0.5,0.2]"
      }
    }
  ],
  "pyautogui_code": "pyautogui.click(960,216,button='left')",
  "execution_result": "success"
}

这些日志有两个价值。

第一,给模型做上下文。

第二,方便失败排查。

例如点错时,你可以看到:

text 复制代码
模型输出坐标是多少?
归一化坐标是多少?
还原后的真实坐标是多少?
执行前截图是什么?
执行后截图是什么?

GUI Agent 没有日志,基本无法调试。


十三、最小项目目录结构

如果要自己落地,可以设计成这样的项目结构:

text 复制代码
ui_tars_local_agent/
├── main.py
├── config.py
├── requirements.txt
│
├── agent/
│   ├── loop.py
│   ├── prompt_builder.py
│   ├── model_client.py
│   ├── parser.py
│   ├── executor.py
│   ├── safety.py
│   └── logger.py
│
├── runtime/
│   ├── screenshots/
│   ├── logs/
│   └── traces/
│
└── tests/
    ├── test_parser.py
    ├── test_executor.py
    └── test_coordinate.py

各模块职责:

text 复制代码
prompt_builder.py:
负责组装 UI-TARS Prompt。

model_client.py:
负责调用 HuggingFace Endpoint 或本地模型服务。

parser.py:
封装 parse_action_to_structure_output。

executor.py:
封装 pyautogui 执行。

safety.py:
动作白名单、坐标校验、高风险确认。

logger.py:
保存截图、模型输出、动作、执行代码、结果。

这样拆开后,后续要替换模型、替换执行器、增加安全策略都会更容易。


十四、不要忽略 Windows DPI 和窗口偏移

如果你做的是 Windows 桌面自动化,这里是非常容易踩坑的地方。

UI-TARS 的后处理代码主要解决的是:

text 复制代码
模型坐标
  ↓
截图坐标

但真实 pyautogui 点击的是:

text 复制代码
屏幕坐标

中间可能还有:

text 复制代码
窗口左上角偏移
标题栏高度
系统 DPI 缩放
多显示器坐标
远程桌面缩放
浏览器缩放

例如你只截取了某个窗口:

text 复制代码
截图坐标:850,120
窗口左上角:100,50
真实屏幕坐标:950,170

这一步 UI-TARS 主仓库不会替你完整处理。

所以本地桌面 Agent 要额外维护:

text 复制代码
screenshot_region = {
  "left": 100,
  "top": 50,
  "width": 1280,
  "height": 720
}

执行时要把截图坐标转换成屏幕坐标。


十五、建议先做"半自动模式"

刚开始不要直接做全自动。

建议先做半自动:

text 复制代码
1. 截图;
2. 调用模型;
3. 打印 Thought + Action;
4. 画出模型点击点;
5. 展示即将执行的 pyautogui 代码;
6. 用户确认;
7. 再执行。

这样你可以快速判断:

text 复制代码
模型是否理解界面;
坐标是否正确;
Action 是否可解析;
pyautogui 代码是否合理;
是否存在危险动作。

等到链路稳定后,再逐步开放自动执行。


十六、安全策略必须前置

本地桌面自动化不同于网页爬虫。

它可能真的点击:

text 复制代码
删除
发送
支付
确认
提交
关闭
卸载
覆盖文件

所以必须限制。

建议安全策略:

text 复制代码
1. 默认不允许操作密码框、验证码、支付页面;
2. 删除、提交、发送、付款动作必须用户确认;
3. 连续失败 3 次自动停止;
4. 单任务最大步数限制;
5. 鼠标坐标越界立即停止;
6. Action 解析失败立即停止;
7. 模型输出未知动作立即停止;
8. 保留完整日志。

UI-TARS 的源码给了执行链路,但安全策略需要你自己补。


十七、从 UI-TARS 到自己的自动化产品

如果你正在做自己的桌面自动化产品,可以把 UI-TARS 当成三层能力来借鉴。

第一层:模型层。

text 复制代码
根据截图和任务,输出 Thought + Action。

第二层:协议层。

text 复制代码
Action 必须是 click/type/hotkey/drag/scroll/finished 这类结构化动作。

第三层:执行层。

text 复制代码
把结构化动作转换成鼠标键盘操作。

你不一定要完全照搬官方代码。

但可以借鉴它的核心思路:

text 复制代码
不要让模型直接生成任意 Python 代码;
让模型输出受控 Action;
Parser 负责结构化;
Executor 负责执行;
Safety 负责拦截风险;
Loop 负责截图反馈。

这比"让大模型直接写 pyautogui 脚本并执行"安全得多。


十八、最小可运行版本的 MVP 功能

如果要做一个 MVP,我建议先只支持这些动作:

text 复制代码
click
type
hotkey
scroll
finished

先不要急着支持:

text 复制代码
drag
right_single
left_double
hover
多窗口控制
文件拖拽
复杂表格操作

MVP 任务可以选择:

text 复制代码
打开网页;
搜索关键词;
填写简单表单;
点击普通按钮;
滚动页面;
复制粘贴文本。

这些动作足够验证整个链路:

text 复制代码
截图 → 模型 → Action → Parser → pyautogui → 反馈

等稳定后,再加 drag、窗口坐标、DPI 适配、失败重试、人工确认等功能。


总结

这篇文章我们把 UI-TARS 从源码解析推进到了本地桌面自动化架构设计。

一个最小本地 Agent 至少需要:

text 复制代码
Screenshot 模块:
获取当前屏幕图像和尺寸。

Prompt 模块:
使用 COMPUTER_USE Prompt 约束模型输出 Thought + Action。

Model Client 模块:
调用 UI-TARS-1.5-7B Endpoint 或本地模型服务。

Parser 模块:
调用 parse_action_to_structure_output,把模型输出转成结构化 action。

Codegen / Executor 模块:
调用或参考 parsing_response_to_pyautogui_code,把 action 变成鼠标键盘操作。

Safety 模块:
动作白名单、坐标校验、高风险确认。

Feedback Loop 模块:
执行后重新截图,继续下一轮。

完整链路是:

text 复制代码
用户任务
  ↓
截图
  ↓
Prompt + Screenshot
  ↓
UI-TARS 模型
  ↓
Thought + Action
  ↓
parse_action_to_structure_output
  ↓
结构化 action dict
  ↓
安全检查
  ↓
parsing_response_to_pyautogui_code
  ↓
pyautogui 执行
  ↓
重新截图

从工程角度看,UI-TARS 最值得借鉴的不是某一行 pyautogui 代码,而是它的分层设计:

text 复制代码
模型负责判断下一步;
Prompt 约束输出协议;
Parser 统一动作格式;
坐标模块处理缩放;
Executor 执行鼠标键盘;
Safety 控制风险;
Loop 负责持续反馈。

这套架构跑通以后,就可以从一个简单的"AI 点击器",逐步升级成真正能完成多步骤任务的本地桌面自动化 Agent。

相关推荐
lys079620002 小时前
多台亚马逊云服务器,在同一个网段的办法
运维·服务器
2601_963282773 小时前
极寒场景专网通信技术实践:黑龙江零下 40℃环境数字对讲组网方案落地解析
大数据·运维·数据库
淼澄研学3 小时前
OpenAI集成Photoshop API:函数调用机制解析与实操指南
ui·photoshop
学习使我健康3 小时前
一个基于 **Jetpack Compose + Material 3** 的 Android 入门示例项目,演示 Compose 声明式 UI 的核心用法
ui·kotlin·android jetpack
沫璃染墨3 小时前
《从零入门Linux系统篇(十四):系统工具篇·五——Git版本控制:从版本管理到协同开发》
linux·运维·服务器·git·gitee·github
元岳数字人小元5 小时前
数字人开源技术优势解析,助力智能交互普及落地
运维·人工智能·开源·人机交互·交互
一夜白头催人泪5 小时前
从0到1搭建AI驱动的UI自动化:无代码DOM设计文档
人工智能·ui·自动化
JXJD20046 小时前
AI 推理服务器高速连接器线束自动化设备定制指南 全场景互连非标整线方案参考
服务器·人工智能·自动化
DeepVisionary6 小时前
再观察小星火:当大厂分成“冰封“,国产轻模式创作者平台如何跑出加速度
python·自动化
王琦03186 小时前
Linux的管理程序
linux·运维·服务器