OpenBrowser 深度解析:让 AI 真正「用上」浏览器的自主代理框架

项目地址:https://github.com/ntegrals/openbrowser

技术栈:TypeScript + Playwright + Vercel AI SDK + Bun

许可证:MIT | 版本:v1.0+ (Production-ready)

最近更新:2026-03-31


一、项目介绍

你给 AI 一个浏览器,它帮你点击、输入、导航、提取数据------全程自主完成任意网站上的任务。这就是 OpenBrowser 的核心理念。

一句话概括:基于 Playwright 构建的、支持多模型的 AI 自主网页浏览框架

它和传统的 Selenium 脚本、Playwright 录制器有本质区别:

维度 传统浏览器自动化 OpenBrowser
任务定义 硬编码选择器 + 脚本步骤 自然语言描述任务
适应性 页面结构变了就挂掉 AI 实时分析 DOM,动态决策
模型 OpenAI / Anthropic / Google
错误处理 脚本中断 LLM 诊断 + 自动重试 + 错误分类
成本感知 Token 级成本追踪
循环检测 多维指纹 + 升级提醒

项目采用 Monorepo 结构,三个包各司其职:

复制代码
packages/
├── core/          # 核心库 (open-browser)
│   └── src/
│       ├── agent/       # Agent 逻辑、对话管理、停滞检测、结果评估
│       ├── commands/     # 25+ 命令的 Schema 和执行器
│       ├── viewport/     # 浏览器控制、事件总线、10 个守卫
│       ├── page/         # DOM 分析、内容提取
│       ├── model/        # LLM 适配器、消息格式化
│       ├── metering/     # 成本追踪
│       ├── bridge/       # IPC 服务端/客户端
│       └── config/       # 配置类型
├── cli/           # 命令行 (@open-browser/cli)
└── sandbox/       # 沙盒 (@open-browser/sandbox)

二、整体架构

先看数据流,理解全局再钻细节:

复制代码
                    ┌─────────────┐
  "帮我查 MacBook 价格"   │             │
  ───────────────►  │    Agent    │  ◄── LLM (OpenAI / Anthropic / Google)
                    │             │
                    └──────┬──────┘
                           │ ① 发送页面状态 + 任务描述
                    ┌──────▼──────┐
                    │   LLM 决策   │  返回 JSON: { currentState, actions[] }
                    └──────┬──────┘
                           │ ② 解析命令列表
                    ┌──────▼──────┐
                    │  Commands   │  click, type, scroll, extract, navigate...
                    └──────┬──────┘
                           │ ③ 执行命令
                    ┌──────▼──────┐
                    │  Viewport   │  Playwright 浏览器实例 + CDP 会话
                    └──────┬──────┘
                           │ ④ 观察结果
                    ┌──────▼──────┐
                    │  DOM / Page │  快照、可交互元素列表、截图
                    └─────────────┘
                           │
                    ⑤ 循环检测 → ⑥ 成本追踪 → 回到 ①

核心循环非常清晰:感知 → 决策 → 执行 → 观察 → 再决策。这是经典的 Agent Loop 模式,和 ReAct(Reasoning + Acting)范式一脉相承。


三、核心原理深度拆解

3.1 Agent 主循环 ------ run() 方法

Agent 类的核心是 run() 方法,这是整个框架的心脏。我把它拆成五个关键阶段:

typescript 复制代码
async run(stepLimit?: number): Promise<RunOutcome> {
  // 阶段 1:初始化
  await this.browser.start();           // 启动浏览器
  this.rebuildInstructionBuilder();      // 构建系统提示词
  await this.autoNavigateFromTask();    // 从任务文本提取 URL 自动导航
  await this.executeInitialActions();   // 执行预配置命令

  // 阶段 2:主循环
  for (let step = 1; step <= effectiveMaxSteps; step++) {
    // 暂停支持
    while (this.state.isPaused) await sleep(100);

    try {
      const result = await this.executeStep(step, effectiveMaxSteps);
      
      // 检查是否完成
      const doneResult = result.find(r => r.isDone);
      if (doneResult) {
        // Simple Judge 快速验证
        if (this.settings.enableSimpleJudge && this.judge) {
          const quickCheck = await this.judge.simpleEvaluate(task, finalResult);
          if (quickCheck.shouldRetry) continue;  // 结果不够好,继续
        }
        break;
      }

      // 阶段 3:计划更新(周期性)
      if (this.shouldUpdatePlan(step)) await this.updatePlan(step);
      
      // 阶段 4:停滞时重新规划
      if (this.settings.restrategizeOnStall) {
        const loopCheck = this.loopDetector.isStuck();
        if (loopCheck.stuck && loopCheck.severity >= 2) {
          await this.updatePlan(step);  // 卡住了,重新规划
        }
      }

      // 阶段 5:对话压缩(防 context 爆炸)
      if (this.messageManager.shouldCompactWithLlm()) {
        await this.messageManager.compactWithLlm(this.model);
      }
    } catch (error) {
      if (error instanceof ModelThrottledError) {
        // 指数退避重试
        const waitMs = error.retryAfterMs ?? 
          Math.min(60_000, this.settings.retryDelay * 1000 * 2 ** this.state.consecutiveFailures);
        await sleep(waitMs);
        continue;  // 限流不计入失败次数
      }
      
      this.state.consecutiveFailures++;
      if (this.state.consecutiveFailures >= this.settings.failureThreshold) {
        // 最后一次 LLM 调用:诊断失败原因
        const diagnosis = await this.makeFailureRecoveryCall(errors);
        throw new AgentError(`Too many consecutive failures`);
      }
    }
  }
}

五个关键设计决策

  1. Simple Judge 机制:任务"完成"不代表做对了。Agent 完成后会用另一个 LLM 调用做快速验证,不达标就继续。这避免了"表面完成但实际错误"的问题。

  2. 限流不计入失败 :API 限流是临时问题,不该和逻辑错误混为一谈。指数退避后直接 continue,不增加 consecutiveFailures

  3. 动态重规划 :检测到停滞时自动触发 updatePlan(),用 LLM 重新审视策略,而不是傻循环。

  4. 对话压缩:随着步数增加,对话历史会膨胀到爆 context。通过 LLM 压缩历史消息,保留关键信息。

  5. 失败诊断:连续失败到达阈值时,不是直接抛错退出,而是让 LLM 做最后一次诊断调用,给出"出了什么问题"和"可以怎么试"。

3.2 单步执行 ------ executeStep()

每一步的执行流程是一个完整的感知-决策-执行闭环:

typescript 复制代码
private async executeStep(step: number, stepLimit: number): Promise<CommandResult[]> {
  const timer = new Timer();

  // ── 感知层 ──
  // 1. 获取浏览器状态
  const browserState = await this.browser.getState();
  
  // 2. 动态命令 Schema:按当前 URL 重建系统提示
  if (this.settings.dynamicCommandSchema) {
    this.rebuildInstructionBuilder(browserState.url);
  }

  // 3. 提取 DOM(通过 CDP 协议获取可访问性树)
  const domState = await this.domService.extractState(
    this.browser.currentPage, this.browser.cdp!
  );

  // 4. 截图(如果启用视觉模式)
  let screenshot: string | undefined;
  if (this.settings.enableScreenshots) {
    const result = await this.browser.screenshot();
    screenshot = result.base64;
    if (this.gifRecorder) {
      this.gifRecorder.addFrame(screenshot, step, browserState.url);
    }
  }

  // ── 决策层 ──
  // 5. 构建状态消息(URL + 标题 + 标签页 + DOM 树 + 滚动位置)
  const stateText = InstructionBuilder.buildTaskPrompt(this.settings.task) + '\n\n' +
    InstructionBuilder.buildStatePrompt(
      browserState.url, browserState.title, browserState.tabs,
      domState.tree, step, stepLimit,
      domState.pixelsAbove, domState.pixelsBelow
    );

  // 6. 循环检测:注入提醒
  const loopCheck = this.loopDetector.isStuck();
  if (loopCheck.stuck) {
    stateText += InstructionBuilder.buildLoopNudge(
      this.loopDetector.getLoopNudgeMessage()
    );
    if (loopCheck.severity >= 3) throw new AgentStalledError(...);
  }

  // 7. 加入策略上下文
  if (this.settings.enableStrategy && this.state.currentPlan) {
    stateText += InstructionBuilder.buildPlanPrompt(this.state.currentPlan);
  }

  // 8. 添加到对话管理器
  this.messageManager.addStateMessage(stateText, screenshot, step);

  // 9. 调用 LLM(带 Zod 验证恢复)
  const completion = await this.invokeLlmWithRecovery(outputSchema, step);

  // ── 执行层 ──
  // 10. 标准化输出 + 执行命令
  const normalizedOutput = this.normalizeOutput(completion.parsed);
  const results = await this.tools.executeActions(
    normalizedOutput.actions as Command[], context
  );

  // 11. 记录指纹用于循环检测
  this.loopDetector.recordAction(actions);
  this.loopDetector.recordFingerprint({
    url: browserState.url,
    domHash: hashPageTree(domState.tree),
    scrollY: domState.scrollPosition.y,
    elementCount: domState.elementCount,
    textHash: hashTextContent(domState.tree.slice(0, 2000)),
  });

  // 12. 过滤敏感数据 + 记录历史
  const filteredResults = this.filterSensitiveData(results);
  this.historyList.addEntry(entry);

  return results;
}

3.3 LLM 调用与 Zod 恢复 ------ invokeLlmWithRecovery()

这是我觉得最精巧的设计之一。LLM 返回的 JSON 可能不符合 Schema,怎么办?

typescript 复制代码
private async invokeLlmWithRecovery(
  outputSchema: z.ZodType<unknown>,
  step: number,
  retryCount = 0  // 最多重试 2 次
): Promise<{ parsed, usage }> {
  const messages = this.messageManager.getMessages();
  
  const invokeOptions: InferenceOptions<unknown> = {
    messages,
    responseSchema: outputSchema,
    schemaName: this.getSchemaName(),
    schemaDescription: 'Agent decision with current state assessment and actions',
  };

  // 深度推理:传递 thinking budget
  if (this.settings.enableDeepReasoning && supportsDeepReasoning(this.model.modelId)) {
    invokeOptions.maxTokens = this.settings.reasoningBudget;
  }

  try {
    const completion = this.settings.modelDeadlineMs > 0
      ? await withDeadline(this.model.invoke(invokeOptions), this.settings.modelDeadlineMs, ...)
      : await this.model.invoke(invokeOptions);
    return { parsed: completion.parsed, usage: completion.usage };
  } catch (error) {
    // Zod 验证失败 → 把错误信息喂回 LLM 让它修
    if (error instanceof ZodError && retryCount < 2) {
      const issues = error.issues
        .map(i => `- ${i.path.join('.')}: ${i.message}`)
        .join('\n');
      
      this.messageManager.addCommandResultMessage(
        'Your previous response had a validation error. ' +
        'Please fix the following issues and respond again:\n' + issues,
        step
      );
      return this.invokeLlmWithRecovery(outputSchema, step, retryCount + 1);
    }
    throw error;
  }
}

设计亮点:不是简单重试,而是把 Zod 的具体验证错误信息(路径 + 消息)回传给 LLM,让它"看着错误修"。这比盲目重试的成功率高得多。

3.4 三种输出 Schema ------ 适配不同模型能力

Agent 根据模型能力动态选择输出 Schema:

typescript 复制代码
private getOutputSchema(): z.ZodType<unknown> {
  // 模式 1:紧凑模式(便宜/快速模型)
  // 只要求 goal + actions,不需要推理过程
  if (this.settings.compactMode || isCompactModel(this.model.modelId)) {
    return AgentDecisionCompactSchema;
  }

  // 模式 2:深度推理模式(支持 thinking 的模型)
  // 跳过显式 brain schema,让模型内部推理
  if (this.settings.enableDeepReasoning && supportsDeepReasoning(this.model.modelId)) {
    return AgentDecisionDirectSchema;
  }

  // 模式 3:标准模式(完整推理 + 动作)
  return z.object({
    currentState: ReasoningSchema,   // 评估 + 记忆 + 下一步目标
    actions: z.array(CommandSchema), // 命令数组
  });
}

这意味着你可以用 gpt-4o-mini 跑低成本任务(紧凑模式),也可以用 o1 跑复杂推理任务(深度模式),同一套代码自适应。


四、命令执行器 ------ 25+ 命令的注册与调度

4.1 命令注册模式

CommandExecutor 使用注册表模式(Registry Pattern),所有命令通过统一接口注册:

typescript 复制代码
this.registry.register({
  name: 'tap',
  description: 'Click on an element by its index',
  schema: TapCommandSchema.omit({ action: true }),
  handler: async (params, ctx) => {
    const { index, clickCount, coordinateX, coordinateY } = params;

    // 坐标点击模式(视觉模型直接给坐标)
    if (this.coordinateClickingEnabled && 
        coordinateX !== undefined && coordinateY !== undefined) {
      const clicks = clickCount ?? 1;
      for (let i = 0; i < clicks; i++) {
        await ctx.page.mouse.click(coordinateX, coordinateY);
      }
      return { success: true };
    }

    // 索引点击模式(通过 DOM 索引定位)
    await ctx.domService.clickElementByIndex(ctx.page, ctx.cdpSession, index);
    return { success: true };
  },
});

完整命令清单:

类别 命令 说明
交互 tap 点击元素(支持索引和坐标两种模式)
type_text 输入文本(支持先清空)
press_keys 键盘按键(Enter, Escape, Ctrl+A...)
select 选择下拉框选项
pick_option 按文本匹配选择下拉选项
upload 上传文件
导航 navigate 导航到 URL(带 URL 白名单/黑名单校验)
back 返回上一页
web_search Google 搜索
search 多搜索引擎搜索(Google/DuckDuckGo/Bing)
标签页 focus_tab 切换标签页
new_tab 新开标签页
close_tab 关闭标签页
滚动 scroll 页面/元素滚动(上下)
scroll_to 滚动到指定文本位置
提取 extract LLM 辅助内容提取
extract_structured 结构化数据提取(JSON Schema)
read_page 读取页面 Markdown 文本
find 查找匹配描述的元素
list_options 获取下拉框所有选项
其他 capture 截图
wait 等待
finish 标记任务完成(终止序列)

4.2 执行调度与错误分类

命令批量执行时有完善的错误处理:

typescript 复制代码
async executeActions(actions: Command[], context: ExecutionContext): Promise<CommandResult[]> {
  const results: CommandResult[] = [];
  const limit = Math.min(actions.length, this.commandsPerStep);

  for (let i = 0; i < limit; i++) {
    try {
      const result = await this.executeAction(actions[i], context);
      results.push(this.maskSensitiveResult(result, context));
      
      // 遇到终止命令(如 finish)停止
      if (result.isDone) break;
      if (this.registry.isTerminating(actions[i].action)) break;
    } catch (error) {
      // 错误分类:把浏览器原始错误翻译成人话
      const interpreted = classifyViewportError(error);
      results.push({
        success: false,
        error: `${interpreted.message} | Suggestion: ${interpreted.suggestion}`,
      });
      // 不可重试的错误(如浏览器崩溃)直接中断
      if (!interpreted.isRetryable) break;
    }
  }
  return results;
}

classifyViewportError 是一个精心设计的错误模式匹配器,覆盖了 20+ 种常见浏览器错误:

typescript 复制代码
const ERROR_PATTERNS = [
  {
    pattern: /net::ERR_NAME_NOT_RESOLVED/i,
    category: 'network',
    message: () => 'DNS resolution failed - the domain could not be found.',
    suggestion: 'Check the URL for typos or try a different URL.',
    isRetryable: false,  // DNS 错了重试也没用
  },
  {
    pattern: /Element is not visible/i,
    category: 'element_not_interactable',
    message: () => 'The element exists but is not visible.',
    suggestion: 'Try scrolling to make the element visible, or use a different element.',
    isRetryable: true,   // 滚动后可以重试
  },
  {
    pattern: /intercepts pointer events/i,
    category: 'element_not_interactable',
    message: () => 'Another element is covering the target element.',
    suggestion: 'An overlay or dialog may be blocking the click. Try closing it first.',
    isRetryable: true,
  },
  // ... 20+ 个模式
];

这个设计的价值 :LLM 收到的是 Element is not visible | Suggestion: Try scrolling to make the element visible,而不是 Playwright 的原始堆栈。LLM 能据此做出下一步决策(比如先 scroll 再 click)。


五、Viewport ------ 浏览器控制层

5.1 生命周期与连接方式

Viewport 支持三种连接方式:

typescript 复制代码
async start(): Promise<void> {
  if (this.options.wsEndpoint) {
    // 方式 1:WebSocket 连接远程浏览器
    this.browser = await chromium.connect(this.options.wsEndpoint);
  } else if (this.options.cdpUrl) {
    // 方式 2:CDP 协议连接
    this.browser = await chromium.connectOverCDP(this.options.cdpUrl);
  } else {
    // 方式 3:本地启动 Chromium
    this.browser = await this.launchBrowser();
  }
  
  // 创建/复用上下文 + 页面 + CDP 会话
  this.context = await this.createContext();
  this._currentPage = await this.context.newPage();
  this.cdpSession = await this._currentPage.context().newCDPSession(this._currentPage);
  
  // 初始化 10 个守卫(Watchdogs)
  await this.initializeWatchdogs();
}

5.2 十大守卫(Watchdogs)

Viewport 初始化了 10 个守卫,各自监听不同的浏览器事件:

typescript 复制代码
this.watchdogs = [
  new LocalInstanceGuard(),      // 防止多实例冲突
  new UrlPolicyGuard(...),        // URL 白名单/黑名单执行
  new DefaultHandlerGuard(),     // 默认下载/弹窗处理器
  new PopupGuard(),              // 弹窗拦截
  new PageReadyGuard(),          // 页面就绪检测
  new DownloadGuard(),           // 下载管理
  new BlankPageGuard(),          // 空白页处理
  new CrashGuard(),              // 崩溃检测
  new ScreenshotGuard(),         // 截图能力保障
  new PersistenceGuard(...),     // 持久化(cookie/storage)
];

守卫按优先级排序(priority 越小越优先),通过统一的 GuardContext 接口 attach 到浏览器上下文。这是一个典型的 责任链 + 观察者 混合模式。

5.3 重连机制 ------ 指数退避

浏览器断连时的自动恢复:

typescript 复制代码
async reconnect(): Promise<boolean> {
  if (this.reconnecting) return false;  // 防重入
  this.reconnecting = true;

  try {
    await this.cleanupForReconnect();  // 清理但不发事件
    let delay = this.reconnectDelayMs; // 初始延迟 1000ms

    for (let attempt = 1; attempt <= this.maxReconnectAttempts; attempt++) {
      try {
        // 重新连接...
        this._isConnected = true;
        await this.initializeWatchdogs();  // 重新挂载守卫
        return true;
      } catch (error) {
        if (attempt < this.maxReconnectAttempts) {
          await sleep(delay);
          delay *= 2;  // 指数退避:1s → 2s → 4s
        }
      }
    }
    return false;
  } finally {
    this.reconnecting = false;
  }
}

5.4 DOM 稳定性等待

这是处理动态页面(SPA、Ajax 加载)的关键:

typescript 复制代码
async waitForStableDOM(timeout = 3000, quietPeriodMs = 300): Promise<void> {
  await page.evaluate(({ timeoutMs, quietMs }) => {
    return new Promise<void>((resolve) => {
      let timer, overallTimer;
      
      const observer = new MutationObserver(() => {
        clearTimeout(timer);
        // DOM 停止变化 quietMs 后认为稳定
        timer = setTimeout(() => {
          observer.disconnect();
          resolve();
        }, quietMs);
      });

      observer.observe(document.body, {
        childList: true, subtree: true,
        attributes: true, characterData: true,
      });

      // 超时兜底:即使一直在变,超时也放行
      overallTimer = setTimeout(() => {
        observer.disconnect();
        resolve();
      }, timeoutMs);
    });
  }, { timeoutMs: timeout, quietMs: quietPeriodMs });
}

原理 :用 MutationObserver 监听 DOM 变化,当连续 300ms 没有变化时认为页面"稳定"了。这对于 React/Vue 等框架渲染的 SPA 尤为重要------DOM 还在异步更新时就提取状态会拿到不完整的数据。


六、停滞检测 ------ StallDetector

这是整个框架里我认为设计最精巧的模块之一。AI Agent 最大的问题之一就是陷入死循环------反复点击同一个按钮、在两个页面之间来回跳。

6.1 多维指纹

StallDetector 不是简单地检测"重复动作",而是构建多维指纹:

typescript 复制代码
export interface PageSignature {
  url: string;         // 当前 URL
  domHash: string;     // DOM 树哈希(快速 32-bit hash)
  scrollY: number;     // 滚动位置(按 200px 分桶)
  elementCount?: number; // 元素数量
  textHash?: string;   // 可见文本哈希(归一化后)
}

6.2 四种循环检测策略

typescript 复制代码
isStuck(): StallCheckResult {
  // 策略 1:连续重复动作
  // 同一个 action hash 连续出现 N 次
  const actionRepetitions = this.countTrailingRepetitions(this.actionHistory);
  if (actionRepetitions >= this.options.maxRepeatedActions) { ... }

  // 策略 2:双步循环(A → B → A → B)
  if (this.actionHistory.length >= 4) {
    const last4 = this.actionHistory.slice(-4);
    if (last4[0] === last4[2] && last4[1] === last4[3]) { ... }
  }

  // 策略 3:三步循环(A → B → C → A → B → C)
  if (this.actionHistory.length >= 6) {
    const last6 = this.actionHistory.slice(-6);
    if (last6[0] === last6[3] && last6[1] === last6[4] && last6[2] === last6[5]) { ... }
  }

  // 策略 4:页面状态不变(URL + 元素数都没变)
  const fpRepetitions = this.countTrailingRepetitions(this.fingerprintHashes);
  if (fpRepetitions >= this.options.maxRepeatedFingerprints) { ... }
}

6.3 动作归一化

为了减少误报,动作哈希做了智能归一化:

typescript 复制代码
private normalizeActionHash(actions: Command[]): string {
  return actions.map(action => {
    switch (action.action) {
      case 'tap':
        return `click:${action.index}`;  // 忽略 clickCount 等瞬态参数
      
      case 'web_search':
        // 搜索词排序后比较:"best pizza NYC" == "NYC best pizza"
        return `search_google:${this.normalizeSearchQuery(action.query)}`;
      
      case 'navigate':
        return `go_to_url:${action.url}`;  // 只用 URL
      
      default:
        return JSON.stringify(action);
    }
  }).join('|');
}

6.4 升级提醒机制

检测到循环后,不是直接报错退出,而是渐进式提醒

typescript 复制代码
const ESCALATING_NUDGES = [
  {
    threshold: 5,   // 重复 5 次触发
    severity: 1,    // 轻度
    message: 'You seem to be repeating similar actions. Consider trying a different approach...',
  },
  {
    threshold: 8,   // 重复 8 次触发
    severity: 2,    // 中等
    message: 'WARNING: You are stuck in a loop... You MUST change your approach...',
  },
  {
    threshold: 12,  // 重复 12 次触发
    severity: 3,    // 严重 → 抛 AgentStalledError
    message: 'CRITICAL: You have been stuck for many steps. This approach is NOT working...',
  },
];

这个梯度设计很关键:轻度时给建议(换个元素试试),中等时给警告(你必须改变策略),严重时直接终止(避免烧钱)。这在 Agent 框架里是少见的------大部分框架要么不检测循环,要么检测到就硬退出。


七、提示词工程 ------ InstructionBuilder

7.1 模板系统

系统提示词使用 .md 模板文件 + 变量插值:

typescript 复制代码
const TEMPLATE_FILES: Record<PromptTemplate, string> = {
  default: 'instructions.md',           // 标准模式
  flash: 'instructions-compact.md',     // 紧凑模式(小模型)
  'no-thinking': 'instructions-direct.md', // 直接模式(深度推理)
};

模板加载后有缓存,支持 clearTemplateCache() 热重载。

7.2 动态命令 Schema

最有趣的是 dynamicCommandSchema 功能------系统提示词会根据当前 URL 动态过滤可用命令

typescript 复制代码
private rebuildInstructionBuilder(pageUrl?: string): void {
  const systemPrompt = InstructionBuilder.fromSettings(
    this.settings,
    this.tools.registry,
    pageUrl,  // ← 传入当前 URL
  );
  this.messageManager.setInstructionBuilder(systemPrompt.build());
}

CommandCatalog.getPromptDescription(pageUrl) 会根据域名返回相关命令。比如在 google.com 上可能隐藏 upload 命令,在表单页面上突出 pick_option 命令。这减少了 LLM 需要处理的 Schema 体积,提高决策准确性。

7.3 每步状态消息结构

每一步注入 LLM 的用户消息有清晰的结构化标签:

xml 复制代码
<agent_history>
  <!-- 之前的动作摘要 -->
</agent_history>

<agent_state>
  <user_request>帮我查 MacBook 价格</user_request>
  <plan>1. 打开 apple.com 2. 找到 MacBook Pro 3. 提取价格</plan>
  <step_info>Step 5 of 25 | Today: 2026-03-31</step_info>
</agent_state>

<browser_state>
  Current tab: ab3f
  Available tabs:
    Tab ab3f: https://apple.com/shop/mac - Mac - Apple
  <page_info>2.3 pages below</page_info>
  Interactive elements (truncated to 40000 characters):
    [Start of page]
    [0] <button> Buy </button>
    [1] <a href="/macbook-pro"> MacBook Pro </a>
    ...
    [End of page]
</browser_state>

<page_specific_actions>
  <!-- 域名特定命令 -->
</page_specific_actions>

XML 标签结构让 LLM 更容易区分上下文的不同部分,这比纯文本拼接效果好很多。


八、成本追踪与敏感数据

8.1 Token 级成本追踪

typescript 复制代码
private updateCostTracking(inputTokens: number, outputTokens: number, step: number): void {
  const stepCost = calculateStepCost(inputTokens, outputTokens, this.model.modelId);
  
  this.state.cumulativeCost.totalInputTokens += inputTokens;
  this.state.cumulativeCost.totalOutputTokens += outputTokens;
  
  if (stepCost) {
    this.state.cumulativeCost.totalInputCost += stepCost.inputCost;
    this.state.cumulativeCost.totalOutputCost += stepCost.outputCost;
    this.state.cumulativeCost.totalCost += stepCost.totalCost;
    
    logger.debug(
      `Step ${step} cost: $${stepCost.totalCost.toFixed(4)} ` +
      `(cumulative: $${this.state.cumulativeCost.totalCost.toFixed(4)})`
    );
  }
}

内置了 PRICING_TABLE,按模型 ID 查询单价。这让你能实时看到一次任务花了多少钱,以及在成本失控时及时叫停。

8.2 敏感数据脱敏

如果你在任务中输入了密码、API key 等敏感信息,框架会在结果中自动遮罩:

typescript 复制代码
private filterSensitiveData(results: CommandResult[]): CommandResult[] {
  if (!this.settings.maskedValues) return results;

  return results.map(r => {
    if (!r.extractedContent) return r;
    let content = r.extractedContent;
    for (const [key, value] of Object.entries(this.settings.maskedValues!)) {
      content = content.replace(new RegExp(escapeRegExp(value), 'g'), `<${key}>`);
    }
    return { ...r, extractedContent: content };
  });
}

配置方式:

typescript 复制代码
const agent = new Agent({
  settings: {
    maskedValues: {
      password: 'mySecretPassword',
      apiKey: 'sk-xxxx'
    }
  }
});
// 结果中 "mySecretPassword" 会被替换为 "<password>"

九、设计亮点与踩坑点

设计亮点

  1. 三种 Schema 自适应:紧凑/直接/标准三种模式适配不同能力层级的模型,一套代码跑 gpt-4o-mini 到 o1。

  2. Zod 错误回传修复:不盲目重试 LLM 调用,而是把具体的 Zod 验证错误(路径 + 消息)喂回去,让 LLM "看着改"。成功率显著高于盲目重试。

  3. 渐进式循环检测:5/8/12 三级阈值 + 四种检测策略(重复动作/双步循环/三步循环/页面停滞),既不过度敏感也不放过真死循环。

  4. 错误翻译 :把 Playwright 的原始错误(net::ERR_NAME_NOT_RESOLVED)翻译成 LLM 能理解的自然语言 + 可操作建议,让 Agent 能自行从错误中恢复。

  5. DOM 稳定性等待:用 MutationObserver 等待 SPA 渲染稳定,比固定 sleep 靠谱得多。

  6. 守卫链设计:10 个独立守卫按优先级排列,各自关注一个维度(URL 策略、弹窗、崩溃、下载...),符合单一职责原则。

  7. 对话压缩:长任务下 context 会爆炸,用 LLM 压缩历史消息而非简单截断,保留语义。

踩坑点

  1. CDP 会话管理复杂 :每次切换标签页都要重新创建 CDP 会话(newCDPSession),如果忘记这一步,后续的 DOM 提取会操作在错误的页面上。源码中 switchTab()closeTab() 都仔细处理了这一点。

  2. hashPageTree 用 32-bit 整数哈希 :这是一个快速的 djb2 变体(hash = ((hash << 5) - hash + char) | 0),速度快但碰撞率不低。对于超长 DOM 树可能有误判,不过配合 textHash 和 elementCount 多维指纹,实际影响可控。

  3. commandsPerStep 默认 10 :LLM 单步最多执行 10 个命令,但遇到 finishterminatesSequence 的命令会提前中断。如果 LLM 在第 10 个命令才输出 finish,前面的 9 个命令已经执行了------成本和副作用都需注意。

  4. scroll_to 用 TreeWalker 查文本 :通过 NodeFilter.SHOW_TEXT 遍历文本节点,只做 toLowerCase().includes() 匹配。对于动态加载的内容(滚动后才渲染的元素),可能找不到目标文本。

  5. 重连后守卫全部重建reconnect()cleanupForReconnect() 会 detach 所有守卫然后重建。这意味着重连期间的浏览器事件(弹窗、下载等)可能丢失。对于需要严格事件追溯的场景需注意。

  6. web_search 硬编码 Google URLbuildGoogleSearchUrl 拼接的是 https://www.google.com/search?q=...&udm=14,其中 udm=14 是 Google 的 "no AI overview" 参数。如果 Google 改了参数名,这里就挂了。search 命令虽然支持多引擎但默认也是 Google。


十、与同类项目对比

特性 OpenBrowser Browser Use (Python) Playwright MCP
语言 TypeScript Python 多语言
运行时 Bun Python Node.js
浏览器引擎 Playwright Playwright Playwright
多模型 OpenAI/Anthropic/Google OpenAI/Anthropic/Google 依赖宿主
循环检测 四维指纹 + 三级升级 基础
成本追踪 Token 级 + 价格表 基础
错误恢复 LLM 诊断 + 分类建议 基础重试
对话压缩 LLM 压缩 N/A
沙盒执行 独立包
交互式 REPL
许可证 MIT MIT Apache-2.0

OpenBrowser 的优势在于工程化程度------它不是一个 demo,而是一个考虑了成本、错误、循环、重连、安全的生产级框架。


十一、总结

OpenBrowser 的核心价值在于:它把 LLM 的"理解网页"能力 和 Playwright 的"操作浏览器"能力,用一个精心设计的 Agent Loop 粘合在了一起

关键架构决策的回顾:

  • Agent Loop:感知(DOM+截图)→ 决策(LLM+Schema)→ 执行(25+ 命令)→ 观察(结果+指纹)→ 循环
  • 鲁棒性三件套:StallDetector(防死循环)+ classifyViewportError(错误翻译)+ invokeLlmWithRecovery(Schema 修复)
  • 成本意识:Token 级追踪 + 预算策略 + 对话压缩
  • 安全意识:URL 白名单/黑名单 + 敏感数据脱敏 + 10 个守卫

如果你在做 AI 浏览器自动化、网页数据采集、或者想理解 Agent 框架的工程实践,这个项目值得仔细读一遍源码。尤其是 agent.tsexecutor.tsstall-detector.ts 这三个文件,浓缩了大量实战经验。


本文基于 OpenBrowser master 分支 (commit 067fc45, 2026-03-31) 源码分析撰写。

相关推荐
_Jimmy_1 小时前
Tool Calling 与 Function Calling 区别
人工智能·python·langchain
ITmaster07311 小时前
告别 IDE?Android CLI 来了,开发进入 AI Agent 时代
android·ide·人工智能
陈明勇1 小时前
一篇文章,多种表达:我用 Seed Evolving 生成知识卡片
人工智能
墨舟的AI笔记1 小时前
ECS 中的确定性随机与回放:让帧同步在 DOTS 上成立
人工智能
图特摩斯科技2 小时前
本体智能应用案例实践分享:汽车零部件库存优化与召回应急保障
人工智能·汽车·palantir·ontology·ontoflow·ontoos
专业工业电源打工人2 小时前
F0505S-2WR3 适配优选 钡特电源 DF2-05S05LS|2W 隔离 DC-DC 模块电源5V转5V硬件选型参数规格解析
大数据·网络·人工智能
a1117762 小时前
三色软糖坠落玻璃池 THreeJS kimi
前端·人工智能·threejs
xian_wwq2 小时前
【学习笔记】解剖 Claude Code —— Anthropic 的 Harness 参考实现-09/15
人工智能·笔记·学习
Black_Rock_br2 小时前
打通 PyTorch Monarch 与 ROCm:单 Controller 架构的异构算力实战
人工智能·pytorch·python·开源