UI-TARS-Desktop(一)——Agent时序
UI-TARS-Desktop(一)——Agent时序
这篇文章从 Agent 运行时的视角,梳理 UI-TARS-Desktop 里一轮任务执行是如何被拆解、组织和推进的。按“启动阶段 -> 一轮 step -> 单个 action -> 收尾阶段”这条主线,我们可以更容易看清 planner、navigator、validator 以及浏览器状态管理之间的协作关系。
总体框架图
先看一张总览图,把任务、历史、网页状态、三类 agent 和 action 执行链路放到同一个视角里:
flowchart TB
task["User Task<br/>例:去某网站搜索商品,并返回最低价和链接"]
history["MessageManager / History<br/>- System prompt<br/>- 原始任务<br/>- 示例 tool call<br/>- 历史 plan<br/>- 历史模型输出<br/>- 历史 action result / error"]
browser["BrowserContext / Page / DOM<br/>- 当前 page<br/>- 其他 tabs<br/>- elementTree<br/>- selectorMap<br/>- screenshot<br/>- pixelsAbove / pixelsBelow"]
prompt["BasePrompt.buildBrowserStateUserMessage<br/>- 当前 URL / title<br/>- 其他 tabs<br/>- 可交互元素列表<br/>- 页面上下剩余内容<br/>- step 信息<br/>- action result / error<br/>- 可选截图"]
planner["PlannerAgent<br/>- Planner prompt<br/>- Message history<br/>- 当前 state message"]
navigator["NavigatorAgent<br/>- Navigator prompt<br/>- Message history<br/>- 当前 state message<br/>- action feedback"]
validator["ValidatorAgent<br/>- Validator prompt<br/>- Task to validate<br/>- 当前 state message<br/>- 可选 current plan"]
planOut["Planner Output<br/>- observation<br/>- done<br/>- challenges<br/>- next_steps<br/>- reasoning<br/>- web_task"]
navOut["Navigator Output<br/>- current_state<br/>- action[]"]
act["Action Execution<br/>- click / input / scroll<br/>- open tab / go url<br/>- switch tab / send key"]
valOut["Validator Output<br/>- is_valid<br/>- reason<br/>- answer"]
actionResults["ActionResults<br/>- extractedContent<br/>- error<br/>- includeInMemory<br/>- isDone"]
task --> history --> browser --> prompt
prompt --> planner
prompt --> navigator
prompt --> validator
planner --> planOut
navigator --> navOut --> act --> actionResults
validator --> valOut --> actionResults
planOut -. 写回 history .-> history
actionResults -. 写回 history .-> history
TLDR:
Agent.run() -> new Executor(...) -> 初始化 BrowserContext / MessageManager / EventManager / PlannerAgent / NavigatorAgent / ValidatorAgent / ActionRegistry -> Executor.execute() 进入循环 -> 每轮可能先 planner.execute() -> 再 navigator.execute() -> 若任务看起来完成,再 validator.execute() -> 最后发出 task 结束事件并清理 browser context。
关键代码入口:
service.tsexecutor.tsnavigator.tsplanner.tsvalidator.ts
1. 启动阶段
时序图:
sequenceDiagram
participant U as User
participant A as Agent.run
participant E as Executor
participant BC as BrowserContext
participant MM as MessageManager
participant EM as EventManager
participant N as NavigatorAgent
participant P as PlannerAgent
participant V as ValidatorAgent
U->>A: run(task)
A->>BC: new BrowserContext(config)
A->>E: new Executor(task, taskId, browserContext, llm, extraArgs)
E->>MM: new MessageManager()
E->>EM: new EventManager()
E->>E: new AgentContext(...)
E->>E: new NavigatorPrompt / PlannerPrompt / ValidatorPrompt
E->>E: new ActionBuilder(context, extractorLLM)
E->>N: new NavigatorAgent(actionRegistry,...)
E->>P: new PlannerAgent(...)
E->>V: new ValidatorAgent(...)
E->>MM: initTaskMessages(systemMessage, task)
A->>E: subscribeExecutionEvents(...)
A->>E: execute()
逐步解释:
Agent.run(task)会先创建Executor。Executor构造函数里先创建:MessageManagerEventManagerAgentContext
AgentContext保存整个任务运行时状态:browserContextmessageManagereventManageroptionspaused/stoppedactionResultsnSteps
Executor接着创建三类 prompt:NavigatorPromptPlannerPromptValidatorPrompt
ActionBuilder.buildDefaultActions()生成一组浏览器动作,并注册到NavigatorActionRegistry。Executor初始化三个 agent:NavigatorAgentPlannerAgentValidatorAgent
- 最后调用
messageManager.initTaskMessages(...),把初始上下文塞进记忆:- system prompt
- 用户 ultimate task
- 一个示例 tool call
[Your task history memory starts here]
这一步相当于把“agent 的初始脑内记忆”建好。
2. Executor 主循环
核心在 executor.ts。
时序图:
sequenceDiagram
participant E as Executor.execute
participant C as AgentContext
participant BC as BrowserContext
participant P as PlannerAgent
participant N as NavigatorAgent
participant V as ValidatorAgent
participant EM as EventManager
E->>BC: getState()
E->>C: emitEvent(TASK_START, browserState)
loop step = 0..maxSteps
E->>E: shouldStop()
alt 需要 planning
E->>N: addStateMessageToMemory() (有时)
E->>P: execute()
P-->>E: PlannerOutput
E->>C.messageManager: addPlan(...)
E->>V: setPlan(next_steps or null)
end
alt not done
E->>N: execute()
N-->>E: {done?}
end
alt done && validateOutput
E->>V: execute()
V-->>E: {is_valid}
end
end
E->>C: emitEvent(TASK_OK / TASK_FAIL / TASK_CANCEL / TASK_PAUSE)
逐步解释:
Executor.execute()开始先重置context.nSteps = 0。- 调
browserContext.getState()获取当前浏览器状态,供TASK_START事件使用。 context.emitEvent(Actors.SYSTEM, TASK_START, ...)->EventManager.emit(event)-> 订阅方收到 execution event。- 进入
for (step = 0; step < maxSteps; step++)。 - 每轮先执行
shouldStop():- 如果
stopped,直接终止 - 如果
paused,一直 sleep 轮询 - 如果连续失败超限,也终止
- 如果
- 判断是否要跑 planner:
nSteps % planningInterval === 0- 或上一次 validator 失败
- 如果要 planner,可能先调用
navigator.addStateMessageToMemory(),把浏览器当前状态追加到 message history。 - 调
planner.execute()得到:observationdonenext_stepsweb_task
Executor把 plan 插入messageManager.addPlan(...)。- 如果
planOutput.result.done === true,说明 planner 认为任务已完成,先不导航,转给 validator 二次确认。 - 如果还没完成,就调用
navigate(),内部会转到navigator.execute()。 - 如果
navigator返回 done,并且配置开启validateOutput,就调用validator.execute()。 - validator 如果判定不通过,会把失败原因写回
context.actionResults,下一轮 planner / navigator 都能看见。 - 循环结束后统一发 task 级结束事件。
3. Planner 的一轮调用
核心在 planner.ts。
时序图:
sequenceDiagram
participant E as Executor
participant P as PlannerAgent
participant MM as MessageManager
participant LLM as chatLLM
participant EM as EventManager
E->>P: execute()
P->>EM: emit(STEP_START, "Planning...")
P->>MM: getMessages()
P->>P: 构造 plannerMessages
P->>LLM: invoke(plannerMessages)
LLM-->>P: structured JSON / raw JSON
P->>EM: emit(STEP_OK, next_steps)
P-->>E: AgentOutput<PlannerOutput>
细节:
PlannerAgent.execute()发STEP_START事件。- 从
messageManager.getMessages()拿到完整历史。 - 构造
plannerMessages = [plannerSystemMessage, ...messages.slice(1)],也就是忽略最早那条 navigator system message。 - 如果
useVisionForPlanner = false且整体useVision = true,会把最后一条 state message 里的图片删掉,只保留文本。 - 调用
BaseAgent.invoke(...):- 支持 structured output 的模型,直接
withStructuredOutput(...) - 不支持的模型,则
chatLLM.invoke(...)后手动抽 JSON、jsonrepair、再过 zod 校验
- 支持 structured output 的模型,直接
- planner 返回高层计划,
Executor再决定下一步。
4. Navigator 的一轮调用
这是最关键的一轮,核心在 navigator.ts。
时序图:
sequenceDiagram
participant E as Executor.navigate
participant N as NavigatorAgent
participant MM as MessageManager
participant BP as BasePrompt
participant BC as BrowserContext
participant Page as Page
participant DOM as DOMService
participant LLM as chatLLM
E->>N: execute()
N->>Event: emit(STEP_START, "Navigating...")
N->>N: addStateMessageToMemory()
N->>BP: getUserMessage(context)
BP->>BC: getState()
BC->>Page: getCurrentPage()
Page->>Page: getState()
Page->>DOM: getClickableElements(...)
Page->>Page: takeScreenshot() (if vision)
BP-->>N: HumanMessage(stateDescription + optional image)
N->>MM: addStateMessage(...)
N->>MM: getMessages()
N->>LLM: invoke(inputMessages)
LLM-->>N: {current_state, action[]}
N->>MM: removeLastStateMessage()
N->>MM: addModelOutputToMemory()
N->>N: doMultiAction(action[])
N-->>E: {done}
逐步展开:
NavigatorAgent.execute()先发STEP_START。- 调
addStateMessageToMemory(),这里做两件事:- 先把上一轮
actionResults中需要保留的内容写进 memory - 再调用 prompt 生成新的当前页面 state message
- 先把上一轮
prompt.getUserMessage(context)实际走的是buildBrowserStateUserMessage(context)。- 这个函数内部调用
browserContext.getState()。 BrowserContext.getState()会:getCurrentPage()page.getState()- 汇总当前页和所有 tab 信息
Page.getState()又会:waitForPageAndFramesLoad()_updateState()
_updateState()里关键步骤:removeHighlight()getClickableElements(...)takeScreenshot()如果useVisiongetScrollInfo()- 更新
_state.elementTree / selectorMap / screenshot / url / title
getClickableElements(...)调 DOM service:- 浏览器页内执行
window.buildDomTree(...) parseNode(...)变成DOMElementNodecreateSelectorMap(...)建立index -> element映射
- 浏览器页内执行
- prompt 再把这些状态拼成一条 HumanMessage:
- 当前 URL / title
- 其他 tabs
- 当前可交互元素列表
- 像素上下文
- step 进度
- 上一步 action result/error
- 如果开了 vision,还会附带 screenshot image
NavigatorAgent把这条 state message 加入MessageManager。- 然后
messageManager.getMessages()拿到完整上下文,送给模型。 NavigatorAgent.invoke(...)会调用模型,期望得到:current_stateaction: [...]
- 拿到模型输出后,先把刚刚的 state message 从 memory 移除。
- 再把模型输出用
addModelOutputToMemory()转成:- 一条 AIMessage tool_call
- 一条 ToolMessage placeholder
- 最后进入
doMultiAction(...)真正执行动作。
5. 单个 action 的调用链
核心在 builder.ts。
时序图,以 click_element 为例:
sequenceDiagram
participant N as NavigatorAgent
participant AR as ActionRegistry
participant A as Action.call
participant BC as BrowserContext
participant P as Page
participant E as EventManager
N->>AR: getAction("click_element")
AR-->>N: Action
N->>A: call(args)
A->>A: zod schema 校验参数
A->>E: emit(ACT_START, desc)
A->>BC: getCurrentPage()
A->>P: getState()
P-->>A: selectorMap
A->>A: 根据 index 找 elementNode
A->>P: clickElementNode(useVision, elementNode)
A->>E: emit(ACT_OK, msg)
A-->>N: ActionResult
doMultiAction() 的实际逻辑:
- 把
response.action归一化成数组。 - 遍历每个 action object。
- 取出
actionName = Object.keys(action)[0]。 actionRegistry.getAction(actionName)?.call(actionArgs)。Action.call()先用 zod schema 校验参数。- 然后进入具体 handler。
常见 handler:
done- 直接发
ACT_START/ACT_OK - 返回
ActionResult({ isDone: true, extractedContent: text })
- 直接发
go_to_urlbrowserContext.navigateTo(url)
search_googlepage.navigateTo("https://www.google.com/search?q=...")
click_elementpage.getState()selectorMap.get(index)page.clickElementNode(...)
input_textpage.getState()- 找 element
page.inputTextElementNode(...)
switch_tabbrowserContext.switchTab(tab_id)
open_tabbrowserContext.openTab(url)
scroll_downpage.scrollDown(...)
send_keyspage.sendKeys(...)
每个 action 执行完,NavigatorAgent 会额外 sleep 1 秒,避免页面变化还没稳定。
6. Validator 的一轮调用
核心在 validator.ts。
时序图:
sequenceDiagram
participant E as Executor
participant V as ValidatorAgent
participant BP as ValidatorPrompt
participant BC as BrowserContext
participant LLM as chatLLM
participant C as AgentContext
E->>V: execute()
V->>C: emit(STEP_START, "Validating...")
V->>BP: getUserMessage(context)
BP->>BC: getState()
BP-->>V: stateMessage
V->>LLM: invoke([systemMessage, stateMessage(+plan)])
LLM-->>V: {is_valid, reason, answer}
alt is_valid = false
V->>C: actionResults = [ActionResult(includeInMemory=true,...)]
V->>C: emit(STEP_FAIL, reason)
else is_valid = true
V->>C: emit(STEP_OK, answer)
end
V-->>E: result
关键点:
- validator 不吃完整历史,只吃:
- 自己的
systemMessage - 当前 state message
- 可选的
plan
- 自己的
- 如果校验失败,会把失败原因塞进
context.actionResults:includeInMemory: true
- 下一轮 navigator 在
addStateMessageToMemory()时会把这个失败原因写回上下文,帮助模型修正。
这一步很关键,因为它构成了一个简单但有效的“反思闭环”。
7. 结束与清理
完整退出路径:
Executor.execute()根据结果发:TASK_OKTASK_FAILTASK_CANCELTASK_PAUSE
service.ts里订阅 execution event。- 如果收到 task 级结束事件,会调用
executor.cleanup()。 cleanup()->browserContext.cleanup()。browserContext.cleanup()会:- 当前页移除高亮
- 遍历所有 attached pages 执行
detachPuppeteer() - 清空
_attachedPages - 清空
_currentTabId
8. 你可以把它理解成的最小闭环
一句话版:
Executor负责大循环和阶段调度Planner负责“想下一步大方向”Navigator负责“看页面并产出操作序列”ActionBuilder负责“把操作序列变成真实浏览器动作”Validator负责“确认任务是不是真的完成”MessageManager负责“把历史、状态、动作结果拼成 LLM 上下文”BrowserContext/Page/DOMService负责“把真实网页压缩成 agent 可消费的状态描述”