☰
智能体训练安全沙箱架构:从隔离设计到弹性调度实战
2026/10/8 3:31:44 网站建设 项目流程

如果你训练过一个会自己写代码的智能体,你大概率经历过这样的场景:策略模型又生成了一段 Python,你明知道它大概率跑不完,但你还得给它一个能执行的环境;它调用某个工具把工作目录搞得一团糟;它没退出循环,CPU 被打满;最要命的是,你根本判断不了这是模型能力问题还是环境问题。我这两年处理这类问题,基本围绕 DeepSeek 模型生态搭了一套弹性计算沙箱基础设施,也就是标题里的 DSec。它要管的事情可以压缩成两句话:模型生成的任意代码和命令,到底在哪儿安全地跑;规模上来之后,这些运行实例怎么做到按需拉起、用后即弃。

这篇文章是给谁看的?如果你在做 agent 训练、RL 策略迭代、代码解释器类产品,或者负责给这类系统提供计算平台,下面这些踩坑记录和架构思路应该能省你不少时间。我不会讲官方文档里那种概念,而是按我实际搭建和调优的顺序来拆:沙箱为什么必须单独做一层、隔离边界怎么划、弹性调度怎么设计、如何和训练循环对接,最后列一些我实测过的配置和踩过的坑。

1. 智能体训练中的沙箱刚需:不可信代码、高并发实例与安全底线

1.1 模型采样出来的动作,本质上是一个不可信程序

传统后端服务里的代码,是工程师写好、代码评审过、测试通过之后才上线的。智能体训练完全不同:策略模型在每一步会采样出若干候选动作,这些动作可能是一次函数调用、一段生成的 shell 脚本、一个写文件的 Python 脚本。没有任何评审,没有任何人工确认,代码直接就要执行。

这里有个容易低估的点:模型生成的代码不是"大概率有问题",而是"在训练集和探索策略的双重作用下,刻意包含各种边界行为"。它可能写满一个临时目录、可能占用全部内存、可能打开几百个文件句柄不关、可能 fork 失控。更麻烦的是,在 RL(强化学习) 训练中,这些行为恰恰是策略探索的一部分,你不能简单地不让它做——你只能让它在不影响训练主链路的环境里随便折腾。

1.2 普通容器和裸机为什么都不够用

早期我们试过直接在 Docker 容器里跑。Docker 本身能隔离文件系统和进程,但训练场景下的问题通常不在"隔离有没有",而在"边界和策略有没有被系统化管理"。你需要一批容器统一挂只读根文件系统,需要按任务级别配置网络白名单,需要限制系统调用,需要在一轮训练结束后把几百个容器全部回收,再为下一轮批量拉起。

裸机或普通 VM 就更不用说了:一是冷启动太慢,训练步与步之间不可能等着一台 VM 慢慢引导;二是资源碎片化严重,有的任务只用 1 个 CPU,有的要 8 个 CPU,靠人工分配根本跑不动。真正缺的不是"某种容器技术",而是一个把隔离策略、弹性伸缩、任务生命周期管起来的沙箱基础设施。

1.3 DSec 的三个设计目标:弹性、高效、可控

我们给 DSec 定的位不是"又一个容器平台",而是一个面向智能体训练的执行层。目标明确为三条。第一是弹性:按训练任务的需求动态伸缩执行实例,高峰期几百个实例同时跑,空闲时缩到接近零,不养闲资源。第二是高效:通过镜像分层缓存、快照复用、预热等手段,把单实例的拉起和销毁代价降下去,让每轮 rollout 的等待时间可控。第三是可控:所有执行行为都有策略约束——能跑什么命令、能写哪里、能访问哪些地址、能占用多少资源——并且这些约束是可审计、可复现的。

这三条目标决定了后面所有的设计取舍:宁可牺牲一点点单实例性能,也要把隔离边界做干净;宁可调度算法复杂一些,也要让整体吞吐不会因为某个任务卡住而崩掉。

2. 隔离边界的设计选择:从命名空间到系统调用的层层收口

沙箱基础设施的核心不是"跑得快",而是"坏不了"。"坏不了"有两层含义:第一,它不能影响宿主和其他任务;第二,它不能对训练主链路产生不可诊断的干扰。为实现这一点,DSec 在隔离层做了几个层次的收口。

2.1 第一层:进程与文件系统隔离

进程隔离和文件系统隔离是沙箱的地基。我们在每个执行实例里使用独立的 PID 命名空间,保证沙箱内的进程树从 PID 1 开始,沙箱内怎么 fork、怎么 kill,都不会污染宿主的进程表。文件系统层面用 overlayfs 构造只读根层加可写临时层的结构:镜像层是只读的,任务运行期间的写入全部落在独立的临时写层,任务结束直接丢弃写层,或者按需保留为快照。

这里有一个实际训练场景很受用的细节:可写临时层的容量必须提前设定,而不是随磁盘走。我们的做法是默认给 1GB 临时盘配额,超出后后续写入直接报 ENOSPC。这个看起来"粗暴"的限制,在智能体训练中反而是优点——写满临时盘是模型探索时最常见的幻觉模式之一,它比让整个 worker 节点的磁盘被日志或垃圾文件塞满要温和得多。

2.2 第二层:资源配额与系统调用限制

资源配额用的是 cgroups 的 CPU、内存和进程数限制。内存限制尤其重要,模型生成的代码如果里面有个无限增长的列表,不用 cgroup 兜底,一个实例就能把整台机器拖垮。我们通常给普通执行任务设置 2 CPU、4GB 内存、512 个进程数的默认配额。关键点是:这些配额不是写死在节点上的,而是跟着任务走,由调度器在每个实例创建时动态下发。

系统调用限制是我觉得很多沙箱做得不够细的地方。默认 Docker 的 seccomp profile 已经挡掉了一批危险 syscall,但对于训练任务来说,我们还需要根据任务策略做二次过滤。比如,绝大多数代码执行任务根本不需要 mount、reboot、swapon 这类调用,也没必要保留 CAP_SYS_ADMIN 这类大权限;工具调用任务里,凡是涉及 bind 到低端口、对宿主机网络栈直接操作的能力,都应该默认去掉。我们内部维护了一张"默认禁用"系统调用清单,除非任务显式声明需要,否则一律在 seccomp 层拦截。

下表是我们常用的三类执行任务的隔离策略对比:

任务类型文件系统网络出口默认资源限额关键禁用项
代码执行只读根+ 1GB 写层默认关闭2 CPU / 4GB / 512 进程mount、reboot、CAP_SYS_ADMIN
工具调用只读根+ 专用工作区任务级白名单2 CPU / 4GB / 512 进程bind 低端口、内核模块加载
模型评测只读根+ 只读数据集挂载仅允许内网数据服务4 CPU / 8GB / 1024 进程异步 IO 直通、设备映射

这个表不是死的,实际训练中经常要按任务微调。但原则始终一样:默认拒绝,显式放行。

2.3 第三层:网络出口与数据进出的审计

沙箱不开网络是最安全的,但智能体任务常常要访问数据服务、搜索 API 之类。我们的做法是给网络出口做任务级白名单,白名单条目跟随任务提交。沙箱内部默认只有本地回环和外网 DNS 解析能力,要访问的域或 IP 必须在策略里显式声明,再由控制面下发到节点的网络规则上。

数据进出审计同样在前置阶段完成:训练任务从对象存储拉取数据集、向训练回传结果,都走固定的挂载点和专用通道,不回传明文痕迹。沙箱产生的可观测数据(标准输出、退出码、资源占用曲线、被拦截的操作记录)会落成结构化日志,作为轨迹样本的一部分随任务结果返回。这一步对于调试模型策略很有用,我后面会专门讲。

3. 弹性调度核心机制:任务队列、池化伸缩与快照复用

隔离层解决"能不能安全跑",调度层解决"几百个任务怎么排队、怎么伸缩、怎么不互相拖累"。这一节是 DSec 最花心思的部分。

3.1 任务生命周期:一个执行请求从哪里开始,到哪里结束

一个执行任务在 DSec 里会经历五个阶段:提交、排队、调度、运行、回收。提交阶段,上层训练框架调用 API 把任务描述发过来,任务描述里包括镜像、启动命令、资源配额、策略标签、超时和回调地址。调度器收到后,先把任务放进优先级队列,再根据当前资源池状态决定是立即执行还是等待。

运行和回收之间有一个容易被忽视的细节:回收集合(release set)的处理。任务结束后,控制面先做结果采集(退出码、输出、轨迹、指标),再做实例销毁。销毁不是直接 kill,而是走一个"优雅回收"流程:停止新进程、通知沙箱内运行的进程主动退出、超时后强杀、最后清理写层并归还资源。这套流程看起来多花了几十毫秒,但避免了大量僵尸进程和未释放的文件句柄。

3.2 池化伸缩:训练高峰怎么扛,空窗期怎么省钱

训练任务的特点之一是"波峰波谷极其明显":一个 epoch 的 rollout 阶段,可能需要同时跑几百个环境实例;epoch 之间做策略更新和评估时,计算量又断崖式下降。如果按峰值容量买机器,大部分时间都在浪费。

DSec 的做法是把 worker 节点组成可伸缩资源池,用两层指标做伸缩决策。第一层是等待队列长度:等待中的任务多了,调度器就申请横向扩容,把池子变大;等到队列空了,节点进入空闲状态。第二层是实例利用率:已经运行的实例 CPU/内存利用率如果连续一段时间低于阈值,就触发缩容。这里有个我们踩过的坑:缩容不能只盯着 CPU 平均值。CPU 平均利用率低,不代表没有突发任务;要同时看 P95 等待时间和队列深度的变化趋势,宁可保留少量热节点,也不要频繁缩容后又被立刻打爆。

3.3 快照复用与镜像预热:冷启动时间怎么压到秒级以下

沙箱实例的创建速度直接决定训练吞吐。如果每个实例都从头拉镜像、解压层、初始化环境,几百个并发实例的启动时间会非常难看。我们做了两件事来压启动成本。

第一是镜像分层预热。训练任务一个批次里通常都是同一镜像、不同执行参数,我们让 worker 节点提前把镜像层全部拉下来并缓存,实例创建直接从本地层组装 rootfs,不再走网络。第二是写层快照复用。同类任务的临时写层初始状态往往是同一个模板(比如预设好的工作目录结构、环境变量、依赖位置),我们把这种初始状态做成快照,新实例直接从快照克隆写层,省掉了每次环境初始化的时间。实测下来(以我们单集群为例),纯代码执行任务从提交到进程启动,冷启动大约 8 秒,热启动(镜像已缓存且快照命中)能压到 1 秒以内。

这个差异对训练是有实际意义的:误以为环境初始化时间可以被忽略的人,通常会在 rollout 吞吐上吃大亏。

4. 与模型生态的接入姿势:harness、训练循环与 API 约定

沙箱基础设施不会单独存在。它一定被接在两条链路里:一条是智能体框架(类似很多人用的 harness 编排层),负责组织 prompt、skill 和工具调用;另一条是 RL 训练循环,负责产生策略、收集轨迹、计算 reward。DSec 处在两条链路的交汇点。

4.1 智能体编排层与沙箱的执行边界

拿 DeepSeek 生态里常见的做法举例:上层用 harness 这类编排工具管理智能体的技能列表、提示词模板和插件,模型决定"下一步调用哪个工具"之后,真正干活的地方需要独立沙箱。DSec 对外提供的就是一个极简执行协议:提交执行请求、获得执行结果、按需取回过程数据。

一个典型的执行请求大概长这样(示例字段,实际按内部约定调整):

{ "task_id": "ep-00087-001", "image": "registry.internal/dsec/py-agent:3.11-v2", "command": ["python", "/workspace/run.py"], "policy": { "network_egress": ["data.internal:443"], "writable_workspace_size_mb": 1024 }, "resources": {"cpu": 2, "memory_mb": 4096}, "timeout_seconds": 300, "callback_url": "http://train-controller/v1/exec/callback" }

编排层不用关心沙箱内部怎么实现隔离,只需要声明"我要什么环境、什么策略、多长超时"。沙箱执行完,把结构化结果回传给训练控制器。这个边界一旦清晰,上下两层都可以独立演进。

4.2 训练循环里的结构化回传:异常也必须是数据

如果你只是把沙箱当成一个"能跑代码的地方",那它和普通的容器平台没区别。DSec 对训练特别有价值的地方,在于把执行结果组织成训练轨迹的一部分。每个任务结束后的回传结果,不只是标准输出和退出码,还包括:超时标记、资源限制触达标记(因为内存还是因为进程数)、策略拦截事件列表、沙箱内资源占用时间线。

这些结构化异常信息对 reward 计算很重要。模型明明想解决一个问题,但环境因为内存超限把它杀了,这属于"工具失败"还是"策略错误",不同判断会把训练引导到完全不同的方向。我们把退出原因分成四类:正常完成、超时、资源超限、策略拦截。训练循环根据类型决定要不要给负激励,以及给多重的负激励。这是把"环境反馈"变成"训练信号"的关键一步。

4.3 离线与批处理:同一个沙箱,另一种调度需求

除了在线训练,还有一类常见需求是离线数据集处理:把一批文档投给模型,让模型生成摘要或标注,再对标注样例做校验。这类任务没有严格的训练时序要求,但对稳定性和失败重试要求高。

DSec 对这类任务提供的是批量执行通道:支持一次提交一组执行请求,调度器按批分配,失败自动重试,批量结束后统一回传结果集合。批量通道和在线通道共用同一套沙箱隔离策略,但调度优先级更低,主要利用离线窗口期的空闲资源。这样既不会抢占在线训练的实例,又能把资源的空窗期用起来。

5. 最小可用部署与容量调优:我实测的配置清单

说完设计,说落地。这一节比较实用,我按"最小可用部署 -> 关键参数 -> 常见调优手段"来讲。

5.1 控制面 + 工作节点的最小拓扑

DSec 的最小部署由三部分组成:控制面、工作节点、持久化侧。控制面承担 API 服务、调度决策和状态存储,工作节点承担沙箱实例的运行,持久化侧是镜像仓库和训练结果的对象存储。

一个能跑通的容灾拓扑大约是这样:

控制面(2+ 实例) ├─ API Server:接收执行请求,做鉴权和策略校验 ├─ Scheduler:任务排队、资源匹配、伸缩决策 └─ State Store:任务状态、节点状态、(可选用关系库或 KV 存储) 工作节点(N 台,按伸缩策略增减) ├─ Sandbox Runner:管理实例生命周期、执行隔离策略 └─ 本地镜像/快照缓存 依赖侧 ├─ 镜像仓库:存放任务镜像和初始快照 └─ 对象存储:存数据集、任务输入输出和回传日志

控制面是两个无状态服务加一个状态存储,横向扩容基本没有压力。工作节点要打上统一的镜像缓存预热策略,避免新节点加入时因为缓存为空而引发下载风暴,这一点我后面在踩坑里展开。

5.2 关键参数与容量公式

容量规划的核心是回答一个问题:训练任务最忙的时候,同时需要多少个沙箱实例,以及每个实例的峰值资源。我给一个简单的估算思路:假设一轮 rollout 需要并发 200 个执行实例,每个默认 2 CPU/4GB,那么理想情况下工作节点需要至少 400 CPU、800GB 内存的可用容量。但实际不能按 100% 使用率来规划,要给调度和伸缩留 30% 左右余量,不然队列一有波动就会连锁超时。

另外两个容易被漏掉的参数是队列深度和单节点实例密度。队列深度决定了任务等待的容忍度;单节点实例密度则取决于沙箱运行时的开销,如果每个实例额外占 300MB 管理内存,200 个实例就是 60GB 的固定开销,这部分要在节点内存预算里单独划出来。

5.3 沙箱运行时选型:runc 与 gVisor 的取舍

最后聊一下沙箱本身用什么运行时。我实测过两类方案。一类是 runc 这一类基于内核命名空间的传统容器运行时,性能开销小、启动快、兼容性好;缺点是内核隔离边界没那么硬,对内核漏洞的容忍度低。另一类是 gVisor 这种用户态内核方案,系统调用拦截做得更彻底,隔离性明显更强,代价是部分系统调用和计算密集场景下的性能损耗,实测在 IO 密集任务里大概有 10% 到 30% 的额外开销。

选择上我们的原则是:模型生成的不可信代码,默认给强隔离(gVisor);来自内部工具链、经过模板约束的任务,给标准容器(runc);涉及敏感数据或外部不可信输入的任务,则放到轻量 VM 里跑。弹性调度和数据面不变,只是底层运行时按策略选择。这样既不用为所有任务买单强隔离的损耗,也不会让所有任务都暴露在同一个脆弱的内核边界上。

6. 踩坑实录:镜像风暴、句柄泄漏与超时误伤

落地一套沙箱基础设施,真正考验人的不是架构设计,而是运行三五天之后冒出来的各种稳定性问题。我把踩得最深的几个坑记录在这里,希望能帮后面的人少走弯路。

6.1 镜像下载风暴:新节点加入时的连锁反应

第一次压测的时候,我们往集群里加了几台新 worker 节点,结果所有新节点同时从镜像仓库拉取同一个任务镜像,仓库带宽瞬间打满,连控制面的健康检查都开始超时。这个问题很隐蔽,因为单台节点看 CPU 和内存都不高,但你去看网络出口,会发现整个集群的可用带宽被镜像拉取吃掉了。

解决办法是给镜像分发加"预缓存 + 限速"双保险。新节点注册后,只允许从仓库拉取调度器指定的预热镜像列表,并且做并发限速;同时把镜像仓库前置一层本地缓存代理(按节点可用区部署),让多数镜像命中缓存。此外,工作节点的镜像缓存不要随便清理,定期按 LRU 淘汰即可,频繁清缓存只会让热节点退化成冷节点。

6.2 文件句柄和临时目录泄漏

沙箱实例销毁后,我们发现 worker 节点的文件句柄数缓慢爬升,跑上几天后部分节点开始无法创建新实例。排查到最后,问题出在两个地方:一是沙箱内进程持有挂载点,外层销毁时机不对,导致挂载点残留;二是任务的工作目录建在了节点的临时文件系统上,实例删除后目录没有同步清理。

修复方式分两层。调度侧,把"实例销毁"做成一个完整状态机:先 umount 写层和挂载点,再删除目录,最后释放资源;如果 umount 失败,打标记进入延迟清理队列,而不是直接放弃。节点侧,加了一个周期巡检任务,统计挂载点数量和临时目录大小,超过阈值就触发清理并上报告警。这个巡检看着土,但在沙箱场景里比什么都管用。

6.3 超时误伤:长任务被一刀切

训练任务里我们最初用一个统一的超时值(比如 120 秒)。后来调试一个数据标注流程时,发现部分标注任务明明快成功了,却在最后一步被超时杀掉。原因很简单:不同任务的单次执行时间方差很大,统一超时按"绝大多数任务"设置,必然误伤长尾任务。

调整方案是给任务引入"超时分布"概念:调度器按任务类型和镜像维护历史执行时长的 P95,提交任务时如果没有显式超时,就用 P95 再加 30% 缓冲作为默认超时。这样既防止死循环任务无限执行,也不会频繁误杀正常的长任务。这个改动之后,我们的任务超时率下降了一个数量级。

6.4 异常实例复现困难:可观测数据一定要留全

最后一个坑属于运维层面的教训。曾经有一个任务会随机失败,且失败时沙箱内进程状态和进程树都查不到。后来发现是我们在回收时把执行过程的完整状态数据(进程树快照、资源指标、关键日志)丢得太早,导致异常实例无法复现。

现在我们的规定是:所有任务执行期间的进程树快照、系统调用拦截日志、资源占用时间线,默认保留 7 天,并和任务结果关联存储。排障时可以直接按 task_id 回溯一个实例从创建到销毁的完整生命周期。这个投入不大,但它让你在大规模训练环境下,面对"随机失败"问题时,不再靠猜。

我个人的体会是,沙箱基础设施的价值不在于它有多强的技术噱头,而在于它让智能体训练变得可以被信任:模型生成的代码可以放开探索,训练过程中不用担心一个失控实例拖垮整批任务。部署上了之后,它更像是一个安静的后台角色——你只在出问题的时候想起它,但好的调度和隔离策略,本就应该让出问题的概率低到让人忽略它的存在。

如果接下来要扩展,我建议优先做两件事:一是把执行结果里错误类型的分类做得更细,直接变成 reward 的输入特征;二是把沙箱的启动成本和训练步的调度做更深的联合优化,让"环境准备好了再开始采样"成为一个标准能力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询