Nanobrowser 源代码解析:多智能体采集循环——Executor 如何驱动 Planner + Navigator 爬取多页
从源码看 Nanobrowser 的 Executor 类如何编排 Planner 和 Navigator 完成多步骤页面采集。核心循环、Planner 周期调度、Navigator 单步执行、失败重试与上限判定。
引言:三步采集任务在 Nanobrowser 内部是怎么跑的
一个典型的采集任务:"打开某个产品页面 → 提取价格 → 翻到下一页"。
在 Nanobrowser 内部,这个任务不是一步完成的。Executor 类把它分解为一个循环——Planner 每隔几步检查一次方向,Navigator 每步执行一个具体操作。两个智能体通过 MessageManager 共享上下文。
executor.ts 是这个机制的核心,约 400 行。
Executor 的构造函数
executor.ts:44-85
export class Executor {
private readonly navigator: NavigatorAgent;
private readonly planner: PlannerAgent;
private readonly context: AgentContext;
private tasks: string[] = [];
constructor(
task: string, taskId: string,
browserContext: BrowserContext,
navigatorLLM: BaseChatModel,
extraArgs?: Partial<ExecutorExtraArgs>,
) {
const plannerLLM = extraArgs?.plannerLLM ?? navigatorLLM;
const messageManager = new MessageManager();
const context = new AgentContext(taskId, browserContext, messageManager, ...);
this.navigatorPrompt = new NavigatorPrompt(context.options.maxActionsPerStep);
this.plannerPrompt = new PlannerPrompt();
const actionBuilder = new ActionBuilder(context, extractorLLM);
const actionRegistry = new NavigatorActionRegistry(actionBuilder.buildDefaultActions());
this.navigator = new NavigatorAgent(actionRegistry, { chatLLM: navigatorLLM, context, prompt: this.navigatorPrompt });
this.planner = new PlannerAgent({ chatLLM: plannerLLM, context, prompt: this.plannerPrompt });
}
}构造函数的关键决策:
-
Planner 和 Navigator 可以用不同的 LLM —— 通过
plannerLLM参数。让 Planner 用更强的模型(如 Claude Sonnet)做规划,Navigator 用更快的模型(如 Haiku)做执行,这是在性能和成本之间的权衡。如果没传plannerLLM,两个智能体共用同一个模型。 -
Navigator 有自己的 Prompt ——
NavigatorPrompt接收maxActionsPerStep参数控制每步最多执行多少个操作。这决定了 LLM 单次调用可以执行的操作数量上限。 -
ActionRegistry 动态构建 ——
ActionBuilder.buildDefaultActions()在运行时根据上下文(BrowserContext、LLM)构建可用的操作列表。不是硬编码的。
核心采集循环
executor.ts:135-230
async execute(): Promise<void> {
const context = this.context;
context.nSteps = 0;
const allowedMaxSteps = this.context.options.maxSteps;
let step = 0;
let latestPlanOutput = null;
let navigatorDone = false;
for (step = 0; step < allowedMaxSteps; step++) {
// 检查是否被暂停或取消
if (await this.shouldStop()) break;
// Planner 周期性执行
if (this.planner
&& (step % context.options.planningInterval === 0 || navigatorDone)) {
navigatorDone = false;
latestPlanOutput = await this.runPlanner();
if (this.checkTaskCompletion(latestPlanOutput)) break;
}
// Navigator 执行一步
navigatorDone = await this.navigate();
}
// 完成状态判定
if (latestPlanOutput?.result?.done) {
// 任务完成
} else if (step >= allowedMaxSteps) {
// 超过最大步数
}
}循环的结构
加载图表中...
planner 触发条件的细节
step % planningInterval === 0 —— 默认情况下,Planner 每 N 步运行一次(planningInterval 在 AgentOptions 中配置)。这避免了每次操作都调用最贵的模型做全文规划。
navigatorDone —— Navigator 的操作如果被标记为"完成"(navOutput.result?.done),下一次循环会触发 Planner 来验证是否真的完成了。这是 Nanobrowser 在已有的 Bug 文章中提到的问题的根因——Validator 的"完成"判断依赖于 LLM 的自我评估。
完成状态判定
循环结束后,Executor 根据以下条件决定任务的最终状态:
| 条件 | 状态 | 触发事件 |
|---|---|---|
plannerOutput.done === true | 完成 | TASK_OK |
达到 maxSteps | 失败 | TASK_FAIL + MaxStepsReachedError |
context.stopped | 取消 | TASK_CANCEL |
| 其他 | 暂停 | TASK_PAUSE |
Planner 的执行
executor.ts:235-290
private async runPlanner(): Promise<AgentOutput<PlannerOutput> | null> {
try {
// 把当前浏览器状态添加到记忆中
let positionForPlan = 0;
if (this.tasks.length > 1 || this.context.nSteps > 0) {
await this.navigator.addStateMessageToMemory();
positionForPlan = this.context.messageManager.length() - 1;
} else {
positionForPlan = this.context.messageManager.length();
}
// 执行 Planner 智能体
const planOutput = await this.planner.execute();
if (planOutput.result) {
// 把规划结果写入消息历史
this.context.messageManager.addPlan(
JSON.stringify(planOutput.result), positionForPlan
);
}
return planOutput;
} catch (error) {
// 特定错误类型直接抛出
if (error instanceof ChatModelAuthError || error instanceof RequestCancelledError || ...) {
throw error;
}
// 其他错误计入连续失败
context.consecutiveFailures++;
if (context.consecutiveFailures >= context.options.maxFailures) {
throw new MaxFailuresReachedError(...);
}
return null;
}
}关键设计:
-
先添加浏览器状态,再运行 Planner——
navigator.addStateMessageToMemory()把当前的 DOM 状态、URL、可见元素等信息写入MessageManager。Planner 读取这些信息来做决策。 -
Planner 输出写入消息历史——规划结果(观察、下一步计划、挑战)被序列化为 JSON 存入消息历史中。后续的 Navigator 操作可以读取 Planner 的规划内容。
-
失败计数——不是所有错误都立即放弃。连续失败次数超过
maxFailures才放弃。Auth 错误、URL 禁止错误、请求取消错误直接抛出——这些错误重试也没有意义。
Navigator 的执行
executor.ts:295-340
private async navigate(): Promise<boolean> {
try {
if (context.paused || context.stopped) return false;
const navOutput = await this.navigator.execute();
if (context.paused || context.stopped) return false;
context.nSteps++;
if (navOutput.error) throw new Error(navOutput.error);
context.consecutiveFailures = 0;
if (navOutput.result?.done) return true;
} catch (error) {
// 同样处理连续失败
context.consecutiveFailures++;
if (context.consecutiveFailures >= context.options.maxFailures) {
throw new MaxFailuresReachedError(...);
}
}
return false;
}Navigator 的执行逻辑比 Planner 纯粹——单步操作。成功则重置失败计数,失败则递增。Navigator 的每一步执行结果(ActionResult)通过 MessageManager 积累,形成操作历史。
错误分类体系
agents/errors.ts 定义了完整的错误类型层级:
| 错误类型 | 含义 | executor 的处理 |
|---|---|---|
ChatModelAuthError | API Key 无效或过期 | 直接抛出,不重试 |
ChatModelBadRequestError | 请求参数错误 | 直接抛出,不重试 |
ChatModelForbiddenError | API 权限不足 | 直接抛出,不重试 |
RequestCancelledError | 用户取消了任务 | 直接抛出 |
ExtensionConflictError | 其他扩展冲突 | 直接抛出 |
URLNotAllowedError | 域名不在白名单中 | 直接抛出 |
MaxStepsReachedError | 超过最大步数 | 循环结束后判定 |
MaxFailuresReachedError | 连续失败超过上限 | 在 runPlanner/navigate 中抛出 |
ResponseParseError | LLM 输出解析失败 | 内部处理,不直接抛出 |
MessageManager:两个智能体之间的通信管道
messages/service.ts 管理 Planner 和 Navigator 之间的消息流动。
关键方法:
// Planner 调用:添加规划结果
messageManager.addPlan(JSON.stringify(planOutput), positionForPlan);
// Navigator 调用:添加浏览器状态
await navigator.addStateMessageToMemory();
// 获取当前消息历史(Planner 和 Navigator 都用到)
const messages = messageManager.getMessages();消息历史的顺序:
[系统提示] → [初始任务] → [状态 1] → [Action 结果 1] → [状态 2] → [Planner 规划 1] → [状态 3] → ...这个顺序很重要——智能体的每次调用都基于完整的操作历史,而不是当前页面快照。
对爬虫开发者的启示
周期性规划 vs 每一步规划
Nanobrowser 选择每 N 步运行一次 Planner,而不是每一步都运行。这个设计对爬虫也有参考价值:
批量采集中的类似策略:
每采集 10 个页面后,检查一次当前进度(类似 Planner)
每采集 1 个页面,执行数据提取(类似 Navigator)
如果中间出错超过 3 次,放弃当前批次(类似 maxFailures)错误分类的生产价值
Nanobrowser 对错误的分类方式值得参考——不是所有失败都需要重试。API Key 过期重试 10 次也没有意义。正确的做法是把错误分为"可恢复"和"不可恢复",对不可恢复的错误立即停止并发出不同级别的告警。
第三步:关键配置参数
| 参数 | 默认值 | 含义 | 爬虫场景建议 |
|---|---|---|---|
maxSteps | 100 | 最大操作步数 | 根据页面复杂度调整 |
planningInterval | 3 | 每多少步运行一次 Planner | 简单提取设为 5,复杂操作设为 1-2 |
maxFailures | 3 | 连续失败上限 | 采集中错误多可以设大,但不超过 5 |
useVision | false | 是否使用视觉模型 | 需要处理复杂布局时开启 |
总结
Executor 的核心是一个优雅的循环设计:
- Planner 周期性指导方向(每 N 步一次,避免不必要的推理成本)
- Navigator 单步执行具体操作(每次调用一次 LLM)
- 失败计数保护(连续失败超过上限则放弃,避免无意义的重试)
- 完整的错误分类(可恢复的 vs 不可恢复的,处理方式不同)
三篇 Nanobrowser 源代码分析文章覆盖了从"页面内容提取"到"元素检测和定位"再到"多智能体协作循环"的完整链路。这三层构成了一个 AI 浏览器智能体完成数据采集任务的全部内部机制。
需要企业代理方案?
我们可根据目标站点、并发规模与稳定性目标提供定制方案。