高可用架构

我们如何构建安全可扩展的智能体沙箱基础架构

导读:给 AI 智能体执行代码的权限,等于把服务器底裤交出去?本文分享了 Browser Use  运行百万级智能体的架构演进。他们放弃传统的工具隔离,转而采用“基于控制面的微型虚拟机”来彻底隔离智能体。

从 AWS Lambda 走向基于控制面架构的 Unikraft 微型虚拟机

演进历程

在 Browser Use,我们运行着数百万个网页智能体(Web Agents)。起初,我们使用 AWS Lambda 运行纯浏览器智能体,其优势在于每次调用都是隔离的、扩展是即时的,且无需担心密钥安全。

随后,我们增加了代码执行功能。智能体可以编写并运行 Python 代码、执行 Shell 命令、创建文件。我们将此构建为一个被智能体视为“工具”的隔离沙箱。安全性表现良好:代码在沙箱中运行,而非在后端运行。

但问题在于:智能体循环(Agent Loop)仍然与我们的 REST API 运行在同一个后端。 重新部署 API?所有运行中的智能体都会崩溃。智能体占用大量内存?API 响应变慢。两种截然不同的工作负载在共享同一个进程。

两种模式

当一个智能体可以执行任意代码时,它理论上可以访问机器上的任何内容:环境变量、API 密钥、数据库凭据、内部服务。因此,它必须与你的基础设施和秘密(Secrets)隔离。实现这一点有两种方式:

  • 模式 1:隔离工具(Isolate the tool)。 智能体运行在你的基础设施上。危险操作(代码执行、终端访问)在独立的沙箱中运行。智能体通过 HTTP 调用沙箱。代码在没有泄露风险的地方运行。
    Image
  • 模式 2:隔离智能体(Isolate the agent)。 整个智能体都运行在一个没有任何密钥的沙箱中。它通过一个持有所有凭据的**控制面(Control Plane)**与外界通信。
Image

智能体变成了“即用即弃”的。 没有可窃取的秘密,没有需要保留的状态,你可以随时杀死它、重启它、独立扩展它。控制面掌握着“真相”。

我们最初采用了模式 1,后来转向了模式 2。


沙箱环境

相同的容器镜像在任何地方都能运行。在生产环境中,它作为 Unikraft 微型虚拟机 (micro-VM) 运行;在本地开发和评估(Evals)中,它作为 Docker 容器运行。通过一个简单的配置开关 (sandbox_mode: 'docker' | 'ukc') 即可控制环境。

生产环境中的 Unikraft

每个智能体拥有独立的 Unikraft 微型虚拟机,启动时间不到一秒。我们通过 Unikraft Cloud 的 REST API 在 AWS 的专用裸金属服务器上进行配置。

沙箱仅从外界接收三个环境变量:SESSION_TOKEN、CONTROL_PLANE_URL 和 SESSION_ID。没有 AWS 密钥,没有数据库凭据,也没有 API 令牌。

Unikraft 提供了开箱即用的 “缩容至零”(Scale-to-zero) 功能。当沙箱空闲时,VM 会挂起;当下一个请求进入时,它会立即恢复。在查询间隙,沙箱几乎不产生费用,但能瞬间唤醒执行后续任务。

我们跨多个 Unikraft 区域(Metros)分布沙箱,以防止单一区域成为瓶颈。

开发与评估中的 Docker

在本地和评估流水线中,相同的镜像作为 Docker 容器运行。同样的镜像、同样的入口点、同样的控制面协议。我们可以在开发笔记本上运行完全相同的智能体,并发启动数百个进行评估,最后部署到生产环境的 Unikraft。


安全加固

在运行任何智能体代码之前,沙箱会执行以下操作:

  1. 仅字节码执行: 在 Docker 构建期间,我们将所有 Python 源码编译为 .pyc 字节码,然后删除所有 .py 文件。智能体框架代码以 root 身份加载到内存中,一旦加载完成,源码即消失。
  2. 权限剥夺: 入口点以 root 身份启动(读取 root 拥有的字节码所需),然后立即通过 setuid/setgid 降权为 sandbox 用户。此后,一切都在非特权下运行。
  3. 环境清理: 将 SESSION_TOKEN、CONTROL_PLANE_URL 和 SESSION_ID 读取到 Python 变量后,我们从 os.environ 中彻底删除它们。即使智能体检查环境变量,它们也不复存在。况且,在沙箱的网络环境之外,这些令牌毫无用处。VM 位于私有 VPC 中,除了与控制面通信外没有任何权限。

控制面如何工作

可以将控制面看作一个代理服务。沙箱无法直接访问外部世界,每个请求都必须经过控制面。

  • 需要调用 LLM?走控制面。
  • 需要上传文件到 S3?走控制面。 这是智能体与其 VM 之外的世界交流的唯一途径。

它是一个无状态的 FastAPI 服务。来自沙箱的每个请求都携带 Bearer: {session_token} 请求头。控制面通过令牌查找会话,验证其有效性,并使用真实凭据执行操作。

LLM 代理

对于每次 LLM 调用,沙箱只发送新消息。控制面在数据库中保存完整的对话历史,在每次调用时重构历史,并将完整的上下文转发给供应商。这保持了沙箱的无状态性:你可以杀掉它并启动一个新沙箱,对话依然能从中断处接续。

控制面还负责强制执行成本限制并处理计费,让沙箱只专注于任务本身。

通过预签名 URL 进行文件同步

沙箱有一个 /workspace 目录用于读写文件。文件同步服务会监控更改并定期同步到 S3,但沙箱永远看不到 AWS 凭据。相反,它向控制面索取预签名 URL:

  1. 沙箱检测到 /workspace 中的文件变动。
  2. 沙箱调用 POST /presigned-urls 并发送文件路径。
  3. 控制面生成受限的 S3 上传预签名 URL。
  4. 沙箱使用这些 URL 直接将文件上传到 S3。

下载过程则反向进行。沙箱获得了受限的 S3 访问权限,而无需持有任何 AWS 凭据。

网关协议 (Gateway Protocol)

在沙箱内部,智能体通过“网关”协议与控制面通信:

classAgentGateway(Protocol):
asyncdefinvoke_llm(self, new_messages, tools, tool_choice) -> LLMResponse: ...
asyncdefpersist_messages(self, messages) -> None: ...

在生产中,ControlPlaneGateway 发送 HTTP 请求;在开发中,DirectGateway 直接调用 LLM 并将历史保留在内存中。智能体代码感知不到差异。相同的接口,相同的行为,不同的后端。


扩展性

控制面是无状态的:验证令牌、执行任务、返回结果。

  • 需要更多智能体?启动更多沙箱。
  • 需要更高吞吐量?在负载均衡器后增加控制面实例。

每一层都根据自身的瓶颈进行扩展。我们的后端运行在基于 ALB 的私有子网 ECS Fargate 上,控制面根据 CPU 利用率自动缩放。沙箱通过 Unikraft 独立扩展,每个会话拥有自己的 VM,并且由 Unikraft 负责跨区域(metros)的调度。

Image

总结

对可以执行代码的智能体进行沙箱化有两种方法:隔离工具(将代码执行放在沙箱中,智能体保留在后端)或隔离智能体(将整个智能体放入沙箱,通过控制面与外界通信)。

我们选择了模式 2。控制面持有所有凭据并充当一切事物的代理:LLM 调用、文件存储、计费。沙箱只接收三个环境变量,无权访问其他任何内容。它在生产中是 Unikraft 微型虚拟机,在开发中是 Docker 容器。无论哪里都运行相同的镜像。

权衡之处在于:每次操作多了一次网络跳转,且需要部署三个服务而非一个。但在实践中,这种延迟相对于 LLM 的响应时间几乎可以忽略不计,而运维复杂度也是团队完全可以胜任的。

核心启示:你的智能体里不应该有任何值得偷的东西,也不应该有任何值得保留的状态。

https://x.com/larsencc/status/2027225210412470668

参考阅读