数据截至 (上游 commit 61962296c38a)
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_wrapper把webui_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_task 把 agent.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 = True、paused = 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_stop↔run)。 - 一把
asyncio.Event实现 HITL:同一事件对象在两次回调间配对,把“等人”做成可超时的阻塞(_ask_assistant_callback↔handle_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.py | run_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.py | handle_submit |
| 停止/暂停 | src/webui/components/browser_use_agent_tab.py | handle_stop, handle_pause_resume, handle_clear |
| 标志位响应 | src/agent/browser_use/browser_use_agent.py | run (state.stopped / state.paused) |