用 BrowserUse 这类框架指挥 AI Agent 去操作浏览器,感觉上是件很省心的事:你只要把需求用大白话写出来,它就会自己决定点哪里、输入什么、翻到哪一页。但真正跑过几轮的人都会明白,只要 Agent 能控制鼠标键盘,它就有能力搞出你完全没想到的乱子。这也是为什么我在做了几周 BrowserUse 自动化之后,强制要求所有任务都必须放进 AgentRun Sandbox 里跑,先隔离再执行,而不是等项目崩了才去追责模型。这篇文章会从架构选择、容器落地、模型网关到问题排查,把整套组合的最佳实践完整盘一遍。
1. AgentRun Sandbox要兜住的那些“真实事故”
1.1 浏览器自动化的风险不只是“点错按钮”
普通爬虫再怎么出错,无非是请求打多了、解析字段匹配不上,最坏情况是被封 IP。但 BrowserUse 这类 Agent 不一样,它把“理解页面”和“执行操作”两个环节连在了一起,模型对页面的任何误读都可能转成真实的鼠标点击、键盘输入、表单提交。
我遇到过最惊险的一次,是让 Agent 批量把内部后台里状态为 pending 的记录更新成 processing。因为模型把页面左侧一个“清空本周筛选”的按钮误解成了“清空当前列表”,执行完的瞬间,筛选条件被重置,页面回到了全量数据视图。如果这时 Agent 再接一个“将记录状态改为 processing”的指令,它就有概率在一个错误的数据范围上批量操作。那次虽然没有造成真实损失,但让我意识到一件事:Agent 执行链路里的任何一个判断失误,都被浏览器自动化放大成了有实际副作用的动作。
所以风险不是单点的,而是三类叠加:意图理解错误、页面结构误判、操作副作用被自动执行。爬虫最多是数据抓错,Agent 则可能把线上状态改错、把表单提交到错误环境、把文件下载到宿主机、甚至把登录态 Cookie 暴露给第三方模型接口。这也是我坚持把沙箱放到架构设计第一优先级的原因。
1.2 沙箱隔离的三层边界:网络、文件与身份
Agent 在浏览器里能做的所有事,本质上都落到三个方向上:访问了哪些网络地址、读写了哪些文件、使用了什么身份凭证。我觉得 AgentRun Sandbox 要做的事情,就是把这三条边界从默认的“宿主全部放开”收紧到“容器内部最小可用”。
第一层是网络边界。容器内最好只允许访问必要域名,比如目标业务站点、模型 API、偶尔要用到的静态资源 CDN,其余一律不放行。如果公司内部环境有统一的 HTTP 出口代理,直接把代理配成白名单模式,比在容器里自己维护 iptables 规则省事得多。
第二层是文件系统边界。Agent 运行时的临时文件、下载文件、浏览器缓存,全部落在容器内的挂载目录里,宿主机目录只读暴露给容器。这样即便 Agent 被诱导去下载恶意文件,执行权限也限制在容器内部,影响面可控。
第三层是身份凭证边界。自动化需要的登录态、Token、Cookie 不能直接写在代码或 prompt 里,要放到沙箱启动时从密钥服务注入的环境变量或配置文件中。BrowserUse 运行期间拿到的会话状态,尽量只保留在容器内的持久化目录,避免被模型端或日志系统回传。
1.3 什么项目真的需要这套组合
不是所有自动化任务都值得上 BrowserUse + AgentRun Sandbox。决策标准可以很简单:如果任务路径在写代码时就能完全确定,用 Playwright 写固定脚本就够;如果任务需要根据页面内容动态决策,才需要考虑 Agent。而一旦引入 Agent,又需要判断操作失败后的影响面。
我自己会跑这套组合的任务有几类:一是管理后台里的多步表单操作,比如从列表页筛选记录、进入详情、修改状态;二是跨页面信息比对,比如把 A 系统的单据号逐个去 B 系统核对,这中间伴随登录跳转和模态框,传统脚本写起来要处理的状态分支非常多;三是高度重复但允许由模型临场发挥的任务,比如根据某条商品的标题去搜索对应类目。
反过来,如果是纯只读信息抓取、公开页面内容聚合,我建议直接用普通爬虫加解析脚本就好,Agent 的 token 成本和不可控性都不划算。沙箱解决的是可信执行问题,不是性能问题,别把架构搞复杂。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 架构设计与选型:BrowserUse不该裸奔在宿主机上
2.1 可参考的分层架构
我内部跑 BrowserUse 任务时的参考架构分五层,每一层职责从字面上就能理解:
- 编排层:负责接收任务、把长任务拆成短步骤、管理重试和退出条件;
- 模型层:通过 LiteLLM Proxy 统一接入不同模型,代码里不直接绑定某家厂商 SDK;
- 执行层:BrowserUse Agent 作为执行主体,把模型决策映射成 Playwright 的浏览器操作;
- 隔离层:AgentRun Sandbox 的容器方案,用来切分执行边界;
- 可观测层:保存每次运行的截图、日志、模型输入输出,用于事后审计。
这套分层的好处是,每一层都能独立替换。模型效果不好可以只改 Proxy 配置;执行环境崩了可以重建容器;任务需要调整只动编排层代码。浏览器层面出了问题,也可以单独升级 BrowserUse 或 Playwright 版本,而不会牵连其他模块。
2.2 容器、远程浏览器与VM,到底怎么选
沙箱落地的方案并不唯一。最常见的是纯 Docker 容器,把 Python 环境、Chromium、BrowserUse 都塞进同一个镜像,启动容器就是沙箱运行环境。它的优点是资源轻、镜像可复用、销毁重建成本低,适合接收临时任务,任务结束直接丢容器。缺点是对网络和内核资源隔离不够精细,如果跑的任务涉及敏感生产数据,容器默认共享宿主内核这一点需要额外安全加固。
远程浏览器池是另一种方案,比如把 Browserless 这类服务专门部署成一组浏览器实例,Agent 通过 CDP 协议连过去。好处是浏览器和业务代码彻底分离,扩展性更强,多个 Agent 并发不会互相抢资源;代价是要额外维护一套浏览器集群,网络链路多了远程开销,也在架构里多出一个需要监控的中间件。
虚拟机自然是隔离最彻底的方案,每个 Agent 独占一个完整系统内核,网络和文件系统都能通过虚拟机快照精确回滚。我现在的处理原则是:任务影响面小、频率高用容器;并发高、需要浏览器集群统一管理用远程浏览器池;涉及强敏感数据,或模型行为完全不可预测的探索型任务,就考虑一次性虚拟机。
2.3 BrowserUse与底层浏览器版本的匹配问题
BrowserUse 本身是一个挺依赖底层组件的框架,它把模型决策转成 Playwright 能执行的浏览器动作。如果你的环境分开升级,很容易出现一种情况:BrowserUse 升级到了新版本,但容器里的 Chromium 还是旧的,导致某些选择器定位、滚动逻辑、表单处理方式不兼容,运行时报各种莫名其妙的错误。
我的经验是不要只固定 browser-use 这个包版本,而是把 browser-use、playwright、chromium 三者的版本组合固定成一个整体,写进 requirements.txt 里做硬约束。构建镜像时不建议用带 latest 的标签去拉最新浏览器,直接锁一个已知能跑通的版本。
另外一个很现实的问题是容器内启动 Chromium 经常因为缺少系统动态库而失败。虽然 playwright install chromium 会装浏览器本体,但系统依赖还是需要额外安装。我自己在 Dockerfile 里一般用 playwright install --with-deps chromium 一步到位,省得逐个排查 libnss3、libatk、libgbm 这些依赖缺失的问题。
2.4 把“最小权限”原则落到Agent上
互联网行业常用的最小权限原则,在做 Agent 自动化的语境下同样成立。容器只是第一层壳,BrowserUse 进程本身还需要按最小权限去配置,而不是给一个 root 用户无限横行。
容器内应该单独创建一个非 root 用户,用来跑 BrowserUse。文件系统上只开放必要的读写目录,浏览器下载目录单独指定到一个空的临时目录,避免 Agent 把文件写到代码目录里。如果沙箱内有可供 Agent 调用的额外工具,比如发邮件、更新数据库、调用内部 API,这些工具的 action 必须做参数白名单校验,不能把整个服务能力都暴露给模型自由发挥。
网络策略也不要只在容器层做一次就结束。BrowserUse 运行过程中如果发现它反复尝试访问非预期域名,要立刻能通过日志发现。我这里会在编排层加一个简单的动作审计,记录 Agent 每一步在哪个页面上操作了什么,当访问域名或操作类型超出预设集合时自动终止任务。
3. 实操:搭一个最小可复现的Sandbox + BrowserUse环境
3.1 基础镜像与系统依赖准备
一个可以直接复现的沙箱镜像,建议从 Python 官方 slim 镜像开始,避免不必要的系统组件增大攻击面。Dockerfile 里除了安装依赖,还需要单独创建运行用户,并预留两个目录:一个用来放 BrowserUse 的浏览器用户数据,一个用来放任务输入输出。
下面是我当前用的 Dockerfile 骨架,你可以直接抄:
dockerfile复制FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
fonts-liberation \
libnss3 libnspr4 libdbus-1-3 libatk1.0-0 \
libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 \
libatspi2.0-0 libx11-6 libxcomposite1 libxdamage1 \
libxext6 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \
libcairo2 libasound2 \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash agent
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
&& playwright install --with-deps chromium
RUN mkdir -p /app/work /app/browser_data \
&& chown -R agent:agent /app
USER agent
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
requirements.txt 里至少应该锁定这几个包:
code复制browser-use==0.2.x
playwright==1.4x
langchain-openai==0.x.x
litellm==1.x.x
不要为了追新直接用最新版本,浏览器类的自动化项目,稳定性往往比新特性更重要,版本组合锁死之后,同类任务复现的成功率会高很多。
3.2 启动容器并规划目录与网络
镜像构建好之后,容器启动参数也需要刻意规划。我常用的启动命令大致如下:
bash复制docker run -it --rm \
--name browseruse-agent \
-v "$PWD/work:/app/work" \
-v "$PWD/browser_data:/app/browser_data" \
--network custom_agent_net \
--cpus=2 \
--memory=2g \
--pids-limit=256 \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=512m \
browseruse-sandbox:latest \
bash
几个参数的作用分别是:--read-only 让容器根文件系统只读,Agent 只能往挂载目录和 tmpfs 里写数据;--pids-limit=256 限制进程数量,防止浏览器进程开太多拖垮内存;--tmpfs 把临时目录放到内存里,且禁止执行二进制,能降低下载到临时文件后被执行的几率。
网络方面我建议单独建一个 bridge 网络,不要让容器直接走宿主机网络。如果需要出网白名单,可以在网络层前面再放一个代理容器,只开放目标站点和模型 API 的访问权限。BrowserUse 里如果不需要访问本地回环以外的服务,这个网络拓扑基本够用。
3.3 用BrowserUse写一个受约束的Agent
代码下面这段是一个最简单的 BrowserUse Agent 示例,模型接口走 LiteLLM Proxy,网络端口默认 4000:
python复制import asyncio
from langchain_openai import ChatOpenAI
from browser_use import Agent
async def main():
llm = ChatOpenAI(
model="gpt-4o",
base_url="http://127.0.0.1:4000",
api_key="sk-local",
temperature=0,
)
agent = Agent(
task=(
"访问 http://admin.local/tasks "
"筛选出状态为 pending 的第一条记录,进入详情页,"
"把状态改为 processing。"
"不要点击删除、清空、导出这类危险按钮。"
),
llm=llm,
max_steps=15,
)
history = await agent.run()
print("任务结束")
if __name__ == "__main__":
asyncio.run(main())
这里有几个实操层面的细节。temperature=0 是必设项,模型在操作浏览器时不需要创造性,只需要稳定复现合理动作。任务描述里我故意加了一句“不要点击删除、清空、导出”,从实际效果看,先显式排除高风险动作比只描述目标更能约束模型行为,模型往往会在决策时主动避开这些按钮。max_steps 一定不能省,没有它,模型一旦陷入循环就会把 token 消耗打满。
BrowserUse 运行结束后会返回一个 history 对象,里面保存了每一步的模型输出和截图。把这些截图归档到宿主机,就是之后的审计材料,模型做了什么、每一步是什么意图,都可以回溯。
3.4 运行记录与可审计输出
很多人把 Agent 跑通就结束了,其实在沙箱化执行里,记录和审计才是收尾的核心。我每次任务结束都会把容器内 /app/work 下的文件统一复制到宿主机按日期命名的目录里,除了常规日志,还会保留 BrowserUse 返回的历史对象序列化结果,里面至少包含每个步骤的模型原始输出、目标 URL、动作类型。
截图保留也有讲究。我会把截图按 step 序号重命名,而不是让框架用默认时间戳命名,因为时间戳在排查时很难一眼对应到具体的操作顺序。配合编排层记录的 action 日志,现场还原才够精确。
考虑到容器是临时创建的,任务完成后容器销毁,如果忘了把这些运行产物复制出来,审计数据就和容器一起没了。我习惯在编排脚本最后加一步归档,不论 Agent 运行成功还是异常退出,都先把 history.json 和 screenshots/ 目录同步到宿主机,再做容器清理。
4. 模型接入与任务编排:LiteLLM Proxy与短任务拆分实践
4.1 用LiteLLM Proxy统一模型入口
BrowserUse 需要模型做决策,但要接入不同模型时,直接在代码里切换厂商 SDK 是很笨的做法。我现在的做法是统一走 LiteLLM Proxy,给 BrowserUse 一个兼容 OpenAI 格式的 base_url,模型切换只改代理配置,业务代码不动。
一个最小配置大致长这样:
yaml复制model_list:
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: claude-3-5-sonnet
litellm_params:
model: anthropic/claude-3-5-sonnet
启动代理之后,BrowserUse 代码里的 base_url 指向 http://127.0.0.1:4000,模型名写成 gpt-4o 或 claude-3-5-sonnet 就好。想对比两个模型在做同一类 BrowserUse 任务时的准确率,只需要在代理配置里加一条模型记录,然后换一下 ChatOpenAI 的 model 参数,不需要改 Agent 逻辑。
这个环节带来的收益是成本可观测。LiteLLM Proxy 会记录每一次模型调用的 token 数量,结合任务步骤数,我能大致估算出单个自动化任务的模型成本。对需要长期跑后台任务的人来说,这个数字比预想的重要,它直接决定了你后续要不要从高精度大模型切到便宜一些的模型来降低成本。
4.2 拆成短任务,不要Agent长跑
跑长任务的天然问题有两个:一是上下文过长导致模型注意力分散,容易出现前半段执行正确、后半段开始犯低级错误的状况;二是单次任务如果二十多步才完成,中间任何一步页面结构不符合预期,后面的步骤都会连锁偏移。
我在实践中把复杂链路拆成四个短阶段:先访问列表页并输出符合条件的记录列表,再根据列表里的目标记录进入详情页,然后在详情页执行状态变更,最后回到列表页校验结果。每个阶段是独立的 BrowserUse Agent 任务,前一个阶段的输出结构化保存成 JSON,作为下一个阶段的输入。
拆完之后的好处非常直观。失败定位不再是“整个 Agent 跑挂了”,而是精确到“第二阶段没有找到目标记录”。重试也更灵活,只重新跑失败的阶段就行,不必让 Agent 重头开始。token 成本也会下降一大截,因为每个子任务的系统提示词都很短,模型不需要反复带着前二十步的截图和动作去推理。
4.3 把约束写成可复用的任务描述库
如果你手上有多个类似的 BrowserUse 任务,会发现每个任务里的“不要点什么”“操作前先确认什么”“遇到弹窗怎么处理”其实高度重复。每次都在任务 prompt 里临时写一遍,不仅繁琐,还容易漏掉某条边界。
我在代码里维护了一个任务描述模板库,把常见的安全约束和业务规则抽成字符串常量,拼装成最终的 task。比如所有涉及后台状态变更的任务,都会统一拼接上这样一段约束:只能操作筛选结果中的记录,不要清除筛选条件,不要切换页面视图,如果出现二次确认弹窗就直接终止任务。
这样一个常量库的维护成本低,但收益很实在。新任务写起来只是几句业务描述加引用几个公共约束,不需要每次重新发誓“我这次一定要把规则写全”。同时约束模板统一也意味着安全规则可以被集中评审,而不是散落在各个 Agent 的 prompt 里没人维护。
4.4 LLM Wiki思维:约束和知识要沉淀
最近看到一个经验分享,有人按 Karpathy 提到的方式维护了一份 LLM Wiki,把日常使用大模型时积累的提示词技巧、易错点、已知坑位都整理成一个持续更新的文档库。这个思路放到 BrowserUse 任务里同样合适,而且价值会被放大。
因为 BrowserUse 的本质是人在给 Agent 编写行为准则,每跑一次任务,你都会发现一种新的模型误判方式或者一种页面处理的更好写法。把这些经验按场景沉淀下来,比如“模态框关闭按钮的稳定定位方式”“分页加载后的等待策略”“登录态失效的判断特征”,后续写新任务时直接查库,远比重新踩一遍坑高效。
我现在每个任务目录里都会放一份 markdown 形式的运行笔记,记录这个任务改了哪些约束、踩过什么坑、验证过哪些页面状态。时间久了它就成了团队共用的 Agent 运行知识库,比任何一个人脑子里的经验都可靠。
5. 常见问题排查与稳定性优化
5.1 我踩过的典型坑速查表
这里把我在实际运行中踩过的问题整理成了表格,每一类都给出了排查方法,可以直接对照:
| 现象 | 常见原因 | 排查方式 | 解决思路 |
|---|---|---|---|
| 容器内 Chromium 启动报缺少动态库 | 系统依赖没有装全 | 看报错里缺失的 .so 文件 | 改用 playwright install --with-deps chromium |
| Agent 反复点击同一个按钮不进入下一步 | 模型上下文过长导致决策漂移 | 打开历史截图看是否点击后无反馈 | 缩短单任务步骤,增加等待条件或换更强模型 |
| 登录态频繁失效 | storage_state 过期 | 看 Agent 是否访问了登录页 | 定期刷新持久化 Cookie,单独维护登录态 |
| LiteLLM Proxy 返回超时 | 上游模型响应过慢 | 查看代理日志里的耗时 | 加超时重试,换低延迟模型 |
| 任务跑到一半被弹窗卡住 | 弹窗未被模型识别为需要处理 | 打开中间截图检查 | 在 task 描述中显式增加弹窗处理规则 |
| 浏览器下载了未知文件到容器 | 下载目录没有单独隔离 | 查看容器文件系统 | 把下载目录固定到 tmpfs,文件生命周期短 |
5.2 排查思路:先看Agent意图还是看DOM状态
定位 BrowserUse 任务失败时,我建议先分清楚是模型的意图决策错了,还是浏览器执行层没等到预期状态。这两个原因的表现很相似,但排查方向完全不同。
先说意图层问题。回看 history 对象里每一步的 model_output,如果模型的下一步动作描述和你想让它做的动作明显不一致,比如本该点击详情却去点了筛选按钮,问题出在任务描述或页面信息过载上。这时需要简化 task 文字,明确目标要素,或者把页面上的无关元素通过脚本隐藏掉,减少模型误判的概率。
再说执行层问题。如果模型输出是对的,比如它明确说要点击某个文本按钮,但页面截图显示点击并没有生效,这时要怀疑页面状态没有加载完成。解决方案是让 BrowserUse 在关键操作前增加显式等待,或把操作拆成两次点击:第一次聚焦目标元素,第二次再执行真正的点击,能够规避很多页面交互时序问题。
5.3 成本控制和并发执行建议
成本控制这件事其实应该在架构层就考虑。BrowserUse 的每次操作都会调用模型,而模型又要读取页面截图来理解当前状态。截图分辨率越高、任务步骤越多,token 消耗增长越明显。我目前的建议是浏览器 viewport 不要设置得过大,够页面正常布局就行,太高的分辨率只会让模型多读很多无用像素。
并发执行要谨慎。如果你同时起多个容器跑 BrowserUse,每个容器内部是一个浏览器实例,内存开销通常会超过直觉。一个 Chromium 进程跑起来占几百 MB 内存很正常,两三个并发任务就能把一个 2G 内存的容器卡死。我会用任务队列而不是简单多进程来解决并发:编排层把任务排进队列,同时最多跑两个容器,其余任务排队,这样不仅资源可控,出现事故时的定位范围也小很多。
另外要强调一点,模型 token 计费和浏览器性能问题只是表象,真正的瓶颈往往是 Agent 的失败重试。一个任务失败后不断重试,可能让成本翻三倍。给每个重试设置独立的最大步数和明确失败原因,比让 Agent 无限次自我修正要安全得多。
6. 最后想说的个人经验
6.1 让Agent在“可破坏”的环境中自由发挥
跑完这段时间,我最深的体感是:BrowserUse 这类工具的上限并不取决于模型有多聪明,而取决于你给了它多大的试错空间。模型的能力确实在快速提升,该点哪个按钮、该填哪个表单,大部分时候它都能判断对,但真正消耗你精力的永远是那 5% 的低概率误判。把沙箱权限收紧到“最坏情况下也只是损坏一个可以随时重建的容器”,你才敢放开手让 Agent 去做事。
6.2 少一点“自由发挥”,多一些“流程护栏”
如果你准备在业务里长期使用这套组合,我的建议是不要追求一个全自动、零干预的 Agent 系统。比较务实的路线是让 Agent 完成步骤明确、状态可检验的具体环节,把删除、付款、审批这类高风险动作留在人类手里。一次运行只有一两个安全闸门,不会牺牲太多效率,却能在无数个深夜救你一次。
这个方向我已经反复验证过,也打算继续沿用下去。先把 BrowserUse 和 AgentRun Sandbox 跑通,把流程约束沉淀成团队的知识库,再慢慢扩大自动化范围。路要一步步走,护栏一定要先建好。
