1. 从“一天 300 万沙箱”说起:这个数字到底意味着什么
第一次看到“一天创建 300 万个沙箱”这个量级,我的反应不是“哇好厉害”,而是下意识开始算账:一天 86400 秒,300 万个沙箱意味着平均每秒要拉起接近 35 个隔离环境。如果这些沙箱是给 Agent 做代码执行、工具调用、rollout 采样用的,那它背后压着的根本不是“能不能跑起来”的问题,而是“整套调度、隔离、回收、观测链路能不能扛住”的问题。
先把概念对齐一下。这里说的沙箱,不是支付宝沙箱支付那种给开发者做联调用的模拟环境,而是 Agent 训练与推理里用来安全执行模型生成代码、命令、工具调用的隔离运行时。Agent 和普通对话模型最大的区别在于:它不只是“说”,它还要“做”——写文件、跑脚本、调接口、读结果、再根据结果决定下一步。只要它要“做”,就必须有一个地方让它去做,而且这个地方不能把宿主机、不能把训练集群、不能把别人的数据搞坏。这个地方就是沙箱。
所以“一天 300 万沙箱”翻译成人话就是:Agent 训练管线里,代码执行/工具执行这一环的并发吞吐和生命周期管理,已经变成了和 GPU 算力同等重要的基础设施瓶颈。很多人做 Agent 项目时把 90% 的精力花在 prompt、框架、模型选型上,最后卡死的地方往往是沙箱:启动慢、并发上不去、环境脏了、执行超时、结果收不回来、日志对不上。这篇就围绕这个场景,把沙箱在 Agent 训练里的定位、架构选型、实操要点和踩坑经验一次讲透。
适合谁看:正在做 Agent 开发、Agent 训练、rollout 采样、代码解释器类产品的同学;也适合刚接触 agent 框架、想知道“沙箱到底该怎么搭”的入门者。下面所有内容都基于常见工程实践展开,涉及具体参数的地方我会把计算过程写出来,方便你按自己的规模换算。
2. 沙箱在 Agent 训练链路里到底站在哪个位置
2.1 先分清 harness、agent、沙箱三者的关系
热词里反复出现 deepseek harness、hermes agent、harness 和 agent 区别,这几个词经常被混着用,我按工程视角给一个清晰的分层:
- Agent:决策主体。它接收任务,规划步骤,决定“下一步该调用哪个工具、传什么参数”。它是大脑。
- Harness:承载体和编排层。它负责把 Agent 的大脑和外部世界连起来——管理对话历史、拼装工具描述、解析模型输出、把工具调用分发出去、把结果塞回上下文。你可以把它理解成“Agent 的骨架和外设总线”。
- 沙箱:执行末端。当 harness 决定要执行一段代码或一条命令时,真正干活的那个隔离环境就是沙箱。
用生活类比:Agent 是司机,harness 是整辆车(方向盘、油门、仪表盘、车载电脑),沙箱是那条专门划出来练车的封闭跑道。司机再聪明,跑道不够、跑道塌了,车也跑不起来。很多人问“harness 和 agent 区别”,一句话:agent 是策略,harness 是执行策略的框架,沙箱是框架伸出去的那只手。
2.2 rollout 阶段为什么最吃沙箱
Agent 训练里有个关键环节叫rollout:让当前策略模型在真实或模拟环境里跑一遍任务,收集轨迹(trajectory),再用这些轨迹去更新模型。和传统 RL 不同,Agent 的 rollout 往往包含大量工具调用和代码执行,每一步执行都要一个干净的、可控的、可复现的环境。
这就解释了为什么沙箱量会爆炸。假设一次 rollout 平均要执行 8 次代码,一次训练 batch 有 4096 条轨迹,那单轮就是 3 万多次执行。如果为了并行加速,每条轨迹的每一步都开独立沙箱,量级直接上百万。再叠加多轮迭代、多任务类型、失败重试,一天 300 万完全在合理区间。
注意:沙箱数量不等于并发数量。300 万是“创建次数”,真正的并发峰值可能只有几千到几万。区分这两个指标,是容量规划的第一步,很多人一上来就按 300 万并发去设计,直接把成本算崩。
2.3 沙箱要同时满足的四个矛盾需求
做 Agent 沙箱最难受的地方在于,它要同时满足几个互相打架的需求:
| 需求 | 说明 | 冲突点 |
|---|---|---|
| 隔离性 | 不能影响宿主机和其他任务 | 强隔离通常更重、更慢 |
| 启动速度 | rollout 要高频创建 | 快启动往往隔离弱 |
| 环境一致性 | 同任务结果可复现 | 缓存复用会带来脏环境 |
| 成本 | 百万级创建要控成本 | 强隔离+快启动+干净=贵 |
这四个需求没有银弹,只能按场景做取舍。训练用沙箱和线上生产用沙箱的取舍就完全不同:训练可以容忍偶发失败和稍长的冷启动,但要求高吞吐和低成本;线上则要求稳定和低延迟。下面讲选型时会反复回到这张表。
3. 沙箱方案选型:从进程级到微虚拟机怎么挑
3.1 四类主流隔离方案对比
市面上做代码沙箱,隔离强度从弱到强大致分四档,我把它们的实测特征整理成表:
| 方案 | 隔离级别 | 冷启动 | 单机密度 | 适用场景 |
|---|---|---|---|---|
| 子进程 + 权限限制 | 进程级 | 毫秒级 | 极高 | 可信代码、内部工具 |
| 容器(namespace+cgroup) | OS 级 | 百毫秒级 | 高 | 大多数 Agent 训练 |
| 微虚拟机(轻量 VM) | 硬件级 | 百毫秒~秒级 | 中 | 不可信代码、多租户 |
| 完整虚拟机 | 硬件级 | 秒级~十秒级 | 低 | 强合规、强隔离 |
选型的核心判断标准只有一个:你执行的代码有多不可信。如果 Agent 生成的是任意 Python、可能rm -rf、可能读环境变量、可能发起网络请求,那进程级隔离基本等于没隔离,容器是底线。如果还要防容器逃逸、防侧信道,那就得上微虚拟机。
3.2 为什么大多数 Agent 训练选容器而不是微虚拟机
一天 300 万的量级,微虚拟机的冷启动和内存开销会直接把成本顶上去。容器方案在“隔离够用 + 启动够快 + 密度够高”这个三角里是最平衡的。具体做法通常是:
- 用namespace做文件系统、PID、网络、IPC 隔离;
- 用cgroup限制 CPU、内存、进程数、磁盘 IO;
- 用seccomp限制危险系统调用;
- 用只读根文件系统 + 可写临时层保证每次执行环境干净。
这套组合能挡住绝大多数“模型乱写代码”的场景。真正需要微虚拟机的,是那种要跑完全不可信二进制、或者有强多租户合规要求的场景。
3.3 镜像分层与预热:把冷启动从秒级压到百毫秒
沙箱启动慢,八成慢在镜像。我的经验是把镜像拆成三层:
- 基础层:OS + 常用运行时(Python、Node、常用库),几乎不变,常驻预热。
- 任务层:某类任务需要的依赖(比如数据分析任务装 pandas、numpy),按任务类型预构建。
- 临时层:本次执行产生的文件,用完即弃。
基础层和任务层提前预热成“半启动”状态——容器已经创建、运行时已经加载,只等挂载临时层和执行命令。这样冷启动能从秒级压到百毫秒级。实测下来,预热池命中率每提高 10%,整体 rollout 吞吐能提升 6%~8%,因为省下的全是等待时间。
提示:预热池不是越大越好。池子太大,空闲容器占内存;池子太小,高峰期现拉容器又慢。经验值是按“峰值并发 × 1.2”配置,再配合一个动态扩缩容策略,低峰期回收。
4. 高并发沙箱调度的核心实现
4.1 调度器要解决的三个问题
一天 300 万沙箱,调度器是真正的核心。它要解决三件事:
- 分配:哪个沙箱给哪个任务,怎么保证不冲突;
- 回收:执行完怎么快速清理、复用、销毁;
- 观测:每个沙箱的状态、耗时、结果怎么追踪。
我见过太多项目把调度写成一个大锁 + 一个队列,量一上来就全堵死。正确做法是分片 + 无锁队列 + 状态机。每个调度分片管理一批沙箱,分片之间互不干扰;任务进无锁队列,worker 抢任务;每个沙箱有明确状态(创建中、就绪、执行中、回收中、已销毁),状态流转全部可观测。
4.2 沙箱生命周期状态机
一个沙箱从生到死,标准状态流转是这样的:
创建中 -> 就绪 -> 执行中 -> 结果收集 -> 回收中 -> 已销毁 \-> 超时 -> 强制回收 -> 已销毁 \-> 异常 -> 标记失败 -> 已销毁关键点在于每个状态都要有超时。创建超时、执行超时、回收超时,任何一个卡住都要能被强制清理。我踩过最深的坑就是回收阶段卡死:某个沙箱执行完但回收进程挂了,容器一直占着资源,几分钟后整个节点内存爆掉。后来加了回收超时和兜底清理,才稳住。
4.3 并发参数怎么算
假设你的目标是支撑峰值 5000 并发沙箱,每个沙箱平均执行 3 秒,单机最多跑 200 个沙箱,那么:
- 需要的节点数 = 5000 / 200 = 25 台;
- 每秒完成的任务数 = 5000 / 3 ≈ 1667 个;
- 一天理论处理量 = 1667 × 86400 ≈ 1.44 亿次执行。
看起来远超 300 万,但实际要打很多折扣:任务分布不均、失败重试、回收延迟、节点故障。经验上按理论值的 20%~30% 估算真实吞吐比较稳妥。所以 300 万/天的目标,峰值并发几千、节点几十台,是完全可实现的规模,不需要神话它。
注意:单机密度不是越高越好。密度太高,一个沙箱内存泄漏就可能拖垮整台机器。我一般把单机内存用到 70% 就设警戒线,留 30% 缓冲。
5. 实操:从零搭一个可复现的 Agent 沙箱执行链路
5.1 环境准备与依赖
先明确这套实操的目标:搭一个能接收代码、隔离执行、返回结果、支持并发的沙箱服务。基础依赖如下(以 Linux 为例):
# 基础运行时 python3 --version # 建议 3.10+ docker --version # 容器方案需要 # 关键内核能力确认 cat /proc/sys/kernel/unprivileged_userns_clone # 确认 namespace 可用如果走容器方案,Docker 或 containerd 都行,containerd 更轻、更适合大规模。我实测在同等硬件下,containerd 的容器创建比 Docker 快 15%~20%,因为少了一层守护进程开销。
5.2 沙箱执行服务的最小实现
下面是一个简化的执行服务核心逻辑,用 Python 写,重点是展示状态管理和超时控制:
import subprocess import tempfile import os import signal class Sandbox: def __init__(self, timeout=5, mem_limit_mb=512): self.timeout = timeout self.mem_limit_mb = mem_limit_mb self.workdir = tempfile.mkdtemp(prefix="sbx_") def run(self, code: str): # 写入临时文件,避免命令行注入 script = os.path.join(self.workdir, "main.py") with open(script, "w") as f: f.write(code) try: proc = subprocess.Popen( ["python3", script], cwd=self.workdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=os.setsid, # 独立进程组,便于整组清理 ) out, err = proc.communicate(timeout=self.timeout) return {"ok": True, "stdout": out.decode(), "stderr": err.decode()} except subprocess.TimeoutExpired: os.killpg(os.getpgid(proc.pid), signal.SIGKILL) # 杀整组 return {"ok": False, "error": "timeout"} finally: self.cleanup() def cleanup(self): # 递归删除工作目录 subprocess.run(["rm", "-rf", self.workdir])这段代码有几个关键设计点值得说:
- 独立进程组:
os.setsid让子进程自成一组,超时时可以整组杀掉,避免子进程再 fork 出孙进程逃逸。 - 临时目录隔离:每次执行一个独立目录,用完即删,保证环境干净。
- 超时兜底:
communicate(timeout=...)是硬超时,配合killpg强制清理。
这只是最小版本,生产环境要换成容器 + cgroup 才能真正限制内存和 CPU。但它的状态管理思路是一样的:创建、执行、超时、清理,四步都要有兜底。
5.3 接入 Agent 框架的调用方式
Agent 框架(无论是自研 harness 还是开源 agent 框架)调用沙箱,通常走一个统一的工具接口。伪代码大概长这样:
def code_exec_tool(code: str, timeout: int = 5): sandbox = pool.acquire() # 从预热池拿一个 try: result = sandbox.run(code) return format_result(result) # 转成模型能读的格式 finally: pool.release(sandbox) # 归还或销毁这里有个容易忽略的点:返回给模型的格式。模型读结果的能力有限,stdout 太长会挤爆上下文。我的做法是截断到固定长度(比如 2000 字符),超长部分存文件并返回路径,让模型按需再读。这样既省上下文,又保留完整信息。
6. 常见问题与排查技巧实录
6.1 沙箱相关高频问题速查表
| 现象 | 可能原因 | 排查方向 | 解决 |
|---|---|---|---|
| 创建超时 | 镜像太大/预热池空 | 看镜像层大小、池命中率 | 分层镜像、扩池 |
| 执行卡死 | 代码死循环/等输入 | 看进程状态、stdin | 硬超时+杀进程组 |
| 内存爆 | 代码申请大内存 | cgroup 限制是否生效 | 加 mem limit |
| 结果丢失 | 回收太快/日志没落盘 | 检查结果收集时序 | 先收结果再回收 |
| 环境脏 | 临时层没清干净 | 检查 workdir 残留 | 强制清理+只读根 |
| 并发上不去 | 调度锁竞争 | 看调度器 CPU、队列深度 | 分片+无锁队列 |
6.2 三个我踩过的坑
坑一:超时杀了主进程,孙进程还在跑。早期只用proc.kill(),结果代码里 fork 出来的子进程继续占 CPU。后来改成killpg杀整个进程组才解决。这个坑在 Agent 场景特别常见,因为模型生成的代码经常起多进程。
坑二:预热池的容器被“用脏”了。为了省启动时间,我一度复用容器,结果上一个任务写的文件被下一个任务读到,导致结果错乱。后来改成“临时层每次重建”,基础层才复用,问题消失。复用可以,但只能复用无状态的部分。
坑三:日志和沙箱 ID 对不上。高并发下,日志混在一起根本没法排查。后来给每个沙箱打唯一 ID,所有日志、指标、结果都带这个 ID,排查效率直接翻倍。这个改动看起来小,但它是可观测性的地基。
6.3 关于 agent 安全的几句实在话
热词里有 agent 安全、a-memguard 这类词,说明大家开始重视了。我的观点很直接:Agent 安全的第一道防线就是沙箱。模型再对齐,也可能被诱导生成危险代码;工具再受限,也可能被绕过。沙箱是最后一道物理隔离,它不依赖模型“听话”,只依赖内核机制。所以做 Agent 项目,沙箱不是可选项,是必选项。至于记忆安全、prompt 注入防护,那是沙箱之上的第二层,不能替代沙箱。
7. 成本与扩展:把 300 万这个数字落到地上
7.1 成本拆解
300 万沙箱一天,成本主要在三块:计算、内存、存储 IO。按容器方案估算,一个沙箱平均占用 0.5 核 + 512MB 内存 + 3 秒,那么:
- CPU 总消耗 = 300万 × 0.5核 × 3秒 = 450万核秒 ≈ 1250 核时;
- 内存总消耗 = 300万 × 512MB × 3秒 ≈ 460万 GB秒 ≈ 1280 GB时。
按这个量级,几十台机器的集群就能覆盖,成本远没有想象中夸张。真正烧钱的是低效:镜像太大导致启动慢、回收不及时导致资源空占、调度不当导致节点闲置。优化这三块,比单纯堆机器有效得多。
7.2 横向扩展的注意点
扩展沙箱集群,最容易忽略的是状态存储。沙箱本身无状态,但调度元数据、结果、日志是有状态的。如果这些也放在本地,节点一多就乱。我的做法是:调度元数据放中心存储,结果和日志异步落对象存储,节点本身尽量无状态。这样加节点就是纯加算力,不用改架构。
提示:扩展时先压测调度器,再压测执行节点。调度器往往是先崩的那个,因为它是有状态的、集中的。分片能缓解,但分片本身也要能水平扩。
8. 我个人的几点体会
做 Agent 沙箱这几年,最大的感受是:它是个“脏活”,但脏活决定上限。模型能力、框架设计这些“聪明活”大家都在卷,最后拉开差距的往往是沙箱这种基础设施——谁能把创建、执行、回收这条链路做得又快又稳又便宜,谁就能把 Agent 训练规模真正推上去。
如果让我给刚入门的同学一个建议:别一上来就追求 300 万,先把单个沙箱跑通、跑稳、跑干净,再谈并发。我见过太多项目,单沙箱还漏内存呢,就开始设计百万并发架构,最后两头都塌。先把状态机、超时、清理这三件事做扎实,规模是水到渠成的事。
最后分享一个我一直在用的小技巧:给沙箱加一个“执行指纹”——把代码哈希、环境版本、执行结果一起存下来。同样的代码在同样环境下,直接返回缓存结果,不用重新执行。在 rollout 场景里,重复代码的比例其实不低,这个缓存能省下可观的算力。实测在部分任务上,命中率能到 15%~25%,等于白捡的吞吐。