UI-TARS SDK 实践指南:用 GUIAgent、Operator 与 UITarsModel 构建跨平台 GUI 自动化 Agent
本文基于 UI-TARS-desktop 仓库中已归档的 1.0 版 SDK 文档(docs/archive-1.0/sdk.md),系统讲解 @ui-tars/sdk 的核心架构与实战用法:从 GUIAgent 的运行循环、状态机与回调协议,到 Operator 接口契约、screenshot()/execute() 的实现要求,再到自定义模型与“规划-执行”分层方案。读完后,你可以在 Node.js 或 Web 环境中装配一个能驱动真实图形界面的 UI-TARS Agent,并知道如何为自己的设备(桌面、浏览器、移动端)编写 Operator。
说明:该文档在仓库中标记为归档(archived),对应的是 SDK 1.0 时代的实验性 API;当前实现位于 packages/ui-tars/sdk(package.json 中版本为 1.2.3),下文的源码引用均以当前仓库实际代码为准。
一、SDK 定位:一个“模型 + 操作器”的跨平台 Agent 框架
@ui-tars/sdk 被官方描述为 “a powerful cross-platform (ANY device/platform) toolkit for building GUI automation agents”,它同时支持 Node.js 与 Web Browser 两种运行环境(从 packages/ui-tars/sdk/package.json 的 browser 字段与 ./core 独立入口可以印证这一点)。
其核心抽象可以用原档中的类图表达:
classDiagram
class GUIAgent~T extends Operator~ {
+model: UITarsModel
+operator: T
+signal: AbortSignal
+onData
+run()
}
class UITarsModel {
+invoke()
}
class Operator {
<<interface>>
+screenshot()
+execute()
}
class NutJSOperator {
+screenshot()
+execute()
}
class WebOperator {
+screenshot()
+execute()
}
class MobileOperator {
+screenshot()
+execute()
}
GUIAgent --> UITarsModel
GUIAgent ..> Operator
Operator <|.. NutJSOperator
Operator <|.. WebOperator
Operator <|.. MobileOperator
在仓库源码中,这套结构落位清晰:
- 入口 packages/ui-tars/sdk/src/index.ts 只导出
GUIAgent、GUIAgentConfig类型、GUIAgentData与StatusEnum,API 面非常收敛; GUIAgent的实现在 packages/ui-tars/sdk/src/GUIAgent.ts:构造时若config.model不是UITarsModel实例,就会用 OpenAI 兼容配置new UITarsModel(config.model)自动包装——这就是文档中“model可以是配置对象或自定义模型实例”两种写法的底层来源;Operator抽象类定义在 packages/ui-tars/sdk/src/types.ts,要求子类实现screenshot()与execute(),并提供可选的静态属性MANUAL(ACTION_SPACES、EXAMPLES?);- 仓库内已内置多个 Operator 实现可参考:packages/ui-tars/operators/nut-js(桌面)、packages/ui-tars/operators/browser-operator(浏览器)、packages/ui-tars/operators/browserbase、packages/ui-tars/operators/adb(移动端)。
二、快速上手
2.1 命令行体验
原档给出的最快捷入口是官方 CLI:
npx @ui-tars/cli start
输入 UI-TARS 模型服务配置(baseURL、apiKey、model)后,即可用自然语言控制电脑:
Need to install the following packages:
Ok to proceed? (y) y
│
◆ Input your instruction
│ _ Open Chrome
└
对应实现位于 packages/ui-tars/cli/src/cli/start.ts(CLI 包入口为 packages/ui-tars/cli/src/index.ts)。
2.2 SDK 基础用法
基础用法围绕 @ui-tars/sdk 包展开,以 nut-js(跨平台电脑控制库)作为 Operator 的例子如下(继承自原档,保持可直接复制):
注:NutJS Operator 支持常见桌面自动化动作:鼠标 click / double click / right click / drag / hover、键盘输入与热键、滚动、截图。
import { GUIAgent } from '@ui-tars/sdk';
import { NutJSOperator } from '@ui-tars/operator-nut-js';
const guiAgent = new GUIAgent({
model: {
baseURL: config.baseURL,
apiKey: config.apiKey,
model: config.model,
},
operator: new NutJSOperator(),
onData: ({ data }) => {
console.log(data)
},
onError: ({ data, error }) => {
console.error(error, data);
},
});
await guiAgent.run('send "hello world" to x.com');
run(instruction, historyMessages?, remoteModelHdrs?) 的完整签名见 packages/ui-tars/sdk/src/GUIAgent.ts。historyMessages 允许跨任务携带历史消息(例如“规划-执行”模式中的上下文接力)。
三、Agent 执行循环:截图 → 推理 → 动作的闭环
原档的时序图描述了每一轮循环的四步协作:
sequenceDiagram
participant user as User
participant guiAgent as GUI Agent
participant model as UI-TARS Model
participant operator as Operator
user -->> guiAgent: "`instruction` + <br /> `Operator.MANUAL.ACTION_SPACES`"
activate user
activate guiAgent
loop status !== StatusEnum.RUNNING
guiAgent ->> operator: screenshot()
activate operator
operator -->> guiAgent: base64, Physical screen size
deactivate operator
guiAgent ->> model: instruction + actionSpaces + screenshots.slice(-5)
model -->> guiAgent: `prediction`: click(start_box='(27,496)')
guiAgent -->> user: prediction, next action
guiAgent ->> operator: execute(prediction)
activate operator
operator -->> guiAgent: success
deactivate operator
end
deactivate guiAgent
deactivate user
对照 packages/ui-tars/sdk/src/GUIAgent.ts 的 while (true) 主循环,可以确认并补充几个文档没有明说的实现细节:
- 截图带重试与校验。
operator.screenshot()包裹在asyncRetry中(retry?.screenshot?.maxRetries),随后用Jimp.fromBuffer解码校验宽高是否合法;非法截图计入snapshotErrCnt,累计达到MAX_SNAPSHOT_ERR_CNT(源码常量 constants.ts 中为 10 次)后以SCREENSHOT_RETRY_ERROR状态终止。 - 滑动窗口送入 VLM。每轮把
conversations经toVlmModelFormat+processVlmParams转换后发给模型(GUIAgent.ts),即文档中screenshots.slice(-5)的“最近 5 张截图”策略;同时携带screenContext(截图宽高)与scaleFactor,这是后续坐标还原的关键输入。 - 模型调用也带重试,
minTimeout: 30s;若抛出 abort 类错误则bail直接退出循环(GUIAgent.ts)。 - 动作逐个执行。
parsedPredictions是数组,逐个交给operator.execute(...),其中screenWidth/screenHeight/scaleFactor/factors全部透传(GUIAgent.ts)。内部动作call_user会把状态置为CALL_USER,finished置为END并跳出循环(GUIAgent.ts)。 - 用户主动停止的收尾钩子。
finally中若状态为USER_STOPPED,会向operator.execute补发一个action_type: 'user_stop'的动作,让 Operator 有机会做清理(GUIAgent.ts)。
模型侧,UITarsModel.invoke() 的实现见 packages/ui-tars/sdk/src/Model.ts:先按 uiTarsVersion 选择 maxPixels 阈值对截图做压缩(preprocessResizeImage),转成 OpenAI 消息格式后调用模型,最后用 @ui-tars/action-parser 包的 actionParser 把原始 prediction 文本解析为 parsedPredictions(含坐标还原所用的 factor、screenContext、scaleFactor)。max_tokens 默认值为 1000(V1.5 为 65535),temperature 默认 0,top_p 默认 0.7(Model.ts)。
四、配置选项全解:GUIAgentConfig
GUIAgent 构造函数接受的配置在源码中定义为 packages/ui-tars/sdk/src/types.ts 的 GUIAgentConfig。结合原档说明整理如下:
| 选项 | 类型 / 默认值 | 说明 |
|---|---|---|
model |
OpenAI 兼容配置对象 或 UITarsModel 实例 |
baseURL(API 端点)、apiKey(鉴权)、model(模型名),其余可选参数同 OpenAI Chat Completions 参数 |
operator |
实现了 Operator 接口的实例 |
决定运行在什么设备上(桌面/浏览器/移动) |
signal |
AbortSignal |
取消操作的信号 |
onData |
(params: { data: GUIAgentData }) => void |
接收 Agent 数据/状态更新 |
onError |
(params: { data, error }) => void |
错误处理回调,error 为 GUIAgentError |
systemPrompt |
string(可选) |
自定义系统提示词;缺省时由 Operator 的 MANUAL.ACTION_SPACES 自动拼装 |
maxLoopCount |
number,默认 25 |
最大交互轮数(源码默认值来自 MAX_LOOP_COUNT,见 GUIAgent.ts) |
loopIntervalInMs |
number,默认 0 |
两轮循环之间的间隔毫秒数 |
retry |
{ model?, screenshot?, execute? },各含 maxRetries 与 onRetry |
三段式重试策略,默认不重试 |
logger |
可选 | 默认 console |
uiTarsVersion |
UITarsModelVersion |
模型版本,影响 max_tokens 与图像压缩像素阈值 |
4.1 onData 的增量语义(重点)
原档特别强调:data.conversations 是 delta,不是完整对话历史。每条 conversation 对象包含 from(human / gpt / screenshotBase64 等角色标识)与 value(消息内容)。在 GUIAgent.ts 中可以印证这一点——每次回调都传 conversations: data.conversations.slice(-1),即只推最后一条;状态变化(如进入 RUNNING / PAUSE)时则推 conversations: [] 的空增量。因此消费方若要还原完整轨迹,需要自行按顺序累积这些增量。完整的 GUIAgentData 结构见 packages/ui-tars/shared/src/types/data.ts(含 version、instruction、systemPrompt、modelName、status、conversations 等字段)。
4.2 状态机
stateDiagram-v2
[*] --> INIT
INIT --> RUNNING
RUNNING --> RUNNING: Execute Actions
RUNNING --> END: Task Complete
RUNNING --> MAX_LOOP: Loop Limit Reached
END --> [*]
MAX_LOOP --> [*]
当前源码中的 StatusEnum(packages/ui-tars/shared/src/types/agent.ts)比 1.0 文档更丰富:init / running / pause / end / call_user / max_loop(已标记 deprecated,为兼容保留)/ error / user_stopped。其中 PAUSE 配合 GUIAgent.pause()/resume()(GUIAgent.ts)可在执行中途挂起再恢复;user_stopped 对应 AbortSignal 触发后的终态。
五、处理 Abort 信号
文档给出的标准取消姿势是向 GUIAgent 传入 AbortController.signal:
const abortController = new AbortController();
const guiAgent = new GUIAgent({
// ... other config
signal: abortController.signal,
});
// ctrl/cmd + c to cancel operation
process.on('SIGINT', () => {
abortController.abort();
});
从源码看,signal?.aborted 会在每轮循环顶部被检查(GUIAgent.ts),命中后置为 USER_STOPPED 并跳出;模型请求内部也会透传该 signal(通过 useContext 注入,见 GUIAgent.ts),使进行中的 OpenAI 调用可被中断。
六、高级用法(一):自定义 Operator
实现自定义 Operator 只需两个核心方法:screenshot() 与 execute()。
6.1 包骨架
原档给出的 operator 包 package.json 模板(保持完整,可直接照抄改造):
{
"name": "your-operator-tool",
"version": "1.0.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"scripts": {
"dev": "rslib build --watch",
"prepare": "npm run build",
"build": "rsbuild",
"test": "vitest"
},
"files": [
"dist"
],
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org"
},
"dependencies": {
"jimp": "^1.6.0"
},
"peerDependencies": {
"@ui-tars/sdk": "^1.2.0-beta.17"
},
"devDependencies": {
"@ui-tars/sdk": "^1.2.0-beta.17",
"@rslib/core": "^0.5.4",
"typescript": "^5.7.2",
"vitest": "^3.0.2"
}
}
仓库内的 packages/ui-tars/operators/nut-js/package.json 等 Operator 包即采用同构的 rslib 构建方案,可作为参照。
6.2 screenshot() 契约
返回 ScreenshotOutput(继承自共享类型 ScreenshotResult,见 packages/ui-tars/shared/src/types/agent.ts):
interface ScreenshotOutput {
// Base64 encoded image string
base64: string;
// Device pixel ratio (DPR)
scaleFactor: number;
}
源码注释强调 base64 应保持物理像素尺寸(physical_pixels = logical_resolution * scaleFactor),这与后续坐标换算直接相关。
6.3 execute() 契约
ExecuteParams 完整定义(与 packages/ui-tars/sdk/src/types.ts 一致):
interface ExecuteParams {
/** Raw prediction string from the model */
prediction: string;
/** Parsed prediction object */
parsedPrediction: {
action_type: string;
action_inputs: Record<string, any>;
reflection: string | null;
thought: string;
};
/** Device Physical Resolution */
screenWidth: number;
/** Device Physical Resolution */
screenHeight: number;
/** Device DPR */
scaleFactor: number;
/** model coordinates scaling factor [widthFactor, heightFactor] */
factors: Factors;
}
注意 factors 是模型坐标系到设备坐标系的缩放因子,UI-TARS 模型输出的坐标默认归一化到 [1000, 1000](源码常量 DEFAULT_FACTORS = [1000, 1000],见 packages/ui-tars/sdk/src/constants.ts),actionParser 在解析阶段即结合 factors、screenContext、scaleFactor 完成还原,Operator 拿到的是可直接落点的 action_inputs。
6.4 完整示例:继承 @ui-tars/sdk/core 的 Operator
import {
Operator,
type ScreenshotOutput,
type ExecuteParams
type ExecuteOutput,
} from '@ui-tars/sdk/core';
import { Jimp } from 'jimp';
export class CustomOperator extends Operator {
// Define the action spaces and description for UI-TARS System Prompt splice
static MANUAL = {
ACTION_SPACES: [
'click(start_box="") # click on the element at the specified coordinates',
'type(content="") # type the specified content into the current input field',
'scroll(direction="") # scroll the page in the specified direction',
'finished() # finish the task',
// ...more_actions
],
};
public async screenshot(): Promise<ScreenshotOutput> {
// Implement screenshot functionality
const base64 = 'base64-encoded-image';
const buffer = Buffer.from(base64, 'base64');
const image = await sharp(buffer).toBuffer();
return {
base64: 'base64-encoded-image',
scaleFactor: 1
};
}
async execute(params: ExecuteParams): Promise<ExecuteOutput> {
const { parsedPrediction, screenWidth, screenHeight, scaleFactor } = params;
// Implement action execution logic
// if click action, get coordinates from parsedPrediction
const [startX, startY] = parsedPrediction?.action_inputs?.start_coords || '';
if (parsedPrediction?.action_type === 'finished') {
// finish the GUIAgent task
return { status: StatusEnum.END };
}
}
}
要点:
- 必须实现:
screenshot()(捕获当前屏幕)、execute()(按模型预测执行动作); - 可选静态属性:
MANUAL.ACTION_SPACES定义动作空间及说明,供 UI-TARS 模型理解; execute()返回ExecuteOutput({ status: StatusEnum }的扩展,见 types.ts),用status: StatusEnum.END主动结束任务;- 加载进
GUIAgent时,ACTION_SPACES会被注入系统提示词。源码中的拼装逻辑见 GUIAgent.tsbuildSystemPrompt:若 Operator 定义了MANUAL.ACTION_SPACES,则用SYSTEM_PROMPT_TEMPLATE并替换占位符{{action_spaces_holder}};否则回退到内置默认动作空间(click / left_double / right_single / drag / hotkey / type / scroll / wait / finished / call_user,见 constants.ts)。
const guiAgent = new GUIAgent({
// ... other config
systemPrompt: `
// ... other system prompt
${CustomOperator.MANUAL.ACTION_SPACES.join('\n')}
`,
operator: new CustomOperator(),
});
参考实现:仓库中的 NutJSOperator 同样以 static MANUAL = { ACTION_SPACES: [...] } 声明动作空间,并在 execute 中按 action_type 分发到 nut-js 的鼠标/键盘 API。
七、高级用法(二):自定义模型
通过继承 UITarsModel 并覆写 invoke() 可以实现自定义模型逻辑(原档示例):
class CustomUITarsModel extends UITarsModel {
constructor(modelConfig: { model: string }) {
super(modelConfig);
}
async invoke(params: any) {
// Implement custom model logic
return {
prediction: 'action description',
parsedPredictions: [{
action_type: 'click',
action_inputs: { /* ... */ },
reflection: null,
thought: 'reasoning'
}]
};
}
}
const agent = new GUIAgent({
model: new CustomUITarsModel({ model: 'custom-model' }),
// ... other config
});
原档附带一条重要提示:不推荐轻易实现自定义模型,因为 UITarsModel 内部封装了大量数据处理逻辑——按 uiTarsVersion 选择像素阈值压缩截图、OpenAI 消息格式转换、Response API 的增量消息与 responseId 管理(Model.ts)、以及 actionParser 的坐标解析与还原(Model.ts)。除非接入非标推理服务,通常只需换 baseURL/model 即可。
八、高级用法(三):规划模型 + 执行 Agent 的分层组合
对于多步骤复杂任务,原档建议组合规划/推理模型(如 OpenAI-o1、DeepSeek-R1)做任务分解,再逐条交给 GUIAgent 执行:
const guiAgent = new GUIAgent({
// ... other config
});
const planningList = await reasoningModel.invoke({
conversations: [
{
role: 'user',
content: 'buy a ticket from beijing to shanghai',
}
]
})
/**
* [
* 'open chrome',
* 'open trip.com',
* 'click "search" button',
* 'select "beijing" in "from" input',
* 'select "shanghai" in "to" input',
* 'click "search" button',
* ]
*/
for (const planning of planningList) {
await guiAgent.run(planning);
}
这种“推理模型出计划、GUI 模型管执行”的分工,与 SDK 的设计契合:GUIAgent.run 每次执行一个子指令并走完整的截图-推理-动作循环,而 run() 第二参数 historyMessages 可进一步在不同子任务间接力上下文。
九、小结与延伸阅读
本文以 docs/archive-1.0/sdk.md 为骨架,结合仓库源码对 @ui-tars/sdk 的三大件做了印证与补充:GUIAgent 的主循环(重试、滑动窗口、终止态)、Operator 的双方法契约与 MANUAL.ACTION_SPACES 的提示词注入机制、UITarsModel 的图像预处理与 action-parser 坐标还原。继续深入可查阅:
- SDK 核心实现:packages/ui-tars/sdk/src/GUIAgent.ts、packages/ui-tars/sdk/src/Model.ts、packages/ui-tars/sdk/src/types.ts
- 共享类型与状态枚举:packages/ui-tars/shared/src/types/agent.ts、packages/ui-tars/shared/src/types/data.ts
- 内置 Operator 参考:packages/ui-tars/operators/nut-js、packages/ui-tars/operators/browser-operator
- CLI 入口:packages/ui-tars/cli/src/index.ts
- 后续版本文档(现行版):docs/sdk.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00