本节摘要:本节解剖 browser-use 的 Agent 循环,把「智能体」还原成一段可读懂的控制流。感知阶段:页面被序列化成带编号的元素清单加页面元信息(URL、滚动位置、已打开标签页),也就是第 1.2 节的编号点击机制在每圈的重新执行。决策阶段:模型每轮输出「思考加动作」——思考解释为什么,动作指定对几号元素做什么。行动阶段:框架把动作还原成 Playwright 调用落进浏览器,执行失败会成为下一轮感知的一部分(错误回灌)。终止判断三类:模型自报完成(附最终结果)、最大步数用尽、错误熔断。本节末尾处理长任务的分解方式与死循环的治理。读完本节,agent.run() 这一个函数调用在你眼里应该展开成一张数据流图。
第 0.2 节的 first_agent.py 里,agent.run() 一个调用包办了一切;本节把它展开。经典控制流写出来平平无奇:
历史 = [任务文本] 循环(最多 max_steps 轮): 页面状态 = 感知(浏览器) # 序列化当前页面 动作, 思考 = 模型(历史 + 页面状态) # 决策:选编号 + 理由 结果 = 执行(动作, 浏览器) # 行动:Playwright 落地 历史.append(页面状态, 思考, 动作, 结果) 若 动作 == 完成: 返回 最终结果 # 终止条件一 返回 未完成 # 终止条件二(步数用尽)
四阶段里没有一个阶段是神秘力量:感知是第 1.2 节的编号清单生成;决策是一次带结构化输出的模型调用;行动是一次浏览器驱动调用;终止是两个 if。Agent 框架的价值不在发明新原理,而在于把这条循环工程化——历史管理、上下文裁剪、失败回灌、动作校验,全是脏活。《深入理解 AI Agent》第 9 章从动作空间设计的原理层面论述了为什么这样的循环能工作,本节继续只管工程。
每轮循环的感知产物是「页面状态的文本表示」,核心是第 1.2 节的编号清单,外加一层页面元信息:
# page_state_demo.txt —— 感知产物示意(对应靶场页面,节选) URL: http://127.0.0.1:8080/demo_todo.html 滚动位置: 顶部 | 打开标签页: 1 [1] 按钮 「添加」 [2] 输入框 标签「新待办」 当前值:空 [3] 链接 「关于」 [4] 列表 「待办清单」 条目:暂无待办
三个工程细节值得注意。每轮重新感知:清单不是生成一次用到尾,每步动作后页面都变了,感知必须重做——这也意味着循环的每一步都有固定的感知成本。当前值入清单:输入框的现值、列表的现内容都在清单里,模型因此能「看见自己上一步的成果」,这是反馈驱动的基础。历史滚动压缩:多轮之后历史膨胀,框架会压缩早期轮次的感知产物(只留要点),否则上下文窗口先于任务耗尽——压缩策略属于框架内部实现,效果上表现为「智能体对很久之前看过的页面细节记忆模糊」。
模型每轮返回的结构大致是「一段思考 + 一个动作」(示意,字段以官方文档为准):
# decision_demo.txt —— 模型单轮输出示意(节选) 思考: 目标是添加待办「买牛奶」。清单里 [2] 是目标输入框, 先填写,再点 [1] 添加按钮。 动作: {"type": "fill", "index": 2, "value": "买牛奶"}
思考与动作分离有两重价值:排障时思考是最好的日志(模型为什么点错,思考里写着);工程上思考不参与执行,只入历史,下轮决策因此能引用上轮的理由。动作侧则高度结构化——type 加编号加参数,编号即第 1.2 节的元素索引。这种「自然语言解释 + 结构化执行」的双轨输出,是当前浏览器智能体的通行做法,第 4.2 节会在 MCP 工具调用里再次见到同构形态。
框架收到动作后做三件事:校验(编号存在、参数合法、动作类型受支持)、执行(映射成 Playwright 调用:fill、click、navigate、scroll 等)、记录(成功或失败写入历史)。执行失败不会被立刻上抛成异常终止任务,而是回灌:错误信息(元素不可见、点击被拦截、导航超时)进入下一轮感知,模型看到「上一步失败了、原因是 X」,自行决定重试、换路径或报告无法完成。
错误回灌是 Agent 循环自带纠错倾向的原因,也是它区别于普通脚本重试逻辑的地方:脚本的重试是「原样再来」,模型的纠错是「换个法子」。代价是失败的每轮都要付一次模型调用,且纠错路径本身也是概率路径——可能越纠越偏,这正是终止条件必须存在的原因。
一个可观察的实验:把靶场页面的「添加」按钮临时改名为「提交」(或加一个遮挡层),跑一个添加任务,观察执行历史里模型如何在失败信息提示下调整动作——这是理解回灌最直接的方式。
| 终止类型 | 触发 | 产物 | 工程含义 |
|---|---|---|---|
| 模型自报完成 | 模型输出完成动作并附最终结果 | 结果 + 历史 | 正常出口;结果对不对需验证(第 6 章) |
| 最大步数用尽 | 轮数达上限 | 未完成状态 + 历史 | 资源护栏;上限值要按任务复杂度设定 |
| 错误熔断 | 连续失败或致命错误 | 错误 + 历史 | 快速失败,避免空转烧钱 |
最大步数的设定经验:简单任务(单页、一两个动作)5 到 10 步;跨页流程 15 到 30 步;再大的任务应该拆成子任务分别跑(下一节的分解话题),而不是放宽步数硬撑。代码侧:
# loop_params.py —— 循环参数与结果检查(写法示意,以官方文档为准) import asyncio from browser_use import Agent from langchain_openai import ChatOpenAI async def main() -> None: agent = Agent( task=( "在 http://127.0.0.1:8080/demo_todo.html 添加两条待办:" "买牛奶、回邮件,完成后报告列表条数。" ), llm=ChatOpenAI(model="gpt-4o"), max_steps=10, ) history = await agent.run() print("是否完成:", history.is_done()) print("最终结果:", history.final_result()) print("动作轮数:", len(history.history)) asyncio.run(main())
死循环的典型形态:模型在两个动作间来回摇摆(点 A、退回、点 A),或反复刷新同一页。治理手段三层:步数上限兜底(必有);任务表述加验收标准(「完成后报告列表条数」就是给模型一个自检锚点,比裸任务显著减少摇摆);重复动作检测(框架或自定义逻辑监测连续相同动作并打断,第 6 章验证会给出通用实现思路)。注意死循环烧的不只是钱——在真实站点上空转本身就是不良访问模式,合规上也不可接受(第 8 章)。
循环的每轮产物(页面状态、思考、动作、结果)按序累积成执行轨迹,它是浏览器智能体最重要的可观测性资产——脚本排障看栈回溯,智能体排障看轨迹。一次失败任务的轨迹开头形如(示意):
# trajectory_demo.txt —— 执行轨迹结构示意(节选) 轮 1 感知: [1]按钮「添加」 [2]输入框「新待办」 ... 思考: 先填写输入框 动作: fill(index=2, value="买牛奶") 结果: 成功 轮 2 感知: ... 输入框当前值:买牛奶 ... 思考: 填写完成,点击添加 动作: click(index=1) 结果: 失败——元素被遮挡 轮 3 感知: ... 思考: 上次点击被遮挡,先滚动再点 动作: scroll(...) → click(index=1) 结果: 成功
读轨迹的三个固定问题:模型看到了什么(感知行——候选清单是否缺目标);模型想了什么(思考行——意图有没有跑偏);动作结果如何(结果行——失败原因是否回灌)。三问分别对应定位层、语义层、执行层的故障,第 6 章的验证策略会在这三问之上建立分级处理。工程上,轨迹应当脱敏后落盘(第 2.2 节),排障与审计都靠它。
循环不是越长越好。任务一长,历史压缩开始丢细节,早期页面状态模糊化后模型容易「忘了自己做到哪」。工程做法是子目标序列:把「查三家供应商的价格并汇总」拆成「查 A 并记录、查 B 并记录、查 C 并记录、汇总」,每个子目标一个独立 Agent 运行(各自的循环短而清晰),中间产物落盘传递。第 10 章会把这种做法升级为多智能体网页工作流;本节只需记住习惯:宁可串三个短循环,不要硬跑一个长循环。
循环转起来后,最先暴露的危险动作是「提交」——支付、删除、发送,这类不可逆动作一旦被模型误触发,验证与重试都救不回来。下一节在循环上装刹车:敏感数据机制、人工确认门与域名白名单。