---
url: https://cubesandbox.com/zh/blog/posts/2026-08-13-lexmount-browser-agent.md
description: >-
  Agent 浏览器运行时是一个复合工作负载。它对 AI Agent
  沙箱的运行有四个核心层面的硬需求：出站网络能否连到真实互联网、批量拉起时的启动时序稳不稳、每个沙箱的运行时状态能否被动态注入、真实容量能不能被准确感知。Lexmount
  团队在把浏览器运行时接入 Cube Sandbox 的实践中，逐一碰上并解决了这四个问题。
---

# 把浏览器搬进 Agent 沙箱：Lexmount 在 CubeSandbox 上的一线实战

**编者按｜** Agent 浏览器运行时是一个"复合工作负载"。它对 AI Agent 沙箱的运行有四个核心层面的硬需求：出站网络能否连到真实互联网、批量拉起时的启动时序稳不稳、每个沙箱的运行时状态能否被动态注入、真实容量能不能被准确感知。

任何一条无法实现，Agent 的行为都会开始漂移。Lexmount 团队在把浏览器运行时接入 Cube Sandbox 的实践中，逐一碰上并解决了这四个问题。本文是他们的一手技术复盘，也顺带回答了一个更本质的问题：轻量沙箱在追求启动性能时究竟放弃了什么，业务侧又该如何补齐。

## 一、应用背景和分层策略

### 1.1 Lexmount 业务背景

Lexmount 自研的 Agent 专属浏览器运行时（Lexmount Insight Flow），核心的应用场景是：让 AI 智能体大规模、稳定地执行网页任务。这个业务形态天然对底层沙箱提出了几条"必须成立"的要求——**能真实访问互联网**（浏览器任务的前提）、**能批量并发拉起**（多用户并行 / 强化学习 batch rollout）、**能给每个沙箱注入不同的运行时状态**（多租户 API Key、任务上下文）、**能准确知道单机能起多少个**（不能空跑虚高）。

选型阶段，我们在开源社区寻找"能轻量起大量沙箱"的工具。实测对比过 OpenSandbox 和 Cube Sandbox 后，我们发现 Cube 的资源占用更低，因此最终选择了后者。从 v0.1.0 跟进到 v0.5.1，围绕上面四条边界，我们分别踩到了公网不通、模板创建失败、环境变量丢失、资源虚假超卖四个坑。

### 1.2 分层策略：不是所有 Agent 都要整个塞进沙箱

在讲具体问题之前，先分享我们对沙箱化的整体路径策略——它决定了后面四个问题各自出现在哪条链路上。我们把沙箱化拆成两条路径，由业务编排层按风险等级、资源占用和业务形态做策略路由。

**第一条路径只隔离 Bash。** 通用对话、问答、轻量编排这类低风险、短执行的场景，Agent Loop 本身保留在业务编排层，只有 `Bash/exec` 工具调用进入沙箱执行。沙箱用完即弃，内部无状态。这条路径的优势是轻——沙箱生命周期短、高频启停、不占常驻资源。

**第二条路径把整个 Agent Runtime 放进沙箱。** 代码开发、浏览器自动化、长时任务这类高风险、重资源、需要常驻或原生交互的场景，Agent 常驻进程整体进入沙箱，享有独立 CPU/内存和持久工作区。这条路径优势是隔离彻底——Agent 在沙箱内有完整运行环境，随时启停，和常驻微服务相比资源差异不大。

两条路径共享同一套 Cube 控制面，由控制面按需创建或复用轻量/完整 Sandbox，并统一编排生命周期。后续还有强化学习 batch rollout 的计划——批量短生命周期沙箱，这也是 Cube 这类轻量沙箱最契合的场景之一。

## 二、出站网络：让沙箱真的能"打开网页"

对浏览器运行时来说，网络是第一硬约束。Agent 打开的每个页面、每次调用的每个 API，都要从沙箱里出到公网。Cube v0.1.0 上手第一天，我们就撞上了这条边界。

Cube v0.1.0 在五一节前发布，创建的沙箱连不上公网。我们换了多个环境都复现，排除了自身环境问题。虽然社区在节后就发布了修复版，但升级后新坑来了——提示创建成功，但模板偶尔建不出来，偶发建出来还是无法联网。

我们写了很多测试脚本和用例，确认 DNS 和直连公网 IP 都超时，先排除了域名解析问题。接着抓包，发现 SNAT session 已经建立，但连接卡在 SYN-SENT，说明 TCP 三次握手没完成。进一步检查宿主机默认路由和出口网卡，均正确。到这一步，IP 层和路由层都没问题，我们开始怀疑 IP 层以下。

让 AI 帮忙检查了一遍 network-agent 的源码，发现了一个可疑点：`getGatewayMacAddr()` 这个函数会把网卡上"第一个 reachable 邻居"当作网关。在多邻居环境下，这个 MAC 可能并不是默认网关的 MAC——如果选错了，SYN 包虽然从网卡发出去了，但在 L2 层被送到了错误的 next hop，根本到不了网关，连接自然超时。

为了实锤这个判断，我们用 `tcpdump` 抓了出口网卡的包。结果很清楚：SYN 确实从 `enp3s0` 发出了，但目的 MAC 属于一个普通邻居，而不是默认网关。

因此，我们的修复思路是改 `getGatewayMacAddr()` 的逻辑：先从接口默认路由解析出 gateway IP，再查该 IP 对应的 neighbor entry，确保选到的是真正的网关 MAC，而不是碰巧排在第一位的 reachable 邻居。同时补了两个测试用例：多邻居场景下非网关 MAC 排在前面的情况，以及缺失默认路由的情况。这个 PR（[#224](https://github.com/TencentCloud/CubeSandbox/pull/224)）已被合并。排查过程中，我们还额外发现了一个 MAC 地址被覆盖的问题，也单独提了修复。

**这条边界踩通之后**，第二条路径（整个 Runtime 塞进沙箱）才真正具备了跑浏览器任务的前提。但很快，下一条边界就来了——不是一个沙箱能不能上网，而是**一次能不能拉起很多个沙箱**。

## 三、批量启动：拉起一百个沙箱前，先解决"进程已起 ≠ 就绪"

Agent 浏览器运行时的第二个业务诉求是并发。一个用户会同时开好几个页面任务、多个用户会同时使用 Agent，未来 RL rollout 还要批量并发拉起短生命周期沙箱。这些场景共同的特点是：**一台机器上会短时间内密集触发沙箱创建请求**。

密集创建会自然导向一个问题：启动时序竞态。我们遇到的第二个坑也是在这里。当时的报错信息是 `unknown service cubelet.services.images.v1.Images`，看起来像是 Cubelet 本身的问题。

最初我们确实优先怀疑 Cubelet——版本不匹配？Images 服务未注册？Cubelet 初始化异常？但检查发现 Cubelet 进程和 gRPC 端口都已经起来了，问题只在重启或整套 one-click 启动后出现，单独等一段时间或调整启动顺序后可恢复。因此开始怀疑依赖初始化竞态。

对照启动脚本，我们找到了根因。旧逻辑是拉起 network-agent 后立即启动 Cubelet，直到 Cubelet 启动以后才用 `/healthz` 检查 network-agent。而 Cubelet 初始化网络插件只等 30 秒——network-agent 启动较慢时会导致 Cubelet 初始化不完整，后续 Images 服务不可用。

因此，"network-agent 没起好"不是进程没启动，而是进程已经存在、但 `/readyz` 尚未通过，Cubelet 就被过早拉起了。这也解释了为什么看起来是 Cubelet 报错，根因却在上游依赖未就绪。`/healthz` 检查的是进程存在，`/readyz` 检查的是依赖服务注册完成——两者不能互相替代。

我们的修复方案是把启动顺序改过来：先等 network-agent 的 `/readyz` 通过，再启动 Cubelet 及后续服务。同时把脚本和 Cubelet 内部等待时间都延长到 120 秒，新增了 `NETWORK_AGENT_READY_TIMEOUT` 环境变量供部署方按机器性能调优。这个 PR（[#304](https://github.com/TencentCloud/CubeSandbox/pull/304)）已被合并。

**表面看这是一个部署脚本问题**，但放到浏览器运行时的业务场景里，它是一个"能不能规模化"的问题——如果批量拉起时依赖服务还没注册就被调用，创建就会大批量失败。业务侧的应对是：所有依赖跨进程 gRPC 的组件，就绪判定统一以 `/readyz` 为准，而不是 `/healthz` 或进程存在。

## 四、动态状态：多租户 API Key 该怎么塞进沙箱

浏览器运行时的第三个诉求是"每个沙箱的运行时状态可以按需注入"——不同用户、不同任务的 API Key、上下文标签、下游服务地址都不同，我们不可能给每种组合都做一个模板。理想情况下：控制面按请求生成一组 env，创建沙箱时传进去，Agent 起来后就能读到。

但我们撞上了 Cube 的一条架构边界。往沙箱里直跑 Agent，需要传 API Key 等环境变量启动。但接口传一组 env 进去，根本没生效。读源码后发现：`CreateSandboxRequest.containers` 被硬编码为 `vec![]`，传入的环境变量值在这一步被掐掉了。

我们提了 PR（[#634](https://github.com/TencentCloud/CubeSandbox/pull/634)），尝试从 CubeAPI 的初始化链路入手修复，把 `env_vars` 接入 container spec。但维护者反馈"与原架构设计不兼容"，PR 被关闭。

被关闭的技术理由，后来我们理解了。Cube 的沙箱创建走的是 snapshot restore，而不是重跑 entrypoint。模板的运行时进程在模板制作阶段已经启动并完成了内存快照，沙箱创建是从这个快照恢复，而不是重新执行 entrypoint。因此，PID-1 的 `Process.Env` 改动影响不到 snapshot 时已经存在的进程——那些进程保留的是快照时刻的环境，不会重新读取 per-sandbox 注入的变量。

envd-backed 的命令执行（`commands.run` / `run_code`）走的是另一条路：envd 的 `process.Start` 从一个 in-process 的 defaults.EnvVars map 中读取并注入环境变量，这个 map 由 envd 的 `/init` endpoint 填充（由 [#566](https://github.com/TencentCloud/CubeSandbox/pull/566) 处理）。两条路径互补但不重叠。

在确认这不是底层缺陷，而是 snapshot restore 的架构约束后，我们继续采用了在早期的实践过程中已经沉淀出来的解决办法，即采用"控制面代理、沙箱内执行"的两段式设计：控制面对外提供 Init API，完成鉴权、身份字段保护、目标实例解析和失败重试；真正的 `/v1/init` 运行在 CubeSandbox 内的 Adapter 或 Agent 业务进程中，通过内存配置更新、同进程环境更新或重启子进程触达 Agent。幂等由 Sandbox 控制面和内部 Adapter 或 Agent 业务进程共同保证。

回头看这个架构约束，我们认同维护者的判断。snapshot restore 带来的启动时延降低是实实在在的——热启动 P95 在亚秒级，这恰恰是浏览器运行时能"批量并发"的前提。而 env 注入本身是偏业务的场景，由业务侧在恢复后显式注入，比依赖 entrypoint 重跑更可控。**这是一条典型的"上层性能换下层灵活性"的取舍**——Cube 选择在启动性能上激进，把状态注入的责任明确交回业务侧；业务侧知道边界之后，用两段式设计补上，链路依然清晰。

## 五、真实容量：从"能起 N 个"到"敢用 N 个"

浏览器运行时是重资源工作负载——每个沙箱里都有一个完整 Runtime 加浏览器实例。业务方最关心的一个数字是：一台机器上到底能跑多少个。我们最早给出的数字，事后来看是虚高的。

早期版本（v0.2.x）在一台 16C/32G 的裸金属服务器上，我们尝试不停创建 Cube，得到了一个比较大的数值。升级新版本后发现创建 30+ 就触发了资源限制报错，和之前的认知差别较大。

排查过程中，开始主要怀疑自己的控制面和生命周期管理模块有 bug。排除自身控制面和生命周期管理逻辑的问题后，我们开始梳理 Cube Sandbox 的代码，在 AI 辅助下定位到根因：v0.2.x 的 one-click 部署里，CubeMaster 使用脚本向 Redis 写了一组 mock 的资源消耗指标，这让 CubeMaster 对资源消耗的感知失效——看起来还有很多余量，实际已经满了。v0.3.0 版本已经修复了该问题。

这个坑本质是在为历史上的测试不够严谨买单。此前"不停创建空跑沙箱"得到的容量数字是虚高的——没有真实流量，沙箱不真正占资源，自然能起很多。升级后 mock 指标被修掉、资源锁真正生效，虚高的数字才被打回原形。

我们后来总结：对于容量评估，最直接的信号是实际创建请求能否成功，需要综合 CubeMaster 的实时节点健康状态、剩余配额、宿主机的 `free/top` 等信号指标，进行交叉验证，形成系统性的容量监控和压测结论，这些单独看都有可能成为"不可信"的信号。

这一条对我们的启示不止是"修一个上报 bug"——它反过来定义了我们后续做容量规划的原则：任何单一信号都不能作为容量结论的依据；容量必须在业务负载真实压上去的条件下测。对浏览器运行时来说，这意味着测试用例必须包含真实页面加载、真实的 Agent 请求，而不是空跑一批沙箱看能起多少。

## 六、当前状态与后续规划

当前我们在生产前验证阶段停留在 v0.5.1，计划后续体验 v0.6.0 的 K8s / Volume 能力。后续会进客户私有化场景，强依赖 K8s 兼容（客户基建用 K8s），因此我们对于 v0.6.0 的 K8s / Volume 验证是下一步重点。

后续规划中还有强化学习 batch rollout 的批量沙箱需求——大量短生命周期沙箱并发创建、执行、回收。这也是 Cube 这类轻量沙箱和我们业务场景的契合点。

## 总结一下可供参考的经验

四个方向摸索下来，有几点感受值得沉淀，也希望能给正在评估或使用 Cube 的同行提供参考：

* **出站网络问题先怀疑 L2。** 当 SNAT session 建立但连接卡在 `syn_sent`、且宿主机路由正确时，问题大概率在 L2 next hop 选择上——`tcpdump` 直接看 SYN 的目的 MAC 是不是网关 MAC，是最快的实锤方式。这条经验对基于 microVM + 自定义网络平面的沙箱都适用，不局限于 Cube。
* **批量场景下，`/healthz` 不能替代 `/readyz`。** 单机低并发时 30 秒足够，一旦拉起频次上来，依赖服务未注册就被调用会大批量失败。所有跨进程依赖，就绪判定统一以 `/readyz` 为准。
* **容量数字不能空跑测出来。** 早期资源上报 bug 叠加"空跑无真实消耗"，会让容量数字被显著高估。容量评估必须在业务负载真实压上去的条件下做，且需要多源信号交叉验证。

Agent 浏览器运行时是一个复合工作负载，它对底层沙箱的压测比大多数场景都苛刻。我们从 v0.1.0 一路跟到 v0.5.1，每一条边界都读到了源码根因，也提了自己的修复。希望这份记录对同样在做 Agent Runtime 的团队有所参考。
