接入生产这几个字听起来轻飘飘的真正做起来全是坑。我们团队在做Agent平台时最大的瓶颈根本不在模型调用这一个环节——今天模型能力再强输出也是一段一段的代码、命令、工具调用序列。真正让人睡不着觉的是Agent生成的这些动作到底在哪跑脚本闯了祸算谁的会话中断了下次还能不能接上以及工具报错了怎么定位是环境问题还是代码问题这所有的答案都落在「Agent沙箱」这一个词上。所以这篇文章我打算把几个核心问题摊开讲沙箱该选什么形态、生产环境的数据怎么持久化、执行协议怎么定才能扛住真实流量。给正在从Demo走向生产的团队做个参考。我尽量不写废话全是实操里踩过的坑和最后采用的方案。1. 为什么生产环境的Agent执行必须有沙箱这道门1.1 Agent执行失控的典型场景如果你以为Agent生成的代码无非就是调调API、写写文件那可能还没见过真实用户量上来之后的样子。我们在内测阶段就遇到过几个典型的失控场景Agent在调试循环里反复自调用同一个脚本每分钟执行几十次直接把下游接口打满生成的清理脚本里出现了rm -rf /tmp/project因为路径拼接错误实际删除范围扩大到了宿主机的挂载目录一个简单的while True循环没有sleep瞬间把一个2核4G的实例CPU打到100%容器直接卡死Agent主动外呼访问公网服务把内部测试凭据带进了第三方请求日志里。这些场景放到生产环境里任何一个都足以引发事故。模型输入是不可信的模型生成的代码更不可信——这句话不是危言耸听而是Agent平台必须默认的安全基线。有同学会问那我在代码里做规则过滤行不行比如禁止rm -rf、禁止死循环。我只能说规则过滤在早期可以挡住一部分但Agent生成的内容组合方式几乎是无限的你今天堵住了字符串拼接的rm -rf明天它换个编码方式、用Python的os.system、或者通过subprocess调用第三方工具规则就失效了。安全边界必须落在基础设施层而不是靠业务代码去猜。1.2 沙箱隔离的四个边界沙箱这道门本质上要把四样东西管住CPU、内存、文件、网络。CPU管理不能允许一个Agent任务吃掉整台机器的计算资源。最直接的手段是cgroup的CPU配额限制单容器最多只能用多少核、多少份额内存管理同样用cgroup限制内存上限超了就触发OOM容器被杀而不是宿主机被拖垮。我的建议是不仅限制总内存还要限制进程数pidscgroup防止fork炸弹把进程表撑爆文件隔离容器只能看到自己的文件系统宿主机目录必须做白名单挂载。镜像尽量保持只读层可写层一律放到临时目录或独立Volume网络隔离默认关闭出网需要外呼的场景走代理网关由网关做一次域名/IP白名单校验再放行。这四个边界不是可选项而是Agent生产环境的入场券。我们曾经天真地以为内网部署无所谓结果一个Agent在沙箱里把内网的配置中心地址给爬了个遍虽然没造成实际破坏但这件事坚定了我们「默认最小权限、按需放行」的原则。很多人会把沙箱理解成一个安全容器其实更准确的说法是沙箱是一套从资源配额到权限降级再到网络管控的完整机制。Docker的namespace cgroup seccomp三层组合已经能挡住绝大多数非恶意但破坏力极大的误操作如果你想防恶意攻击者那就得考虑更强隔离的方案下面会细讲。2. 沙箱选型Docker、gVisor、Firecracker与重度隔离方案2.1 主流的四条技术路线对比市面上的沙箱方案整理下来大概四类方案隔离层级启动耗时资源开销兼容性运维复杂度Docker容器runC内核共享百ms级低极高低gVisorrunsc用户态内核拦截秒级中高部分系统调用不支持中FirecrackermicroVM硬件虚拟化百ms~秒级中高高需精简内核高QEMU/KVM全虚拟机硬件虚拟化秒级极高极高很高Docker容器是最容易上手的选择因为几乎所有Agent要跑的东西Python、Node、Java、Shell在标准基础镜像里都能跑团队对这个技术栈太熟了遇到问题好排查。但它的问题在于共享内核——一旦内核出现提权漏洞容器边界是可能被突破的对高安全场景不够。gVisor 相当于在用户态实现了一个内核层Sentry应用的系统调用先被拦截再由用户态内核代理执行。好处是即使容器内发生了提权攻击者面对的也只是一个仿真内核想逃逸的难度大增坏处是部分依赖深度内核特性的程序比如某些需要复杂io_uring的数据库、部分GPU调用跑不了兼容性打折。Firecracker 是AWS构建lambda底座的microVM方案每个沙箱是一个极轻量的虚拟机启动速度可以做到和容器差不多但隔离级别和全虚拟机一致。这是我个人最看好的方向适合对隔离和密度同时有高要求的团队代价是镜像内核需要裁剪、网络和存储初始化要多写一些胶水代码。QEMU/KVM全虚拟机最稳但开销太大启动也慢一个Agent任务如果平均只需要运行几十秒全VM形态的成本不够划算。2.2 我们最终选型的关键依据我们的选择是默认Docker容器敏感任务用gVisorFirecracker作为后续演进方向。理由并不复杂Agent执行的任务特点决定了我们每天要起停海量短生命周期沙箱启动速度、资源密度是硬指标Docker在这两项上表现最好团队对Docker的运维经验最成熟镜像构建、监控、排障体系都能复用没必要为了「看起来更安全」而牺牲整个团队的排障效率通过叠加 seccomp 策略、capability drop、只读根文件系统、禁用默认网络桥接Docker的实际安全水位可以被拉高一大截足以应对「AI生成代码自动执行」这种非恶意但易失控的场景对需要处理用户敏感数据的任务单独选择 gVisor 运行时作为高风险任务的加固通道。选型上我们留了运行时切换的开关同一个镜像可以用不同runtime启动后续迁Firecracker的时候上层协议不用动。这里多说一句选型上的心得沙箱选型不要只看隔离强度还要看和你的执行协议、持久化方案怎么配合。如果沙箱启动要五秒钟你的执行协议还按同步HTTP来设计用户点一次运行代码就要等十秒体验就崩了。我们在主要场景控制冷启动在1秒内具体怎么优化在后面章节展开。2.3 镜像瘦身与启动耗时优化沙箱执行链路能不能扛住生产流量一个常被忽略的指标就是冷启动耗时。Agent用户交互是实时的你在那里等沙箱起来再执行代码五秒以上基本就没人愿意用了。我们当时的优化路径有三条基础镜像极简每个执行环境单独定制镜像不要把通用开发环境这一类大而全的镜像作为沙箱基底。比如Python任务的镜像只需要Python runtime、pip、常用shell工具不需要gcc编译链。我们曾把一个Python基础镜像从900MB压到220MB启动速度直接上了一个台阶层缓存与预拉取镜像层尽量稳定公共层在集群节点上提前拉好。业务层体积控制在几十MB级别避免每次启动都从仓库拉大文件。应用层变动频繁没关系只要底层不变Docker的layer cache就有效容器池复用这是最暴力也最有效的一招。提前在节点上预热一批空闲容器处于已创建未执行的状态接任务时直接复用把容器创建时间从几百毫秒压缩到几十毫秒。复用要留意脏数据问题每次任务结束后把可写层重置或直接销毁容器只复用底层的镜像和基础容器配置。优化完的实测数据是最常用的Python执行沙箱从收到请求到容器内进程启动中位数耗时在500ms左右极端冷节点上从cgroup创建到可执行也能控制在1秒上下。这个数字对交互式Agent来说基本无感。3. 持久化设计沙箱内会话存活与状态恢复的落地方式3.1 沙箱里为什么不能直接写文件初期的平台实现里我们把Agent的临时文件、中间产物、生成的代码都直接写进了容器的可写层想着反正是临时的任务结束就无所谓了。结果线上跑起来暴露了两个严重问题第一容器可写层本身不能可靠持久化。容器一销毁数据就没了即使容器没销毁节点重启、调度迁移也会让数据丢失。用户问刚才Agent执行到一半的结果去哪了我们没法回答。第二把临时状态和业务状态混在一起整个系统没有清晰的数据边界。哪些要留、哪些可删、过期策略怎么定全都没法回答。最后我们定了一条核心原则沙箱内部的可写层只允许保存瞬时执行状态任何需要跨任务存活的数据必须显式写到外部持久化服务。Agent执行时的瞬时状态指的是什么进程的stdout/stderr缓冲、临时文件、环境变量、当前工作目录的快照——这些可以活在容器的可写层里。而业务状态指的是用户任务的输入参数、Agent每一步的思考过程、工具调用的请求与响应、最终产出的文件内容——这些必须离开沙箱落到可靠的存储里。3.2 元数据与执行产物的分层存储落地方案上我们把持久化拆成了几个层次执行产物Agent生成的代码文件、图片、PDF、数据表等走对象存储。我们自建了MinIO按tenant_id/task_id/run_id/组织目录文件名保留原始相对路径。对象存储的优势是天然支持海量小文件、读写带宽大、生命周期管理方便还可以直接生成预签名URL给前端展示元数据任务记录、会话ID、退出码、耗时、日志路径、产物清单落在MySQL。每次沙箱执行结束沙箱内的执行器会把结构化元数据上报到控制面控制面写库。日志文本走独立的日志系统ELK/Loki不塞进MySQL消息事件流走Kafka或者轻量点的Redis Stream。任务进度、工具调用事件、输出增量全部以事件流方式推送既提供给前端实时展示也作为后续回放和分析的数据源etcd我们只用来存分布式协调类状态比如沙箱节点注册、任务锁、运行时配置绝不存大文件或业务数据。好多人一上来就想用etcd存一切其实它不擅长存大的对象数据硬存会把集群性能拖垮。这里我特别想说一下日志路径的设计。一个Agent任务跑起来stdout里可能混着程序输出、错误堆栈、工具调用的打印乱七八糟。我们规定沙箱执行器把每一类日志写到不同的文件比如stdout.log、stderr.log、tool_call.jsonl结构化了才能被后续的日志平台索引和分析。不然排查一个问题要在一坨字符串里人肉翻那效率太低了。3.3 会话恢复与可写层快照Agent产品的体验和普通API有个很大区别用户可能打开一个Agent会话让它执行到一半然后关掉浏览器过几个小时再回来希望会话还能继续。这对沙箱持久化是个大挑战。我们的方案分两层第一层是热会话保持。沙箱创建后控制面通过心跳机制维持会话活性只要用户还在交互沙箱就持续复用。心跳超时我们默认5分钟无活动触发回收流程先尝试优雅销毁杀不掉的强制杀。为了防止空闲沙箱堆积控制面有独立的Reaper组件扫描超时会话。第二层是冷状态恢复。如果会话已经回收用户重新激活会话时我们需要让Agent想起之前的执行状态。做法是周期性对容器的可写层做快照导出成overlay.tar.gz存到对象存储恢复时先启动镜像再挂载快照层。恢复后工作目录、已经生成的文件、环境变量基本都还在Agent可以从接近上次断点的位置继续执行。但要注意快照不是万能的。如果Agent执行依赖了某些进程内状态比如一个持有外部连接的长驻进程快照恢复后进程已经丢了这种状态我们无法完整还原。我们的处理方式是把这个物理限制暴露给上层应用由Agent业务层在关键节点用显式检查点的方式把逻辑状态落库而不是依赖容器快照。比如Agent每完成一个工具调用就把当前的结果摘要和下一步计划写入会话表这样即使沙箱被重新拉起Agent也能靠逻辑检查点来恢复上下文。快照频率也要权衡。太频繁每次执行都导出大文件存储和CPU都受不了太低频恢复点丢了太多进度。我们按RPO需求来定对大多数Agent任务每完成一个工具调用节点打一次轻量快照只记录逻辑状态同时每5分钟或每次任务结束时做一次容器层快照。两个配合起来既能快速恢复执行环境又能精确恢复语义状态。4. 执行协议设计请求、响应、事件与任务状态机4.1 协议契约从HTTP调用到双通道状态模型最早我们的Agent沙箱调用方式和普通接口一样同步HTTP POST传入代码返回执行结果。等到真正接生产立刻发现行不通一个Agent任务不是执行一段代码到返回那么简单它是一串任务编排——Agent可能要先创建一个Python环境然后执行代码代码里又要调用某个工具工具再反馈给AgentAgent决策后又执行下一步。整个过程可能有几十次子执行必须用异步模型来承载。我们最终定的协议是控制面HTTP 数据面WebSocket/SSE的双通道模型控制面负责任务的创建、取消、查询、超时设置走普通的HTTP API请求/响应严格同步方便重试数据面负责执行过程中的实时事件回传包括stdout增量、工具调用记录、运行状态变化、错误信息通过WebSocket内网或SSE对外推给上层平台。协议语义上我们定义了一套轻量的JSON-RPC风格接口核心方法就几个方法方向说明create_session控制面→沙箱创建执行会话带上镜像、资源限制、白名单配置execute_task控制面→沙箱下发一段要执行的动作脚本、命令、工具调用参数task_event沙箱→控制面执行过程中的增量事件按session_idevent_id有序推送terminate_task控制面→沙箱请求取消某次任务沙箱内执行器收到后先优雅终止进程树再杀容器get_task_status控制面→沙箱查询任务当前状态主要给控制面重建状态机时用这里的关键设计是事件必须带序状态必须可重建。每个事件都带event_seq单调递增序号控制面根据序号去重和排序避免WebSocket重传导致前端出现乱序输出。另外沙箱会定期把自身的汇总状态当前任务ID、运行状态、关键输出偏移量上报给控制面作为状态重建的基线。4.2 任务取消与TTL强制回收Agent任务的执行时长天然不可控有的几秒结束有的能跑几十分钟甚至挂死。生产上你不可能无限等下去所以协议里必须把取消和超时作为一个一等公民设计。我们的做法是三层兜底用户主动取消前端点停止控制面调terminate_task。沙箱执行器收到取消信号后先向进程组发SIGTERM宽限期3~5秒让进程有机会做清理比如关闭文件、回滚状态还没死直接SIGKILL任务级超时每个任务创建时带上timeout_seconds字段执行器内部用定时器盯着。这个超时通常由上层Agent根据任务复杂度估算一般工具调用类任务默认120秒代码生成类任务给到300秒会话级TTL即使单任务不超时整个会话的生命周期也得受限。常规会话TTL我们默认4小时到点强制销毁沙箱防止看起来没在跑但进程一直在后台占资源的僵尸会话。取消操作还有一个容易忽略的点取消要在外部留下痕迹。我们在协议里规定沙箱被取消后必须上报一个terminated事件带上最后的退出码和一段原因说明控制面拿到这个事件才把任务状态置为最终态已取消。否则前端就会出现用户点了取消但任务状态一直是运行中的悬置问题。4.3 幂等、重试与风暴防护生产环境网络抖动不可完全避免。控制面和沙箱之间的HTTP调用断了重发一次会不会导致任务重复执行这个问题必须靠协议上的幂等设计解决。我们在所有控制面写操作里强制带request_id沙箱端用session_idrequest_id做幂等键。比如execute_task重复收到相同的request_id直接返回上一次的受理结果不会二次执行。另一件重要的事是流量风暴防护。Agent在无人值守时如果逻辑出错可能反复提交同一个任务形成雪崩。我们在控制面给每个会话加了令牌桶限流默认每秒最多允许发起5次execute_task超出直接拒绝并返回429错误。这个限流不是针对业务的限制纯粹是给失控的Agent踩刹车。除此之外控制面和沙箱之间还有一层熔断如果某个沙箱节点连续上报大量执行失败比如容器启动异常、磁盘写满控制面会把这个节点主动摘除流量进入健康检查队列。这个机制保证的是一个坏节点不能拖垮整个沙箱池。5. 接入生产之后的稳定性复盘真实流量暴露的问题5.1 资源泄漏与回收不及时沙箱接入线上最初两周我们的最大事故不是沙箱被攻破而是资源泄漏。具体表现是节点上的容器数量只增不减磁盘慢慢被撑满最终导致部分任务创建失败。一开始我们以为是控制面的回收逻辑没跑查了一圈发现回收逻辑正常问题出在进程没有死透。Agent在沙箱里起了子进程SIGKILL杀掉了主进程但子进程成了孤儿反而活了下来继续占用CPU和内存。更隐蔽的是有些进程在等待外部IO一直处于不可中断状态容器被销毁了进程却还挂在宿主机的进程树里。排障思路供参考先看节点上的容器数和/proc下的进程数是否持续增长找到孤儿进程的父进程IDPPID1的进程基本就是孤儿再顺藤摸瓜找到对应的容器ID确认回收链路是只杀了主进程还是对整个进程组process group做了处理。修复方案沙箱内所有任务启动都生成独立的进程组回收的时候用kill(0, -pgid)把信号发给整个进程组同时启用内核的pidscgroup限制进程数不能无限增长。另外节点上部署了周期性的Cron扫描把宿主机上进程所属容器已不存在的残留进程统一清理兜底。5.2 文件系统逃逸与危险调用的伪装生产流量起来之后我们遇到了几个属于边缘攻击手法的情况虽然不是恶意攻击者但足够让人冒冷汗。比如Agent通过Python生成一个软链接指向宿主机挂载目录然后向软链接路径写入文件。在容器内看起来是写自己的临时目录实际上数据穿透到了挂载边界之外。这类问题的本质是我们对挂载点的权限管控不够细。解决方案是除了只读根文件系统我们对所有挂载点强制指定nosuid,nodev,noexec并且把Agent可写的路径限制在容器内的专用工作目录宿主机侧挂载进来的目录全部设为只读。再比如某些Agent任务会尝试执行mount操作或修改系统网络配置。多数情况下这并非恶意只是Agent从网上学来的命令没有经过安全评审就应用了。我们在seccomp策略里显式放行了一小部分安全系统调用其他的一律EPERM。配置项整理了一个基线清单策略类别具体配置capability只保留CAP_NET_BIND_SERVICE其余全部dropseccomp白名单模式只放行文件读写、进程管理、信号等基础调用挂载权限容器内新mount一律禁止网络默认关闭容器出网外呼走代理白名单5.3 失控Agent的任务循环与限流生产里最难防的其实是逻辑正确但行为失控的Agent。我印象最深的一个事故某个Agent在处理一个数据清洗任务时对每一条脏数据都触发一次重跑整个Pipeline的工具调用。因为这个Agent的判断逻辑写歪了它对同一批数据反复提交了上百次任务。如果当时没有执行层的限流我们整个任务队列会瞬间被打爆。所以我把执行协议里的限流机制再强调一遍限流不是防用户是防Agent自己。模型天然有坚持自己的方案的倾向一旦没有外部刹车它可能在一个错误方向上反复用力。我们在协议层做了两处控制每个会话的并发任务数上限为1不允许一个会话同时跑多个沙箱任务从根上防止任务爆炸每个会话的任务提交速率按令牌桶限制超限返回错误码Agent收到错误后必须改变策略否则它自己会陷入重试死循环。还有一个非常重要但常被忽略的Agent的每一步工具调用都必须有审计记录。沙箱执行器在每次任务执行时自动记录完整的出入参、执行耗时、退出码并保存一份不可篡改的日志散列。这既是安全问题排查的依据也是后续做Agent行为分析和策略优化的数据基础。写在最后的一点体会把Agent沙箱接入生产环境本质上是一个不断设置边界-突破边界-再设置边界的过程。没有任何一次选型或配置可以一劳永逸真实流量总会用你想不到的方式考验你。我个人特别想强调的是协议设计要走在沙箱实现前面。如果先把容器跑通了再补协议往往会因为接口语义不清晰而反复推翻重来。我们是在镜像还只有两三个的时候就把执行协议、事件模型、取消语义、幂等规则全部定义清楚后面换runtime、加快照、上gVisor上层几乎没改动。还有一个给刚起步团队的实用建议如果资源有限不一定要一开始就上最重型的沙箱方案。Docker seccomp cgroup 显式持久化这一套组合已经能覆盖绝大多数Agent生产场景。先把执行链路的可观测性做出来——每个任务的输入输出、耗时、资源使用、日志全部结构化管理——再去追求更高的隔离形态。毕竟沙箱本身不是目标让Agent安全、稳定、可审计地跑起来才是。