跳到主要内容

01 · 主循环:一步怎么走完

本章讲经典 Agent 的节奏:run() 怎么反复跑 step(),一个 step() 内部三个阶段各干什么,以及一步里下多个动作时怎么防止「基于旧页面的编号去操作新页面」。

1.1 它要解决的小问题

LLM 一次只能回一段话。要让它「持续操作浏览器直到完成任务」,就得有一个外层循环:每一轮把最新页面给它、收它的动作、执行、再把结果反馈回去。这就是 agent loop。

1.2 思路/直觉

像玩回合制游戏:每回合先「观察棋盘」(感知),再「想一步」(LLM),然后「落子」(执行动作),系统结算后进入下一回合。回合上限是 max_steps(默认 500),模型主动喊「我完成了」就调用 done 结束。

1.3 主循环骨架

run() 的核心就是一个 while:

# 示意,非源码;对应 Agent.run() 的主体
while self.state.n_steps <= max_steps:
if self.state.paused: # 支持暂停/恢复(Ctrl+C)
await self._external_pause_event.wait()
if self.state.consecutive_failures >= max_failures + ...: # 连续失败太多就停
break
if self.state.stopped: # 被外部叫停
break
await self.step(step_info) # 跑一步(见下)
if 最后一个动作 is done: # 模型喊完成
break

真实实现见 agent/service.py:2492 Agent.run,while 在 agent/service.py:2589。注意三个早停条件:暂停、连续失败超限(agent/service.py:2599)、被 stop。

1.4 一个 step 的三阶段

step() 把一轮拆成清晰的三段(agent/service.py:1027 Agent.step):

Phase 1 _prepare_context() 感知 + 组装给 LLM 的消息
│ 拿浏览器状态(DOM 清单 + 截图)
│ 注入历史、计划、各种 nudge(提醒)

Phase 2 _get_next_action() 调 LLM 拿结构化输出
_execute_actions() multi_act 把动作落到浏览器

Phase 3 _post_process() 记录下载/更新计划/失败计数/死循环检测

几个值得注意的细节:

  • 清状态的时机很讲究。 在感知完、调 LLM 前,代码会清掉 last_model_output / last_result(agent/service.py:1062)——这样如果 LLM 调用或动作执行超时,也不会把上一步的陈旧数据留在历史里。
  • 截图总是拍。 即使 use_vision=False 也拍截图(agent/service.py:1089 注释),因为云端同步要用,且现在拍图很快。
  • 整步包在 try/except 里。 任何异常都汇到 _handle_step_error 一处处理(agent/service.py:1072),finally_finalize 收尾。

1.5 感知阶段:状态怎么变成消息

_prepare_context(agent/service.py:1079)做的事,按顺序:

  1. browser_session.get_browser_state_summary(include_screenshot=True) 拿到「带编号元素清单 + 截图 + URL + 标签页」。
  2. _update_action_models_for_page(url) 按当前 URL 过滤出可用动作(有的动作限定域名)。
  3. message_manager.create_state_messages(...) 把这些拼成 LLM 消息。
  4. 一串「nudge」注入:预算警告、重规划提醒、探索提醒、死循环检测提醒(_inject_loop_detection_nudge,agent/service.py:1149)、到了最后一步就强制 done(agent/service.py:1150)。

这里 nudge 是 Browser Use 的一个务实设计:不改 prompt 主体,而是按运行时状况临时往上下文里塞一句提醒(「你已经在同一个 URL 待了 3 步了,换个思路」),引导模型自救。

1.6 行动阶段:multi_act 与「页面变了就停手」护栏(核心巧妙)

LLM 一步可以返回多个动作(上限 max_actions_per_step,默认 5,见 agent/views.py:71),比如「在 7 号输入『北京』+ 点 9 号搜索」。但这里有个隐患:这些动作的编号都是基于这一步开始时那张页面快照算的。一旦第一个动作让页面变了,后面动作的编号可能就指向了别的元素。

multi_act(agent/service.py:2719)用两层护栏解决:

对动作队列里第 i 个动作:
执行 action[i]

├─ 若 result.is_done / error / 是最后一个 ──► 结束

├─ 【护栏 1·静态标记】该动作注册时标了 terminates_sequence?
│ (navigate / search / go_back / switch_tab 这类天然会换页)
│ ──► 跳过剩下所有动作

└─ 【护栏 2·运行时检测】比较动作前后的 URL 和焦点 target:
post_url != pre_url 或 焦点变了?
──► 跳过剩下所有动作

真实实现:护栏 1 在 agent/service.py:2804(读 registered_action.terminates_sequence),护栏 2 在 agent/service.py:2811(对比 pre_action_url / post_action_urlagent_focus_target_id)。terminates_sequence 这个静态标记在动作注册时打,例如 Tools 里导航、go_back 等动作都标了 terminates_sequence=True(tools/service.py:461tools/service.py:583)。

还有一条小规则:done 只能作为单动作出现——如果 done 出现在动作队列第 2 个及以后,直接 break 不执行(agent/service.py:2751),防止模型「边做边宣布完成」。

1.7 失败计数与死循环

_post_process(agent/service.py:1211)处理结算:

  • 只有「单动作步」的错误才计入连续失败(agent/service.py:1227)。多动作步出错交给死循环检测和重规划 nudge 处理,不直接累加失败——避免误杀。
  • 一旦有成功,consecutive_failures 清零。
  • 连续失败到 max_failures(可选再给最后一次 final_response_after_failure)就在主循环里停。

1.8 关键细节/坑

  • LLM 调用有超时。 _get_next_actionasyncio.wait_for(..., timeout=llm_timeout) 包住(agent/service.py:1176),超时会抛带「把思考写短点」提示的 TimeoutError
  • provider 故障可切备用模型。 get_model_output 捕获限流/provider 错误后尝试切 fallback_llm(agent/service.py:1967_try_switch_to_fallback_llm,可重试的状态码集合见 agent/service.py:1994)。
  • 动作数会被裁剪。 即使模型多返回了动作,也会被截到 max_actions_per_step(agent/service.py:1955)。

1.9 代码地图

主题文件符号
主循环agent/service.pyAgent.run
单步三阶段agent/service.pyAgent.step_prepare_context_get_next_action_execute_actions_post_process
多动作护栏agent/service.pyAgent.multi_act
静态标记tools/registry/service.pyRegisteredAction.terminates_sequence
备用模型agent/service.py_try_switch_to_fallback_llm