布鲁斯的技术小屋

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.ts
  • executor.ts
  • navigator.ts
  • planner.ts
  • validator.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()

逐步解释:

  1. Agent.run(task) 会先创建 Executor
  2. Executor 构造函数里先创建:
    • MessageManager
    • EventManager
    • AgentContext
  3. AgentContext 保存整个任务运行时状态:
    • browserContext
    • messageManager
    • eventManager
    • options
    • paused/stopped
    • actionResults
    • nSteps
  4. Executor 接着创建三类 prompt:
    • NavigatorPrompt
    • PlannerPrompt
    • ValidatorPrompt
  5. ActionBuilder.buildDefaultActions() 生成一组浏览器动作,并注册到 NavigatorActionRegistry
  6. Executor 初始化三个 agent:
    • NavigatorAgent
    • PlannerAgent
    • ValidatorAgent
  7. 最后调用 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)

逐步解释:

  1. Executor.execute() 开始先重置 context.nSteps = 0
  2. browserContext.getState() 获取当前浏览器状态,供 TASK_START 事件使用。
  3. context.emitEvent(Actors.SYSTEM, TASK_START, ...) -> EventManager.emit(event) -> 订阅方收到 execution event。
  4. 进入 for (step = 0; step < maxSteps; step++)
  5. 每轮先执行 shouldStop()
    • 如果 stopped,直接终止
    • 如果 paused,一直 sleep 轮询
    • 如果连续失败超限,也终止
  6. 判断是否要跑 planner:
    • nSteps % planningInterval === 0
    • 或上一次 validator 失败
  7. 如果要 planner,可能先调用 navigator.addStateMessageToMemory(),把浏览器当前状态追加到 message history。
  8. planner.execute() 得到:
    • observation
    • done
    • next_steps
    • web_task
  9. Executor 把 plan 插入 messageManager.addPlan(...)
  10. 如果 planOutput.result.done === true,说明 planner 认为任务已完成,先不导航,转给 validator 二次确认。
  11. 如果还没完成,就调用 navigate(),内部会转到 navigator.execute()
  12. 如果 navigator 返回 done,并且配置开启 validateOutput,就调用 validator.execute()
  13. validator 如果判定不通过,会把失败原因写回 context.actionResults,下一轮 planner / navigator 都能看见。
  14. 循环结束后统一发 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>

细节:

  1. PlannerAgent.execute()STEP_START 事件。
  2. messageManager.getMessages() 拿到完整历史。
  3. 构造 plannerMessages = [plannerSystemMessage, ...messages.slice(1)],也就是忽略最早那条 navigator system message。
  4. 如果 useVisionForPlanner = false 且整体 useVision = true,会把最后一条 state message 里的图片删掉,只保留文本。
  5. 调用 BaseAgent.invoke(...)
    • 支持 structured output 的模型,直接 withStructuredOutput(...)
    • 不支持的模型,则 chatLLM.invoke(...) 后手动抽 JSON、jsonrepair、再过 zod 校验
  6. 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}

逐步展开:

  1. NavigatorAgent.execute() 先发 STEP_START
  2. addStateMessageToMemory(),这里做两件事:
    • 先把上一轮 actionResults 中需要保留的内容写进 memory
    • 再调用 prompt 生成新的当前页面 state message
  3. prompt.getUserMessage(context) 实际走的是 buildBrowserStateUserMessage(context)
  4. 这个函数内部调用 browserContext.getState()
  5. BrowserContext.getState() 会:
    • getCurrentPage()
    • page.getState()
    • 汇总当前页和所有 tab 信息
  6. Page.getState() 又会:
    • waitForPageAndFramesLoad()
    • _updateState()
  7. _updateState() 里关键步骤:
    • removeHighlight()
    • getClickableElements(...)
    • takeScreenshot() 如果 useVision
    • getScrollInfo()
    • 更新 _state.elementTree / selectorMap / screenshot / url / title
  8. getClickableElements(...) 调 DOM service:
    • 浏览器页内执行 window.buildDomTree(...)
    • parseNode(...) 变成 DOMElementNode
    • createSelectorMap(...) 建立 index -> element 映射
  9. prompt 再把这些状态拼成一条 HumanMessage:
    • 当前 URL / title
    • 其他 tabs
    • 当前可交互元素列表
    • 像素上下文
    • step 进度
    • 上一步 action result/error
    • 如果开了 vision,还会附带 screenshot image
  10. NavigatorAgent 把这条 state message 加入 MessageManager
  11. 然后 messageManager.getMessages() 拿到完整上下文,送给模型。
  12. NavigatorAgent.invoke(...) 会调用模型,期望得到:
    • current_state
    • action: [...]
  13. 拿到模型输出后,先把刚刚的 state message 从 memory 移除。
  14. 再把模型输出用 addModelOutputToMemory() 转成:
    • 一条 AIMessage tool_call
    • 一条 ToolMessage placeholder
  15. 最后进入 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() 的实际逻辑:

  1. response.action 归一化成数组。
  2. 遍历每个 action object。
  3. 取出 actionName = Object.keys(action)[0]
  4. actionRegistry.getAction(actionName)?.call(actionArgs)
  5. Action.call() 先用 zod schema 校验参数。
  6. 然后进入具体 handler。

常见 handler:

  • done
    • 直接发 ACT_START/ACT_OK
    • 返回 ActionResult({ isDone: true, extractedContent: text })
  • go_to_url
    • browserContext.navigateTo(url)
  • search_google
    • page.navigateTo("https://www.google.com/search?q=...")
  • click_element
    • page.getState()
    • selectorMap.get(index)
    • page.clickElementNode(...)
  • input_text
    • page.getState()
    • 找 element
    • page.inputTextElementNode(...)
  • switch_tab
    • browserContext.switchTab(tab_id)
  • open_tab
    • browserContext.openTab(url)
  • scroll_down
    • page.scrollDown(...)
  • send_keys
    • page.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

关键点:

  1. validator 不吃完整历史,只吃:
    • 自己的 systemMessage
    • 当前 state message
    • 可选的 plan
  2. 如果校验失败,会把失败原因塞进 context.actionResults
    • includeInMemory: true
  3. 下一轮 navigator 在 addStateMessageToMemory() 时会把这个失败原因写回上下文,帮助模型修正。

这一步很关键,因为它构成了一个简单但有效的“反思闭环”。

7. 结束与清理

完整退出路径:

  1. Executor.execute() 根据结果发:
    • TASK_OK
    • TASK_FAIL
    • TASK_CANCEL
    • TASK_PAUSE
  2. service.ts 里订阅 execution event。
  3. 如果收到 task 级结束事件,会调用 executor.cleanup()
  4. cleanup() -> browserContext.cleanup()
  5. browserContext.cleanup() 会:
    • 当前页移除高亮
    • 遍历所有 attached pages 执行 detachPuppeteer()
    • 清空 _attachedPages
    • 清空 _currentTabId

8. 你可以把它理解成的最小闭环

一句话版:

  • Executor 负责大循环和阶段调度
  • Planner 负责“想下一步大方向”
  • Navigator 负责“看页面并产出操作序列”
  • ActionBuilder 负责“把操作序列变成真实浏览器动作”
  • Validator 负责“确认任务是不是真的完成”
  • MessageManager 负责“把历史、状态、动作结果拼成 LLM 上下文”
  • BrowserContext/Page/DOMService 负责“把真实网页压缩成 agent 可消费的状态描述”