浏览器智能体(browser agent)常被描述成「AI 自己上网」。但它真正做的事要朴素得多: 把页面压缩成一段文本或图片交给模型,模型回一个动作, 然后重复。理解这个「投影—动作」循环,就能推导出它的全部能力边界。 本文按 模型 / harness / 浏览器 三层拆开,一直讲到 CDP 消息。
先用严格的一句话把边界划清楚,再引入贯穿全文的洞察。
浏览器智能体是一种软件:它用一个 AI 模型,朝着某个目标去操作网页浏览器。你描述任务,它自己去打开页面、导航、选择动作,并在继续之前检查结果。因为能适应意外变化并从错误中恢复,它特别适合无法预先写死步骤的流程。
「找出这个仓库的 star 数」「把这个月的发票下载下来」。没有选择器,没有步骤,只有意图。
每一步都要看当前页面、决定点哪里或输入什么、然后确认这一步是否奏效。
结构化数据、一个已提交的表单、或一份带引用的答案。注意:跑完不等于成功。
原文把浏览器智能体的组成拆成三部分。点击任意一层展开它的职责、输入输出与典型故障点。
根据任务和当前页面,选出一个动作。它拿到的页面信息可能是文本、结构化数据,也可能是截图——由 harness 决定。
click(index=7)、type(text="…")、done(result=…)选错元素、把页面上的诱导文字当成用户指令(提示注入)、在没真正完成时宣称完成。这类失败表现为「它很自信但结果是错的」。
让循环继续转下去:给模型上下文,运行模型请求的工具,把结果传回去供模型决定下一步。它是唯一同时看得见「模型」和「浏览器」的一层,也因此承担了绝大多数工程复杂度。
原文提到 Browser Use 的 Browser Harness 走了一条变体路线:它把浏览器能力直接挂到已有的编程智能体上,让那个智能体在运行过程中自己写缺失的辅助函数,而循环仍由编程智能体的 runtime 管理——相当于不给模型新框架,只给它新工具。
加载网站,并跟踪标签页、cookie 和登录状态。它不是「被模拟」的——它是一个真浏览器在真渲染。这是浏览器智能体相对纯 HTTP 抓取的根本优势:它继承了一个真实人类会话的全部上下文。
Chrome DevTools Protocol (CDP) 或 WebDriver BiDiPlaywright MCP 把 Playwright 包装成模型可调用的工具被反爬系统质询或封禁、会话过期、多因素认证需要人介入。这一类失败换更强的模型也解决不了——因为瓶颈不在推理,而在访问权。
原文的说法很直接:「浏览器智能体基本上就是一个花哨的循环。」这句话是整个架构的骨架。
把当前页面投影成模型可读的形式,page = observe()
模型根据投影选一个动作,action = pickAction(page)
通过协议把动作变成真实事件,result = execute(action)
检查这一步是否奏效,goalReached = checkResult(result)
while (!goalReached) { page = observe() action = pickAction(page) result = execute(action) goalReached = checkResult(result)}
因为网页是有状态且异步的。这是新手最容易低估的地方,也是绝大多数"明明点对了却没生效"的根因。
| 异步问题 | 后果 | harness 该怎么处理 |
|---|---|---|
| 命令成功 ≠ 页面就绪 | 导航指令返回了,页面可能还在加载;此时读到的内容是旧的 | 等导航事件(如 Page.loadEventFired)并加超时,而不是等命令响应 |
| 页面随时会变 | 菜单弹出、弹窗盖住表单、新结果替换了刚才读的内容 | 每次动作后重新观察,不复用上一轮的投影 |
| 旧引用会失效 | 导航会销毁 JS 执行上下文,之前拿到的对象引用全部作废 | 不要缓存元素句柄,用"重新查询"代替"持有引用" |
| 单页应用延迟渲染 | 文档加载完成不代表内容就绪 | 等到你真正需要的那个元素出现,而不是等 load |
循环允许随时挂起等人:让人登录、补一个缺失信息、或批准一个敏感动作。 把它当成架构的一等公民,比事后补一个"异常处理"要可靠得多。
这是整个架构里最关键的工程决策。原文列出两条路,且可以同时使用:结构化数据,或截图。
文档对象模型:表示页面上的元素与文本。它是浏览器解析标记后形成的树。
浏览器基于 DOM 构建的辅助树,供屏幕阅读器等辅助技术使用。每个对象有四个关键属性。
让具备视觉能力的模型直接读渲染后的页面,并输出坐标去点击。
CDP 的 Accessibility.enable 官方说明里写着:启用无障碍域后,AXNodeId 才能在多次调用之间保持稳定;
但这会给页面带来性能开销,直到你把它关掉。也就是说,"让智能体看得更清楚"是有运行时成本的——
这属于典型的架构权衡,不是免费的开关。
| 维度 | DOM | 无障碍树 | 截图 |
|---|---|---|---|
| 定位方式 | 选择器 / 节点 ID | 角色 + 名称 | 像素坐标 |
| 上下文体积 | 很大 | 较小 | 中等(含图像) |
| 语义理解 | 需模型自己推断 | 直接给定 | 依赖视觉能力 |
| 自绘 / canvas | 几乎无用 | 几乎无用 | 可用 |
| 精确文字输入 | 好 | 好 | 差 |
| 小控件精度 | 高 | 高 | 易失准 |
模型输出的是文本。文本要变成真实的鼠标点击和键盘输入,中间必须有一条被广泛实现的协议通道。
| 方案 | 是什么 | 跨浏览器 | 谁在用 |
|---|---|---|---|
CDPChrome DevTools Protocol |
Chromium 系浏览器的底层调试 / 控制协议。Chrome DevTools 本身就用它和浏览器通信;也是 Playwright、Puppeteer 的底座。 | 仅 Chromium | 绝大多数浏览器智能体 / 自动化库 |
| WebDriver BiDi | W3C 工作草案(Working Draft)阶段的双向协议,目标是跨浏览器提供类似的自动化能力。 | 是 | 需要多浏览器覆盖的场景 |
MCPModel Context Protocol |
不是浏览器协议,而是把浏览器能力包装成模型可调用工具的中间层。底层仍跑真实自动化(如 Playwright MCP 底层就是 Playwright)。 | 取决于底层 | 已有编程智能体想"外挂"浏览器时 |
把能力分组。常用几个:
Page 导航、截图Runtime 执行 JavaScriptDOM 节点查询、盒模型Accessibility 无障碍树Input 鼠标 / 键盘事件Network 请求与响应任何可以被调试或控制的东西:一个页面、标签页、iframe、worker。每个 target 一般有 targetId。
你与某个特定 target 通信的连接。附加成功后 Chrome 给你一个 sessionId,用于指明后续命令发给谁。
CDP 用的是改造过的 JSON-RPC。命令带 id、method 和可选的 params;
响应重复同一个 id 并带上 result 或 error;
事件只有 method,没有 id——靠这个区分"这是回我的"还是"这是浏览器自己发生的"。
// 1) 命令:带 id,等待匹配的响应{ "id": 2, "method": "Page.navigate", "params": { "url": "https://example.com" } }// 2) 响应:id 对上了,说明这是命令回执{ "id": 2, "result": { "frameId": "A7789DDD…" } }// 3) 事件:没有 id,是浏览器主动通知{ "method": "Page.loadEventFired", "params": { "timestamp": 181921.758 } }
id: 2 的响应,只说明命令被接受了,不说明页面加载完了。
真正代表"就绪"的是那条没有 id 的 Page.loadEventFired。所以必须先开监听、再发导航,
并加上超时。顺序错了就会稳定地读到旧页面——一个非常经典的时序 bug。
把上一节的抽象概念落到具体调用,这张表就是"底层架构"最实在的部分:
| 要做的事 | CDP 命令 | 说明 |
|---|---|---|
| 取整页无障碍树 | Accessibility.enableAccessibility.getFullAXTree | 「取根文档的整棵无障碍树」,可带 depth、frameId 限制范围 |
| 按名字查控件 | Accessibility.queryAXTree | 在无障碍树里查询,而不是在 DOM 里猜选择器 |
| 取 DOM 根节点 | DOM.getDocument | 拿到根节点后可用 querySelectorAll 继续查 |
| 把节点变成坐标 | DOM.getBoxModelDOM.resolveNode | 无障碍节点带 backendDOMNodeId,据此取盒模型算出可点中心 |
| 点一下 | Input.dispatchMouseEvent | type / x / y / button / clickCount |
| 输入文字 | Input.insertText | 直接插入文本,不必逐字符模拟按键 |
| 截图 | Page.captureScreenshot | 可调 format / quality / clip |
| 存整页快照 | Page.captureSnapshot | MHTML 格式,包含 iframe、shadow DOM、外部资源与内联样式 |
| 给智能体用的专用接口 | Page.getAnnotatedPageContent | 实验性,带 includeActionableInformation 参数——可见 Chromium 已在协议层直接为智能体铺路 |
把前四节合起来,一个可运行的浏览器智能体只需要大约这些。下面是结构示意,不是完整可运行代码——但每一行的职责都是真实的。
import json, asyncio, websocketsclass MiniAgent: def __init__(self, ws_url, llm, task, max_steps=25): self.ws_url = ws_url self.llm = llm self.task = task self.max_steps = max_steps # 刹车之一:步数上限 self._id = 0 async def send(self, ws, method, params=None): # CDP:命令带 id,响应回来时靠 id 配对 self._id += 1 msg = {"id": self._id, "method": method} if params: msg["params"] = params await ws.send(json.dumps(msg)) while True: # 跳过无 id 的事件,直到等到响应 r = json.loads(await ws.recv()) if r.get("id") == self._id: return r async def observe(self, ws): # 投影:取无障碍树,而不是整个 DOM await self.send(ws, "Accessibility.enable") tree = await self.send(ws, "Accessibility.getFullAXTree", {"depth": 8}) return self.flatten(tree) # 压成「角色+名字」的紧凑列表 async def act(self, ws, a): if a["type"] == "click": # 无障碍节点带 backendDOMNodeId → 盒模型 → 中心点 box = await self.send(ws, "DOM.getBoxModel", {"backendNodeId": a["nodeId"]}) x, y = self.center(box) for t in ("mousePressed", "mouseReleased"): await self.send(ws, "Input.dispatchMouseEvent", {"type": t, "x": x, "y": y, "button": "left", "clickCount": 1}) elif a["type"] == "type": await self.send(ws, "Input.insertText", {"text": a["text"]}) async def run(self): async with websockets.connect(self.ws_url) as ws: for step in range(self.max_steps): page = await self.observe(ws) # 1 观察 action = self.llm.decide(self.task, page) # 2 决策 if action["type"] == "done": return action["result"] await self.act(ws, action) # 3 执行 await asyncio.sleep(0.3) # 4 等页面稳定后再观察 raise RuntimeError("达到步数上限仍未完成")
sleep(0.3) 是幼稚的等待,生产环境要等事件或特定元素;
② 没有成本与时间刹车,只有步数;
③ 没有对页面内容做不可信输入处理,存在提示注入风险。把这三个补上,才算接近可用。
browser-use(Python / TypeScript,MIT)——uv add browser-use 后构造 Agent(task=…, llm=…, browser=…),调 agent.run() 取 history.final_result();
② 用托管 API,只提交任务、取回结果;
③ 如果你已经在用编程智能体,给它加浏览器工具可能就够了——它的 runtime 本来就在管推理循环。
这是最省钱的一节。很多被做成智能体的任务,本来用 API 或脚本会更快、更便宜、更可靠。
服务有可用 API 就用 API;没有 API 而流程可预测,就用脚本(Playwright / Selenium / Puppeteer);只有当下一步取决于页面上出现什么、步骤无法预先写死时,才用智能体。
| 维度 | API | 脚本 | 智能体 |
|---|---|---|---|
| 决策性质 | 无(直接取数据) | 确定性:同样输入 → 同样决策 | 随机性:同样任务可能走不同路径 |
| 网站改版 | 通常无感 | 要改代码 | 往往能自己适应 |
| 单次成本 | 最低 | 低 | 高且不可预测 |
| 结果可重复性 | 高 | 高 | 低 |
| 适合 | 有接口的一切 | 每周固定下载同一份报表 | 在陌生网站里找资料、结构各异的表单 |
原文引用了 Browser Use Cloud 上 V2/V3/V4 智能体的运行数据(2026-10-18 快照),按运行次数占比统计。
原文把限制列成清单。真正有用的做法是把每条限制映射回架构层——这样看到故障现象,就能直接推断该修哪里。
机器人管理系统会质询或拦截自动化流量,速率限制会约束请求频率;有的站点允许公开浏览,却在登录或结算处加紧检查。
诊断:若错误是 403 / 质询页 / 验证码,换更强的模型没有用——瓶颈在浏览器与网络身份,要动的是代理、指纹、会话与节奏。
会话会过期,多因素认证需要人来输入。复用已认证会话来减少重复登录,但流程仍要处理"会话已失效"。
诊断:表现为突然被弹回登录页。设计上必须内置挂起等人的分支,而不是让智能体自己猜密码。
可能选错商品、漏填必填项、或在尚未保存时就报告完成。「跑完没有报错」不等于成功。重试还可能造成重复下单、重复投递。
诊断:这一层最危险,因为失败是静默的。原文给的纪律:重试前先查保存记录或确认页;做提取时把返回值与源页面对照——JSON 合法不等于数据正确。
页面里可以埋入指令,试图把智能体从你的任务上引开。这是把不可信内容喂进决策层的经典后果。
诊断:应对方式不是"教模型别听",而是架构隔离:把页面内容当不可信输入、限制智能体能访问的账号与动作、对敏感动作强制人工批准。原文诚实地加了一句:这些措施降低风险,但不能消除。
因为跑在 LLM 上,你无法预知一个任务要一步、两步还是很多步。按用量计费时,更多模型调用与浏览器时长直接推高成本;失败后重跑还要再付一次。
诊断:这是控制层问题,靠限额与统计解决,不是靠省钱提示词。下一节给出正确算法。
原文给了一个很实用的成本口径,能避免绝大多数自欺。
| 常见算法 | 问题 |
|---|---|
| 只算成功那一次的花费 | 系统性低估。失败与重试的成本被隐藏 |
| 按"每次运行均价"报预算 | 方差极大,均值没有预测力 |
| 只统计 API 费用 | 漏掉浏览器时长(云浏览器按小时计费) |
| 总花费 ÷ 已验证成功数 | 唯一能用来做决策的口径 |
原文给的上手路径很克制,值得照做——因为智能体的失败模式是静默的,先用低风险任务建立直觉。
例如采集商品价格。不要一上来就做下单、投递、付款这类有副作用的任务。
规定:允许访问哪些页面、必须返回哪些字段、什么算成功。这三件事写清楚,比优化提示词更有效。
运行前设定时间、步数、成本上限。(若用开源库,对应 max_steps 一类参数。)
托管智能体只管提交任务与取回结果;自建则用开源库配本地或云浏览器。如果已有编程智能体,先试"加浏览器工具"这条最轻的路。
记录四个指标:已验证完成率、耗时、总成本、人工修正次数。单次成功没有意义,重复性才是信号。
只在理解了"它在哪里需要人帮忙"之后,才增加更多站点或任务。
模型决定做什么,harness 维持循环并翻译,浏览器持有真实会话。三者的失败模式互不重叠,所以要分开诊断。
模型只能读 token。它的"视力"完全取决于 harness 把页面投影成 DOM、无障碍树还是截图。投影决定上限。
异步让"命令成功"变得不可信,概率性让"同样任务"变得不可复现。所以必须校验结果、必须设限额、必须按已验证成功算成本。
Accessibility.getFullAXTree、Accessibility.enable 的性能提示、DOM.getBoxModel、Input.dispatchMouseEvent、Page.captureScreenshot、Page.captureSnapshot、Page.getAnnotatedPageContent 的实验性标注)。本页所有命令名与参数均据此核对,未凭记忆书写。
chromedevtools.github.io/devtools-protocoluv add browser-use、Agent(task, llm, browser) 接口、Browser.from_system_chrome()、profile 同步只传 cookie 不含 localStorage)。
github.com/browser-use/browser-use