Nanobrowser 源代码解析:多智能体采集循环——Executor 如何驱动 Planner + Navigator 爬取多页

从源码看 Nanobrowser 的 Executor 类如何编排 Planner 和 Navigator 完成多步骤页面采集。核心循环、Planner 周期调度、Navigator 单步执行、失败重试与上限判定。

亿牛云技术团队2026年4月11日5 分钟阅读

引言:三步采集任务在 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 });
  }
}

构造函数的关键决策:

  1. Planner 和 Navigator 可以用不同的 LLM —— 通过 plannerLLM 参数。让 Planner 用更强的模型(如 Claude Sonnet)做规划,Navigator 用更快的模型(如 Haiku)做执行,这是在性能和成本之间的权衡。如果没传 plannerLLM,两个智能体共用同一个模型。

  2. Navigator 有自己的 Prompt —— NavigatorPrompt 接收 maxActionsPerStep 参数控制每步最多执行多少个操作。这决定了 LLM 单次调用可以执行的操作数量上限。

  3. 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 步运行一次(planningIntervalAgentOptions 中配置)。这避免了每次操作都调用最贵的模型做全文规划。

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;
  }
}

关键设计:

  1. 先添加浏览器状态,再运行 Planner——navigator.addStateMessageToMemory() 把当前的 DOM 状态、URL、可见元素等信息写入 MessageManager。Planner 读取这些信息来做决策。

  2. Planner 输出写入消息历史——规划结果(观察、下一步计划、挑战)被序列化为 JSON 存入消息历史中。后续的 Navigator 操作可以读取 Planner 的规划内容。

  3. 失败计数——不是所有错误都立即放弃。连续失败次数超过 maxFailures 才放弃。Auth 错误、URL 禁止错误、请求取消错误直接抛出——这些错误重试也没有意义。

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 的处理
ChatModelAuthErrorAPI Key 无效或过期直接抛出,不重试
ChatModelBadRequestError请求参数错误直接抛出,不重试
ChatModelForbiddenErrorAPI 权限不足直接抛出,不重试
RequestCancelledError用户取消了任务直接抛出
ExtensionConflictError其他扩展冲突直接抛出
URLNotAllowedError域名不在白名单中直接抛出
MaxStepsReachedError超过最大步数循环结束后判定
MaxFailuresReachedError连续失败超过上限在 runPlanner/navigate 中抛出
ResponseParseErrorLLM 输出解析失败内部处理,不直接抛出

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 次也没有意义。正确的做法是把错误分为"可恢复"和"不可恢复",对不可恢复的错误立即停止并发出不同级别的告警。

第三步:关键配置参数

参数默认值含义爬虫场景建议
maxSteps100最大操作步数根据页面复杂度调整
planningInterval3每多少步运行一次 Planner简单提取设为 5,复杂操作设为 1-2
maxFailures3连续失败上限采集中错误多可以设大,但不超过 5
useVisionfalse是否使用视觉模型需要处理复杂布局时开启

总结

Executor 的核心是一个优雅的循环设计:

  1. Planner 周期性指导方向(每 N 步一次,避免不必要的推理成本)
  2. Navigator 单步执行具体操作(每次调用一次 LLM)
  3. 失败计数保护(连续失败超过上限则放弃,避免无意义的重试)
  4. 完整的错误分类(可恢复的 vs 不可恢复的,处理方式不同)

三篇 Nanobrowser 源代码分析文章覆盖了从"页面内容提取"到"元素检测和定位"再到"多智能体协作循环"的完整链路。这三层构成了一个 AI 浏览器智能体完成数据采集任务的全部内部机制。

需要企业代理方案?

我们可根据目标站点、并发规模与稳定性目标提供定制方案。