前面二十篇文章,我们基本把 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_output 和 parsing_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_height 和 image_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_url 和 api_key,然后调用 client.chat.completions.create(...)。示例中还使用了 temperature=0.0、max_tokens=400 和 stream=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_point、end_point、point 统一替换成 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 * width 和 model_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 pyautogui 和 import time,然后根据 action_type 分支生成不同代码:hotkey 转 pyautogui.hotkey(...),type 默认走 pyperclip.copy(...) 加 ctrl+v,drag/select 转 moveTo + dragTo,scroll 转 pyautogui.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。