跳到主要内容

03 · 运行生命周期与人在回路:从点 Submit 到出 GIF

这是本项目最长、最有料的胶水代码:run_agent_task(src/webui/components/browser_use_agent_tab.py:275)。它把 01 章的“yield 即实时”和 02 章的“agent 子类”缝在一起。本章按时间线拆成 6 个阶段,并单独讲清最妙的“人在回路求助”。

3.1 一次任务的时间线(先看全景)

点 Submit

[阶段1] 取任务文本;空则告警返回 :303-307
│ yield → 按钮变“⏳运行中”、输入框禁用 :312-323
[阶段2] 按前缀取所有设置(agent_settings./browser_settings.) :327-411
[阶段3] _initialize_llm 造主模型(可选再造 planner 模型) :414-421 / 358-376

[阶段4] 造/复用 CustomController(注入求助回调)+ 浏览器 + context :429-495
[阶段5] 造 BrowserUseAgent,挂 step/done 两个回调 :515-555

[阶段6] agent_task = create_task(agent.run()) ← 后台真正干活 :558-560

└── while not agent_task.done(): :563-688 ← 心脏
处理 暂停 / 停止 / 求助等待 / 刷新聊天 / 推送截图

收尾: 存 history JSON、显示 GIF、按设置关浏览器、恢复按钮 :690-772

3.2 阶段 1–5:组装(略讲,挑要点)

这几段都是 01 章“按 id 取值 → 造对象”的套路,只点三处要点:

  • MCP 配置解析:从 agent_settings.mcp_server_config 取 JSON 字符串,解析成 dict,后面注入控制器(browser_use_agent_tab.py:347-355)。
  • 控制器只造一次并注入求助回调:ask_callback_wrapperwebui_manager 闭包进去,再 setup_mcp_client(browser_use_agent_tab.py:424-433)。
  • agent 复用 vs 新建:若 manager 里已有 agent,就 add_new_task 复用(保留浏览器会话);否则新建并挂回调(browser_use_agent_tab.py:523-555)。这呼应 README 的“persistent session”卖点。

两个回调是 UI 实时性的来源(都在 browser_use_agent_tab.py:515-521):

  • step_callback_wrapper_handle_new_step:每步把截图 base64 + 模型 JSON 拼成一条 assistant 消息,追加进 bu_chat_history(browser_use_agent_tab.py:134-197)。
  • done_callback_wrapper_handle_done:任务结束追加“耗时/token/结果/错误”小结(browser_use_agent_tab.py:200-221)。

注意:回调只往共享列表里塞数据,不直接碰前端。真正把数据推给前端的是下面的轮询循环。

3.3 阶段 6:轮询循环(心脏)

后台用 asyncio.create_taskagent.run() 跑起来(browser_use_agent_tab.py:558-560),主回调则进入 while not agent_task.done()(:563)。每一圈循环依次检查四种情况:

每圈(约 0.1s 一次, :688):
1. 若 agent.state.paused → yield“▶️恢复”按钮, 内层等到取消暂停或结束 :564-597
2. 若 agent.state.stopped → 给 1s 让 run() 优雅退出, 否则 cancel, break :600-615
3. 若 bu_response_event 非空(agent 在求助) → 阻塞等用户回复 :619-650
4. 常态: 聊天变长就刷新聊天框; headless 则截图推给 browser_view :653-686

截图那段值得看一眼:仅当 headless=True 时,主动调 context 截图并把 base64 包成 <img> 推给 browser_view(browser_use_agent_tab.py:660-682)——有头模式下你直接看真实窗口,无需 UI 内嵌。

3.4 暂停 / 停止:UI 只是“设标志位”

关键认知:UI 的暂停/停止按钮不直接控制 agent,只是去翻 02 章那两个标志位,真正响应在上游 run 循环里。

  • 停止(handle_stop, browser_use_agent_tab.py:836-873):设 agent.state.stopped = Truepaused = False,然后把按钮置灰。run 循环下一圈看到 stopped 就 break(回看 browser_use_agent.py:88)。
  • 暂停/恢复(handle_pause_resume, browser_use_agent_tab.py:876-903):调上游 agent.pause()/resume(),做“乐观更新”——先改按钮文字,实际状态由主循环后续确认。
用户点 Stop ──→ handle_stop 设 agent.state.stopped=True

(另一协程)BrowserUseAgent.run 的 for 循环 ── 下一圈检测到 stopped → break

run_agent_task 的 while 循环 ── 检测到 stopped → 收尾 (browser_use_agent_tab.py:600)

两个协程通过共享的 agent.state 通信,没有直接调用关系——这是异步胶水的典型解耦。

3.5 人在回路(HITL):用 asyncio.Event 把控制权交回人

这是本项目最漂亮的一段协作。流程:agent 调 ask_for_assistant 动作(02 章) → 控制器转发到 UI 注入的回调 → 回调挂起等人 → 人回复 → agent 拿到回复继续。

Agent 决定求助
│ 调动作 ask_for_assistant(query) custom_controller.py:60

ask_callback_wrapper → _ask_assistant_callback browser_use_agent_tab.py:224
│ 1. 往聊天追加 “**Need Help:** ...”
│ 2. bu_response_event = asyncio.Event() ← 创建一把锁
│ 3. await event.wait() (最多等 3600s) :247-249 ← 阻塞在这
▼ ……同时,主轮询循环看到 event 非空,把输入框/按钮切成“求助模式” :619-633
用户在输入框打字 + 点 Submit
│ handle_submit 发现 event 未 set → 写入 user_help_response → event.set() :804-809

_ask_assistant_callback 的 await 解除 → 取回复 → 追加到聊天 → 返回给 agent :262-269

核心三行(示意,提炼自真实逻辑):

# 示意:HITL 的等待端, 真实见 browser_use_agent_tab.py:242-262
webui_manager.bu_response_event = asyncio.Event() # 立一把锁
await asyncio.wait_for(webui_manager.bu_response_event.wait(), timeout=3600) # 挂起等人
return {"response": webui_manager.bu_user_help_response} # 人回复后继续
# 示意:HITL 的唤醒端, 真实见 browser_use_agent_tab.py:804-809
if webui_manager.bu_response_event and not webui_manager.bu_response_event.is_set():
webui_manager.bu_user_help_response = user_input_value # 存回复
webui_manager.bu_response_event.set() # 解锁,放 agent 走

重点看: 等待端与唤醒端是两次不同的回调调用(一次是 run 循环里的求助、一次是用户点 Submit),它们靠 webui_manager 上的同一个 bu_response_event + bu_user_help_response 配对。超时 3600s 兜底,超时就告诉 agent “用户没回”,让它别死等(browser_use_agent_tab.py:251-260)。

3.6 收尾:存历史、出 GIF、按需关浏览器

循环退出后(browser_use_agent_tab.py:690-772):等任务真正结束 → save_history(history_file) 存 JSON → 若 GIF 存在则显示 → finally 里按 keep_browser_open 决定是否关浏览器(:744-752)→ 最后 yield 一次把所有按钮恢复成可用初态。整段被大 try/except 包住,setup 阶段出错也会 yield 一条错误聊天兜底(:774-790)。

巧妙之处

  • 回调写共享列表 + 循环读列表:彻底解耦“agent 后台产出”与“前端刷新”,长度变化即信号(browser_use_agent_tab.py:653-657)。
  • 按钮只设标志、循环来响应:停止/暂停不直接干预 agent,靠 agent.state 共享标志位跨协程通信(handle_stoprun)。
  • 一把 asyncio.Event 实现 HITL:同一事件对象在两次回调间配对,把“等人”做成可超时的阻塞(_ask_assistant_callbackhandle_submit)。
  • 复用 agent 保会话:已有 agent 就 add_new_task,实现 README 承诺的“跨任务保持浏览器登录态”(browser_use_agent_tab.py:549-555)。

边界与坑

  • 轮询是固定 0.1s sleep(browser_use_agent_tab.py:688),不是事件驱动——刷新有最多约 100ms 延迟,且高频截图有开销。
  • _ask_assistant_callback 里检查的是 hasattr(... , "_chat_history")(:230),但实际用的字段叫 bu_chat_history——这个判断名对不上,属可疑代码(读源码时注意,别被误导)。
  • HITL 超时长达 1 小时(:248),期间该协程一直挂起占用。

代码地图

主题文件符号
主生命周期src/webui/components/browser_use_agent_tab.pyrun_agent_task
每步回调(截图+JSON)src/webui/components/browser_use_agent_tab.py_handle_new_step, _format_agent_output
完成回调src/webui/components/browser_use_agent_tab.py_handle_done
求助等待端src/webui/components/browser_use_agent_tab.py_ask_assistant_callback
提交/唤醒端src/webui/components/browser_use_agent_tab.pyhandle_submit
停止/暂停src/webui/components/browser_use_agent_tab.pyhandle_stop, handle_pause_resume, handle_clear
标志位响应src/agent/browser_use/browser_use_agent.pyrun (state.stopped / state.paused)