前面二十一篇文章,我们已经把 UI-TARS 主仓库里的核心内容基本拆完了。
包括:
text
prompt.py:如何约束模型输出 Thought + Action
action_parser.py:如何解析模型动作、统一坐标、生成 pyautogui 代码
README_deploy.md:如何通过 HuggingFace Endpoint 调用 UI-TARS-1.5-7B
README_coordinates.md:如何处理 Qwen2.5-VL 坐标映射
action_parser_test.py / inference_test.py:如何验证动作解析和坐标处理
这一篇不再单独讲某个函数,而是做一次二次开发实战。
目标是:
基于 UI-TARS 的设计思路,做一个简单的 Windows 自动操作 Agent。
它不追求完整产品化,也不一上来支持所有复杂任务,而是先跑通最小闭环:
text
截图
↓
调用模型
↓
得到 Thought + Action
↓
解析动作
↓
安全检查
↓
执行鼠标键盘
↓
重新截图
这就是一个 Windows GUI Agent 的最小原型。
一、先明确:我们要做的不是完整 UI-TARS-desktop
UI-TARS 主仓库并不是完整桌面客户端,而是提供了模型部署、Prompt、动作解析、坐标转换和 pyautogui 代码生成等基础能力。官方 Quick Start 也把流程分为模型部署与推理、Post Processing 两步:先获取模型输出,再用 parse_action_to_structure_output 和 parsing_response_to_pyautogui_code 做动作后处理。
所以本文的目标不是复刻完整 UI-TARS-desktop,而是用 UI-TARS 的开源组件和设计模式,搭一个最小 Windows Agent。
这个 Agent 先支持几类基础动作:
text
click
type
hotkey
scroll
finished
暂时不急着支持:
text
drag
right_single
left_double
hover
多窗口切换
复杂文件拖拽
系统级权限操作
这样可以先把核心链路跑通,再逐步扩展。
二、最小 Windows Agent 的整体架构
一个最小 Windows 自动操作 Agent,可以拆成 7 个模块:
text
windows_ui_agent/
├── main.py
├── config.py
├── requirements.txt
│
├── agent/
│ ├── screenshot.py
│ ├── prompt_builder.py
│ ├── model_client.py
│ ├── parser.py
│ ├── safety.py
│ ├── executor.py
│ └── loop.py
│
└── runtime/
├── screenshots/
├── logs/
└── traces/
模块职责如下:
text
screenshot.py:
负责截取当前屏幕或当前窗口。
prompt_builder.py:
负责组装 UI-TARS Prompt、用户任务和历史动作。
model_client.py:
负责调用 UI-TARS-1.5-7B Endpoint 或其他兼容模型服务。
parser.py:
负责调用 parse_action_to_structure_output,把模型输出转成结构化 action。
safety.py:
负责动作白名单、坐标边界检查、高风险动作拦截。
executor.py:
负责执行 click、type、hotkey、scroll、finished。
loop.py:
负责截图 → 推理 → 解析 → 执行 → 再截图的循环。
这套架构的核心思想是:
不要让模型直接控制电脑,而是让模型输出受控动作,再由本地程序解析、检查和执行。
这和 UI-TARS 的设计一致:ui-tars 包负责把 VLM 生成的 GUI action instructions 解析成 pyautogui 脚本,并支持坐标转换和 smart image resizing。
三、安装依赖
先建一个 Python 虚拟环境。
bash
python -m venv .venv
.venv\Scripts\activate
安装依赖:
bash
pip install ui-tars pyautogui pillow openai pyperclip
如果你要使用窗口识别、前台窗口截图,可以后续再加:
bash
pip install pywin32 pygetwindow
最小版本先只用全屏截图,这样更容易跑通。
ui-tars 官方 README 中也给出了安装方式:pip install ui-tars 或 uv pip install ui-tars。
四、第一步:截图模块
新建 agent/screenshot.py:
python
import os
import time
from pathlib import Path
import pyautogui
def capture_screen(output_dir: str = "runtime/screenshots") -> dict:
"""
截取当前整个屏幕。
返回截图路径、宽度、高度。
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
ts = time.strftime("%Y%m%d_%H%M%S")
path = os.path.join(output_dir, f"screen_{ts}.png")
image = pyautogui.screenshot()
image.save(path)
width, height = image.size
return {
"path": path,
"width": width,
"height": height,
}
这个函数有三个关键输出:
text
path:截图文件路径
width:截图宽度
height:截图高度
其中 width 和 height 非常重要。
因为后面 parse_action_to_structure_output 和 parsing_response_to_pyautogui_code 都需要知道图片尺寸。官方 Post Processing 示例中,解析模型输出时需要传入原图宽高,生成 pyautogui 代码时也要传入 image_height 和 image_width。
五、第二步:Prompt 模块
UI-TARS 的 prompt.py 中定义了桌面端 Prompt。
它要求模型输出:
text
Thought: ...
Action: ...
并定义桌面动作空间:
text
click(point='<point>x1 y1</point>')
left_double(point='<point>x1 y1</point>')
right_single(point='<point>x1 y1</point>')
drag(start_point='<point>x1 y1</point>', end_point='<point>x2 y2</point>')
hotkey(key='ctrl c')
type(content='xxx')
scroll(point='<point>x1 y1</point>', direction='down or up or right or left')
wait()
finished(content='xxx')
这些内容都在 COMPUTER_USE_DOUBAO 模板里。
新建 agent/prompt_builder.py:
python
from ui_tars.prompt import COMPUTER_USE_DOUBAO
def build_system_prompt(task: str, language: str = "Chinese") -> str:
"""
使用 UI-TARS 的 COMPUTER_USE Prompt。
"""
return COMPUTER_USE_DOUBAO.format(
instruction=task,
language=language
)
def build_history_text(history: list[dict], max_items: int = 5) -> str:
"""
把最近几步动作转成简短历史文本。
"""
if not history:
return ""
recent = history[-max_items:]
lines = ["\n## Action History"]
for item in recent:
lines.append(f"Step {item['step']}:")
lines.append(item.get("response", "").strip())
lines.append("")
return "\n".join(lines)
实际调用模型时,可以把:
text
任务说明
+
历史动作
+
当前截图
一起发给模型。
六、第三步:把截图转成 base64
如果调用 HuggingFace Endpoint 或 OpenAI 兼容多模态接口,通常需要把截图转成 base64。
新建一个工具函数:
python
import base64
def image_to_base64(path: str) -> str:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
请求中使用:
text
data:image/png;base64,<base64内容>
官方部署文档的 Python 测试代码也导入了 base64、PIL.Image、OpenAI 等,用 OpenAI SDK 调用 HuggingFace Endpoint,并通过流式响应拼接模型输出。
七、第四步:模型调用模块
新建 agent/model_client.py。
这里以 HuggingFace Endpoint 的 OpenAI 兼容接口为例:
python
from openai import OpenAI
class UITarsModelClient:
def __init__(self, base_url: str, api_key: str, model: str = "tgi"):
self.client = OpenAI(
base_url=base_url,
api_key=api_key,
)
self.model = model
def infer(self, messages: list[dict]) -> str:
"""
调用 UI-TARS Endpoint,返回 Thought + Action 文本。
"""
stream = self.client.chat.completions.create(
model=self.model,
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
为什么 temperature=0.0?
因为 GUI Agent 不是写作文,而是要输出稳定、可解析、可执行的动作。官方部署示例中也是使用 temperature=0.0、max_tokens=400、stream=True 调用模型。
八、第五步:组装 messages
新建 agent/messages.py:
python
from agent.prompt_builder import build_system_prompt, build_history_text
from agent.utils import image_to_base64
def build_messages(task: str, screenshot_path: str, history: list[dict]) -> list[dict]:
prompt = build_system_prompt(task)
history_text = build_history_text(history)
image_b64 = image_to_base64(screenshot_path)
text = prompt
if history_text:
text += "\n" + history_text
return [
{
"role": "user",
"content": [
{
"type": "text",
"text": text,
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_b64}"
},
},
],
}
]
这里要注意:不同 Endpoint 的消息格式可能有差异。
如果你使用的是官方 HuggingFace Endpoint 示例,可以参考它的 test_messages.json 结构;如果用其他多模态模型服务,需要按对应 API 文档调整。
但整体思想不变:
text
Prompt 协议
+
当前截图
+
历史动作
九、第六步:Parser 模块
新建 agent/parser.py:
python
from ui_tars.action_parser import parse_action_to_structure_output
def parse_response_to_actions(
response: str,
image_width: int,
image_height: int,
model_type: str = "qwen25vl",
) -> list[dict]:
"""
把模型输出 Thought + Action 转成结构化 actions。
"""
return parse_action_to_structure_output(
response,
factor=1000,
origin_resized_height=image_height,
origin_resized_width=image_width,
model_type=model_type,
)
parse_action_to_structure_output 会做这些事:
text
清理模型输出;
把 <point>x y</point> 转为坐标;
把 point / start_point / end_point 统一成 start_box / end_box;
提取 Thought / Reflection / Action;
调用 parse_action 解析函数调用;
处理坐标归一化;
返回结构化 actions。
源码中可以看到,它会先处理 <point>,再替换 start_point=、end_point=、point=,如果 model_type == "qwen25vl" 还会先计算 smart_resize,然后把 Qwen2.5-VL 的绝对坐标除以 resize 后宽高。
十、Windows 坐标适配:先从全屏截图开始
为了降低难度,MVP 先使用全屏截图。
也就是说:
text
截图坐标 = 屏幕坐标
这样 pyautogui.click(x, y) 直接可用。
后续如果你改成"只截取某个窗口",就要处理窗口偏移:
text
真实屏幕 x = 截图内 x + 窗口 left
真实屏幕 y = 截图内 y + 窗口 top
如果不处理这个偏移,就会出现:
text
模型看的是窗口截图;
pyautogui 点的是全屏坐标;
最终点击偏移。
Windows 上还要注意 DPI 缩放。
如果系统是 125%、150% 缩放,截图坐标和鼠标坐标可能不完全一致。这个问题 UI-TARS 主仓库没有替你完整处理,需要你在 Windows 自动化项目中单独适配。
MVP 建议先这样做:
text
1. Windows 显示缩放设置为 100%;
2. 使用单显示器;
3. 使用全屏截图;
4. 浏览器或目标软件窗口最大化;
5. 先跑简单任务。
这样能先验证主链路。
十一、第七步:Safety 模块
不要让模型输出什么就直接执行。
新建 agent/safety.py:
python
ALLOWED_ACTIONS = {
"click",
"left_single",
"hotkey",
"type",
"scroll",
"wait",
"finished",
}
RISKY_HOTKEYS = {
"alt f4",
"ctrl w",
"ctrl q",
"shift delete",
}
RISKY_WORDS = {
"删除",
"支付",
"付款",
"提交订单",
"发送",
"卸载",
"格式化",
}
def validate_actions(actions: list[dict]) -> None:
for action in actions:
action_type = action.get("action_type")
action_inputs = action.get("action_inputs", {})
if action_type not in ALLOWED_ACTIONS:
raise ValueError(f"暂不允许执行动作: {action_type}")
if action_type == "hotkey":
key = action_inputs.get("key") or action_inputs.get("hotkey") or ""
if key.lower() in RISKY_HOTKEYS:
raise ValueError(f"高风险快捷键,已拦截: {key}")
text = str(action)
for word in RISKY_WORDS:
if word in text:
raise ValueError(f"检测到高风险词,已拦截: {word}")
UI-TARS 官方 README 的 Limitations 中也提醒,GUI 自动化能力可能被滥用,模型可能出现误识别 GUI 元素、幻觉或次优动作。
所以二次开发时必须加安全层。
最小版本里,建议:
text
只允许 click / type / hotkey / scroll / finished;
默认拦截删除、付款、发送、提交订单等动作;
未知 action_type 直接停止;
连续失败直接停止。
十二、第八步:Executor 模块
这里有两种做法。
第一种,直接使用 UI-TARS 生成 pyautogui 代码。
python
from ui_tars.action_parser import parsing_response_to_pyautogui_code
def build_pyautogui_code(actions, image_width: int, image_height: int) -> str:
return parsing_response_to_pyautogui_code(
responses=actions,
image_height=image_height,
image_width=image_width,
input_swap=True,
)
源码中 parsing_response_to_pyautogui_code 会根据 action_type 生成不同 pyautogui 代码:hotkey 会生成 pyautogui.hotkey(...),type 默认使用 pyperclip.copy(...) 加 ctrl+v,drag/select 会生成 moveTo + dragTo,scroll 会生成 pyautogui.scroll(...),鼠标点击类动作会从 start_box 还原真实坐标后生成 click/doubleClick/rightClick/moveTo。
第二种,自己写安全执行器。
我更推荐产品化时使用第二种。
因为 UI-TARS 源码里 click、drag、scroll 等分支使用了 eval(start_box) 或 eval(end_box) 来解析坐标字符串。研究代码里这样方便,但真实产品里,模型输出属于不可信输入,最好改成 ast.literal_eval 或严格坐标解析。
MVP 可以先打印代码,不自动执行:
python
def preview_code(code: str) -> None:
print("\n========== 即将执行的 pyautogui 代码 ==========")
print(code)
print("=============================================\n")
然后人工确认再执行:
python
def execute_code_with_confirm(code: str) -> None:
preview_code(code)
confirm = input("是否执行?输入 y 执行,其他键取消:").strip().lower()
if confirm != "y":
print("已取消执行。")
return
exec(code, {})
半自动模式对前期调试非常重要。
十三、type 输入为什么默认走剪贴板?
UI-TARS 执行层中,type 默认 input_swap=True。
源码会生成:
python
import pyperclip
pyperclip.copy('...')
pyautogui.hotkey('ctrl', 'v')
如果内容以 \n 结尾,还会额外生成:
python
pyautogui.press('enter')
这个逻辑在 parsing_response_to_pyautogui_code 的 type 分支里。
为什么这样做?
因为在 Windows 上,逐字输入中文、特殊符号、JSON、路径、代码片段经常不稳定。
剪贴板粘贴更适合:
text
中文
长文本
特殊符号
文件路径
网址
代码
多行内容
但它也有风险:
text
会覆盖用户剪贴板;
某些输入框禁止粘贴;
远程桌面剪贴板可能失效;
密码框、验证码框不应该自动输入。
所以产品化时最好保存并恢复剪贴板。
十四、第九步:主循环
新建 agent/loop.py:
python
from agent.screenshot import capture_screen
from agent.messages import build_messages
from agent.parser import parse_response_to_actions
from agent.safety import validate_actions
from agent.executor import build_pyautogui_code, execute_code_with_confirm
class WindowsAgent:
def __init__(self, model_client, model_type: str = "qwen25vl", max_steps: int = 10):
self.model_client = model_client
self.model_type = model_type
self.max_steps = max_steps
self.history = []
def run(self, task: str):
for step in range(1, self.max_steps + 1):
print(f"\n========== Step {step} ==========")
screen = capture_screen()
print(f"截图: {screen['path']} ({screen['width']}x{screen['height']})")
messages = build_messages(
task=task,
screenshot_path=screen["path"],
history=self.history,
)
response = self.model_client.infer(messages)
print("\n模型输出:")
print(response)
actions = parse_response_to_actions(
response=response,
image_width=screen["width"],
image_height=screen["height"],
model_type=self.model_type,
)
print("\n结构化动作:")
print(actions)
validate_actions(actions)
code = build_pyautogui_code(
actions=actions,
image_width=screen["width"],
image_height=screen["height"],
)
if code.strip() == "DONE" or "DONE" in code:
print("任务完成。")
return
execute_code_with_confirm(code)
self.history.append({
"step": step,
"screenshot": screen["path"],
"response": response,
"actions": actions,
"code": code,
})
print("达到最大步数,停止。")
这个循环实现了最小 Agent:
text
截图
↓
调用模型
↓
解析动作
↓
安全检查
↓
人工确认
↓
执行
↓
保存历史
↓
下一轮
十五、入口 main.py
新建 config.py:
python
HF_BASE_URL = "你的 HuggingFace Endpoint Base URL"
HF_API_KEY = "你的 HuggingFace API Key"
MODEL_NAME = "tgi"
MODEL_TYPE = "qwen25vl"
MAX_STEPS = 10
新建 main.py:
python
from config import HF_BASE_URL, HF_API_KEY, MODEL_NAME, MODEL_TYPE, MAX_STEPS
from agent.model_client import UITarsModelClient
from agent.loop import WindowsAgent
def main():
task = input("请输入要执行的桌面任务:").strip()
if not task:
print("任务不能为空。")
return
client = UITarsModelClient(
base_url=HF_BASE_URL,
api_key=HF_API_KEY,
model=MODEL_NAME,
)
agent = WindowsAgent(
model_client=client,
model_type=MODEL_TYPE,
max_steps=MAX_STEPS,
)
agent.run(task)
if __name__ == "__main__":
main()
运行:
bash
python main.py
测试任务可以从简单的开始:
text
打开浏览器搜索 UI-TARS
在记事本中输入 hello world
点击当前页面中的搜索框
向下滚动网页
复制地址栏内容
不要一开始测试"删除文件""发送邮件""提交订单"这类高风险任务。
十六、坐标可视化:一定要做
做 Windows 自动操作 Agent,最容易出错的是坐标。
UI-TARS 官方坐标文档说明,对于 Qwen2.5-VL 这类模型,要先计算 smart_resize(height, width) 后的新尺寸,再把模型输出坐标映射回原图坐标;文档示例使用 model_output_width / new_width * width 和 model_output_height / new_height * height 得到真实位置,并把点画到图片上。
所以你应该增加一个可视化工具。
例如每次模型输出 click 后,在截图上画出目标点:
python
from PIL import Image, ImageDraw
import ast
def visualize_click(screenshot_path: str, start_box: str, output_path: str):
image = Image.open(screenshot_path)
width, height = image.size
coords = ast.literal_eval(start_box)
x1, y1, x2, y2 = coords
x = int(((x1 + x2) / 2) * width)
y = int(((y1 + y2) / 2) * height)
draw = ImageDraw.Draw(image)
r = 8
draw.ellipse((x - r, y - r, x + r, y + r), fill="red", outline="red")
image.save(output_path)
这样你能快速判断:
text
模型点的位置是否正确;
坐标是否偏移;
是否点到了按钮边缘;
是否点到了错误区域。
没有坐标可视化,GUI Agent 很难调试。
十七、Windows 自动化必须处理的坑
做 Windows Agent,至少会遇到这些问题。
1. DPI 缩放
Windows 显示缩放如果不是 100%,pyautogui 坐标可能和截图坐标不一致。
MVP 阶段建议先设为 100%。
2. 多显示器
多显示器会带来负坐标、扩展屏偏移等问题。
MVP 阶段建议只用单显示器。
3. 窗口焦点
模型以为浏览器在前台,但实际焦点在别的软件。
执行前可以强制把目标窗口置顶,或者先让用户手动准备好窗口。
4. 输入法状态
type 默认走剪贴板粘贴,可以减少输入法问题,但不能完全避免焦点错误。
5. 权限问题
某些管理员权限窗口、UAC 弹窗、安全软件界面,pyautogui 可能无法操作或不应该自动操作。
6. 远程桌面
RDP、虚拟机环境里,剪贴板、鼠标坐标、DPI 缩放更容易出问题。
所以实战时先从普通桌面窗口开始,不要直接挑战复杂系统界面。
十八、为什么建议先做半自动?
完整自动执行风险很高。
建议 MVP 采用半自动模式:
text
模型输出 Thought + Action
↓
显示结构化 actions
↓
显示 pyautogui 代码
↓
显示坐标可视化图
↓
用户确认
↓
执行
这样你可以观察:
text
模型是否理解任务;
Action 格式是否正确;
坐标是否准确;
pyautogui 代码是否合理;
是否存在危险动作。
等稳定后,再把部分低风险动作改成自动执行。
例如:
text
scroll 自动执行;
click 普通区域自动执行;
type 需要确认;
hotkey 需要确认;
删除、提交、支付永远确认。
十九、第一批测试任务
建议用这 5 个任务做 MVP 测试。
任务 1:点击搜索框
text
请点击浏览器页面上的搜索框。
验证:
text
截图 → 模型识别输入框 → click → 坐标还原 → 鼠标点击
任务 2:输入关键词
text
在当前输入框中输入 UI-TARS。
验证:
text
type → pyperclip → ctrl+v
任务 3:搜索并回车
text
在浏览器地址栏输入 UI-TARS GitHub 并回车搜索。
验证:
text
type(content='...\n') → 粘贴 + enter
任务 4:向下滚动
text
向下滚动页面,找到下载按钮。
验证:
text
scroll(point='...', direction='down')
任务 5:完成判断
text
如果已经看到 UI-TARS GitHub 页面,就结束任务。
验证:
text
finished(content='...')
这五个任务可以覆盖 MVP 最核心动作。
二十、不要一开始支持复杂动作
暂时不要急着支持:
text
拖拽文件
右键菜单
双击桌面图标
操作系统设置
删除文件
发送邮件
付款
验证码
密码输入
原因是这些任务涉及:
text
更高风险;
更复杂坐标;
更复杂状态变化;
更强安全要求。
先把低风险链路跑稳,再逐步扩展。
二十一、如果要升级成产品,下一步做什么?
MVP 跑通后,可以按这个顺序升级。
1. 增加安全执行器
不要 exec(code),改成自己根据结构化 action 调 pyautogui。
2. 增加窗口截图
只截取目标窗口,减少无关信息。
3. 增加窗口坐标偏移
把窗口内坐标映射到屏幕坐标。
4. 增加 DPI Awareness
处理 Windows 缩放。
5. 增加失败检测
执行后截图,对比界面是否变化。
6. 增加任务日志
保存:
text
截图
模型输出
结构化 action
生成代码
执行结果
可视化坐标
7. 增加人工接管
用户随时可以暂停、跳过、停止。
8. 增加本地任务模板
比如:
text
浏览器搜索
网页填写表单
打开软件
复制粘贴内容
简单文件管理
这样就从 demo 慢慢变成可用产品。
二十二、UI-TARS 二次开发的核心启发
UI-TARS 给我们的最大启发不是某一段 pyautogui 代码,而是它的分层方式。
text
模型层:
负责看截图、理解任务、输出下一步。
Prompt 层:
约束模型必须输出 Thought + Action。
Parser 层:
把自由文本动作变成结构化 action dict。
坐标层:
处理 point、start_box、end_box、smart_resize、归一化坐标。
Executor 层:
把结构化动作变成鼠标键盘操作。
Safety 层:
拦截高风险动作。
Loop 层:
执行后重新截图,进入下一轮。
这比"让大模型直接写 Python 自动化脚本并执行"安全得多。
因为模型输出被限制在受控动作空间里。
UI-TARS 的桌面 Prompt 明确规定了可用动作和输出格式,执行层再把结构化动作转成 pyautogui 代码;这正是 GUI Agent 工程化的关键。
总结
这篇文章我们做了一次 UI-TARS 二次开发实战设计:用 UI-TARS 做一个简单的 Windows 自动操作 Agent。
最小架构是:
text
截图模块
↓
Prompt 模块
↓
模型推理模块
↓
Parser 模块
↓
Safety 模块
↓
Executor 模块
↓
Feedback Loop
MVP 先支持:
text
click
type
hotkey
scroll
finished
先不支持高风险和复杂动作。
核心代码链路是:
text
pyautogui.screenshot()
↓
COMPUTER_USE Prompt + screenshot
↓
UI-TARS Endpoint
↓
Thought + Action
↓
parse_action_to_structure_output
↓
结构化 actions
↓
安全检查
↓
parsing_response_to_pyautogui_code
↓
人工确认
↓
pyautogui 执行
↓
重新截图
真正落地时,重点不是"能不能让鼠标动起来",而是:
text
坐标是否准确;
动作是否安全;
执行后是否验证;
失败后是否停止;
日志是否完整;
用户是否可以随时接管。
跑通这个 MVP 之后,就可以继续扩展:
text
窗口截图
DPI 适配
安全执行器
拖拽和右键
任务模板
日志回放
人工接管
这样,一个基于 UI-TARS 思路的 Windows 自动操作 Agent,就从源码学习进入了真正的二次开发阶段。