
先别管Agent会给出多花哨的回答实际运行时候才是真正见真章的地方。我见过太多人讨论Agent的时候把沙箱这个词挂在嘴边可真要问一句你这沙箱怎么配的、隔离到哪一层、Agent在里面能干什么不能干什么对方往往就含糊了。这篇文章不说虚的直接从原理层把沙箱拆开Agent为什么一定要跑在沙箱里不同隔离方案之间差的到底是什么以及你自己动手落地一个最小可用沙箱时需要盯住哪些关键点。1. 先弄清楚Agent为什么非得进沙箱不可1.1 LLM生成的下一步动作天然不可控很多人对Agent有个误解觉得它就是一个更聪明的对话工具问什么答什么。但真正跑过Agent的人都知道Agent的核心能力在于行动它会调用工具、写文件、执行命令、访问网络。也就是说模型输出的一串文字会被翻译成一系列真实发生在操作系统里的行为。问题就出在这个翻译过程上。大模型生成文本是基于概率分布的它在决定下一步动作的时候并没有可靠的因果判断能力。同一个需求上一轮它可能规规矩矩执行了一条只读命令下一轮因为上下文里出现了一个误导性词汇它可能就直接尝试了一处危险的目录操作。这不是它学坏了这是生成式模型固有的不确定性。再叠加一个现实Agent拿到的工具权限往往来自运行它的那个进程。如果这个进程直接跑在你的开发机或生产机上那Agent的所有行为都和你的正式运行环境共享同一套权限体系。换句话讲你让Agent帮我看看日志它理论上也能顺手改掉日志目录里的配置。所以沙箱的意义就是把Agent放进一个权限边界清晰、执行痕迹可回收、失败了也不牵连外部的独立空间里。1.2 不隔离会怎样我亲眼见过的三条事故链条光说很危险太抽象我直接讲三个我在不同项目里真实撞见的事故画面。第一个是环境破坏型。某个内部工具把Agent直接跑在宿主机上让模型负责整理临时文件。对话模型推理出来的结论是临时文件太多直接清空重启服务效率更高于是真的执行了一整套清理和重启命令。重启之后服务倒是起来了但另一个团队的缓存数据被连带清掉了等到发现时已经过了半个多小时。第二个是依赖冲突型。Agent被要求安装一个分析包它直接调用了系统级的包管理器做全局安装把整个Python依赖树搅乱了。事后排查那个环境里好几个不相关的项目全部启动失败因为它们的依赖全被锁死或者覆盖了。这种事故很难第一时间联想到Agent排错成本极高。第三个是数据外溢型。Agent在调试某个网络请求的时候访问了本不该访问的内网存储节点把敏感数据读进了上下文然后又通过日志文件写出来了。它不是故意的但结果就是数据被拖出了它不该在的地方。这三条事故链的共同点是什么都不是模型主观恶意而是Agent没有边界意识而运行环境又没有替它把边界守住。沙箱干的事情很简单把能改的和不能碰的用机制隔离而不是寄希望于模型自觉。1.3 沙箱保护的是两条线主机不被破坏Agent不被污染做了几年Agent框架之后我越来越觉得沙箱是一种双向保护。它当然首先保护主机防止危险命令、资源耗尽、数据泄漏。但另一条线很多人没意识到沙箱也保护Agent本身的运行状态可控。你想一个问题如果Agent直接在宿主机上操作它创建的文件、临时缓存、写坏的配置全部散落在系统各个角落。一次会话结束后你根本说不清这个Agent到底碰过哪些东西。但如果让Agent在一个独立容器里执行整个会话就有一个清晰的边界。调试的时候你看到的Agent状态就是它自己环境里的状态而不是被外部进程干扰、污染过的状态。用完销毁所有观察痕迹一并清理下一次会话又是干净起步。这对我做多智能体系统的意义很大我可以在沙箱里给Agent设定只准看到任务相关的文件——它不感知主人机器的全局信息;它也不容易在长对话里被无关残留信息误导。本质上沙箱把Agent的认知边界和系统边界统一了。2. 沙箱的隔离层进程原语、容器、微虚拟机差在哪2.1 第一层Linux自带的隔离原语聊实现之前得先聊聊操作系统本身提供的基础能力。Linux系统里隔离一个进程可以靠五件套namespace、cgroup、seccomp、chroot/pivot_root、capabilities。namespace用来隔离视角。让进程只能看到自己的进程列表、自己的网络栈、自己的挂载点。cgroup用来限制资源。CPU用多少、内存用多少、进程数量上限都是这一层管的。seccomp负责过滤系统调用把mount、ptrace这类高危操作挡在门外。chroot或者pivot_root可以把进程的文件系统视角锁在一个目录里。capabilities则是把root权限拆成细粒度能力能删掉绝大部分特权操作。如果你只是临时想让某个命令不要乱跑用这些原语组合一下就够了。开个受限的bash限制它的网络和写权限跑完就扔。这套方案的优点是快一个隔离环境的启动时间可以压到几十毫秒缺点是隔离强度有限毕竟调用方和内核还是同一套一旦碰到能借内核漏洞突破的代码防线很容易被撕开。2.2 第二层容器方案隔离但不等于安全再往上一层就是大家最熟悉的容器。容器的本质是把前面那套Linux原语打包成更好用的接口把镜像、网络、存储、进程管理都标准化了。开发者一行命令就能拉起一个最小Linux环境。但这里我必须把一句话说清楚容器不等于安全沙箱。默认配置下容器和宿主机共享同一个内核它只是做了命名空间层面的隔离。如果运行在容器内的代码里藏着一个能利用内核漏洞的逃逸 payload一旦突破就直接回到宿主机视角。所以拿容器当Agent沙箱不能启一个默认容器就完事。至少要做到不给特权模式不挂宿主机目录关掉不必要的capabilities限制资源上限根文件系统设成只读进程以非root身份运行。我后面会给出一个可直接抄的参数模板这些细节都会包含进来。2.3 第三层更强的隔离用户态内核与微虚拟机当Agent跑的是完全不可信的代码或者你要对外提供代码执行这类多租户能力时普通容器的隔离强度就不够看了。这时候要上更强的方案。一条路线是用户态内核典型代表是gVisor。它把系统调用拦截下来在用户态模拟内核行为。容器里的进程以为自己在正常调系统接口实际上每个调用都先经过一层检查和模拟。好处是隔离强度比普通容器高启动开销又比传统虚拟机小很多兼容性也不错多数现有镜像可以直接跑。另一条路线是微虚拟机。Firecracker和Cloud Hypervisor这类技术把硬件虚拟化环境做得很轻盈每个实例可以独占非常少的内存却拥有真正的虚拟机级隔离。很多云函数平台、公开代码沙箱底层就是这类方案。它比传统虚拟机轻但隔离层次是实打实的。再往上就是完整虚拟机QEMU/KVM这一类。隔离性最强但启动慢、资源开销大。除非你处理的Agent任务极度敏感比如要接触私钥或者直接操作生产数据否则一般用不上。多数Agent的日常代码执行容器或者microVM就足够了。2.4 一张表看懂常见沙箱方案怎么选方案隔离强度启动速度资源开销典型使用场景进程级隔离原语较弱毫秒级极低临时限制单个命令普通容器Docker中等百毫秒到秒级低私有Agent的常规代码执行gVisor较强百毫秒到秒级中不可信代码且要求兼容容器接口微虚拟机强秒级中高多租户公开代码执行完整虚拟机最强秒级到分钟级高极端敏感任务我给别人的建议一直是不要一上来就上最重的方案也不要图省事开个默认容器就完事。先问自己三个问题——Agent跑的是谁给的代码代码来源可信到什么程度如果逃逸成功最坏影响是什么答案清楚了选型自然就出来了。3. 落地一个最小可用沙箱先裁剪权限再封装执行3.1 动手前把边界写下来我习惯把Agent沙箱理解成作业间Agent可以在里面折腾但墙是明确的门是受控的用完了整个房间推倒重建。要搭这个作业间第一步不是写代码而是把边界定义清楚。拿我最近一个项目来说边界是这样五条Agent不能访问宿主机上除指定工作目录以外的任何文件Agent默认没有外网访问能力单次执行时间不超过30秒内存上限512MBCPU核数不超过0.5执行结束后容器和临时文件全部清理。这五条看起来简单但它们直接决定了后面的所有参数。你的场景可能不一样比如需要让Agent访问某个内部服务那就在网络策略里单独放行指定地址而不是直接给它开整个网络。规则越少越容易守住规则越多越容易漏。3.2 一个可以直接抄的容器参数模板下面是我最近在项目里用的模板假设本机已经装好了容器运行时且有一个工作目录/path/to/workspacedocker run --rm \ --name agent-exec-sandbox \ --network none \ --memory 512m \ --cpus 0.5 \ --pids-limit 128 \ --read-only \ --tmpfs /work-run:size256m \ --cap-drop ALL \ --security-opt no-new-privileges \ --user nobody \ --workdir /work-run \ -v /path/to/workspace:/workspace:ro \ python:3.12-slim \ python /workspace/task.py这几个参数没有一个是多余的我逐个解释一下我为什么这么配--network none是第一优先级。断了网Agent既不能往外传数据也不能做横向探测。在我所有沙箱配置里这一条是绝对默认项。--read-only把根文件系统锁成只读Agent想往系统目录里写任何东西都写不进去。--cap-drop ALL把Linux capabilities全部丢弃很多提权、挂载、改系统参数的操作直接不可用。--security-opt no-new-privileges防止进程后续再提升权限。--user nobody保证进入容器后不是root身份。--pids-limit 128限制进程数上限防止Agent写出一个fork炸弹式的脚本瞬间把资源打满。--memory和--cpus把资源盒固定住避免一次执行吃光整台机器。--tmpfs /work-run给Agent留一个可写临时区它中间产生的文件都放这里容器一删就什么都没了。我把这套模板称为最小可写空间原则允许Agent干活但只给一个临时工位所有持久化路径全部走宿主机的只读挂载。3.3 封装成一个执行接口光有docker run命令还不够Agent在跑的时候需要一个更统一的调用入口。我习惯在容器命令外面再包一层服务对外暴露两个接口一个提交执行任务一个查询任务状态。提交任务时你至少要传这几个字段镜像名、要执行的命令、资源限制、超时时间、是否断网、环境变量、临时区大小。服务端拿到这个请求之后去调度一个容器跑跑完把标准输出、错误信息和退出码回传。以下是请求体的大致形状{ image: python:3.12-slim, cmd: [python, /workspace/task.py], memory: 512m, cpus: 0.5, timeout_seconds: 30, network: false, tmpfs: /work-run }这里有一个细节容易踩坑不要在宿主机的临时目录里直接创建执行文件。因为容器是独立文件系统你得把脚本塞进它的可视范围我用的是挂载方式直接把宿主工作目录以只读模式挂进容器。这样Agent能读到脚本和输入数据但写不回去输出只能落在临时区。3.4 从Agent框架里把工具接到沙箱上Agent要使用这个沙箱还得过一道工具调用的桥。绝大多数Agent编排框架都支持自定义工具工具被定义成一个函数带有清晰的参数说明模型根据需求决定调用哪个工具、传什么参数。我们要做的事情就是把工具的执行函数内部换成刚才那个沙箱服务的请求。比如Agent有个执行Python代码的工具用户问帮我统计这批数的均值模型会生成一个Python脚本然后调用这个工具把脚本内容传进来。执行函数里直接POST给沙箱服务拿到运行结果后回传给模型。这里有个实操上的小经验不要直接让Agent把整段代码丢进容器跑除非你做了严格的命令白名单设置。工具层的可控性远高于系统调用层——因为工具接口知道这是出于什么目的调用的比在沙箱里无差别执行脚本要安全得多。3.5 会话级的沙箱映射还有一个容易被忽略的设计沙箱的粒度。每执行一次工具就起一个新容器丢弃所有中间状态听起来干净但Agent在长任务场景里经常需要在多步之间保留一些上下文比如上一步生成的文件这一步要继续读。我的做法是引入会话概念。每个Agent会话对应一个长期存活的沙箱容器这个容器里有一个共享的工作目录。会话内的多步执行都在同一个容器里完成文件、临时状态都保留在这个工位上。等整个会话结束再把容器销毁。这样一来Agent的记忆和系统的状态边界是同步的出了问题也好排查你只需要看这个会话容器的日志和文件系统。要注意的是常驻容器会比一次性容器更容易积累垃圾所以我还会加一条额外的定时清理策略空闲超过一定时间的会话容器直接删掉避免长时间挂着一堆僵尸工位。4. 沙箱真正跑起来之后策略、超时与数据生命周期4.1 命令白名单比黑名单好用得多沙箱参数配好了不等于策略就完善了。真正决定风险水平的是你在沙箱内部允许Agent执行什么。这个策略层我强烈建议用白名单而不是黑名单。黑名单的思路是禁止某些危险命令比如不让执行rm、不让改系统文件。但这东西根本防不住——危险命令的变体太多了一个脚本可以绕过黑名单把目录清空、把文件挪走、甚至用解释器Swift执行任意内容黑名单永远跟不上想象力。白名单的思路正好反过来只允许Agent在沙箱内调用你预先指定的命令和接口其他一律拒绝。比如只允许读写指定临时目录下的文件只运行白名单内的Python解释器命令只访问列表里的几个内网服务。Agent在这个环境里再活跃也只能在规则圈定的空间里动。配合上一步的沙箱配置白名单策略落地之后Agent的施工范围才算真正可控。4.2 超时、并发与结果截断我接手沙箱服务之后最早碰到的线上故障就是没有给执行设超时。某个Agent写了一个无限循环容器CPU被打满任务队列后面所有请求全部排队整个服务都卡死了。从那之后我的经验是超时必须贯穿两层。第一层是容器层超时。启动容器时就要带上超时时间比如30秒。一旦超过直接杀掉容器返回超时错误。第二层是工具调用层超时。Agent框架那边调用工具本身也要设一个比容器超时更短一点的等待上限避免框架一直阻塞。并发也要控。Agent应用经常会出现瞬间拉起多个任务的情况如果你不限制并发宿主机压力会直接拉爆。我的方案是加一个简单的任务队列同一时间最多跑4个沙箱容器其余排队等候。这样就算Agent发疯一样申请执行资源占用也始终在一个可控水位。结果截断是我后来补的另一个关键点。有时候Agent执行的程序会产生超长输出比如打印了10万行日志这些输出全量回传给大模型会把上下文窗口塞爆Token费用也会失控。所以我在沙箱服务里默认只回传输出前5000个字符再加上一个尾部摘要既保留关键信息又避免上下文被灌满。4.3 数据生命周期与上下文污染沙箱隔离了进程和文件但有一个地方很容易被忽略Agent的执行结果最终是要回到上下文窗口里的。因为模型要读这些信息来做下一步决策所以沙箱产生的数据会从容器这一侧泄漏到对话上下文那一侧。这意味着两件事。第一沙箱内产生的输出里如果含有敏感信息这些信息会进入大模型的上下文可能随后被其他Agent看到甚至被写入日志。针对这一点我在沙箱服务的输出处理里加了一道过滤把明显的密钥、令牌、内网路径等模式直接做脱敏替换只保留形态不保留内容。第二执行产物到了会话结束时要彻底销毁。容器销毁之后那个承载对话上下文的会话本身要记得清理临时文件。我之前吃过一个亏某Agent在沙箱里生成了一堆分析报告会话结束后我删了容器但宿主机工作区里的挂载副本没删干净过了几天磁盘报警才发现。现在我的铁律是所有沙箱产生的临时挂载数据跟容器同生命周期容器删了挂载目录也必须跟着清。4.4 沙箱逃逸的现实威胁与纵深防御聊到沙箱就绕不开逃逸这个词。我的判断是如果你只是给内部使用的Agent做隔离逃逸概率其实不高因为攻击者需要构造精心设计的绕过代码而且还要恰好在你的Agent任务中跑起来。但如果你做一个面向外部用户的帮我跑代码服务逃逸就是每天都在发生的攻防战。不管哪种情况我都建议做纵深防御而不是把希望全部押在某一层隔离上。我的防御习惯是这么排的最外层是权限边界——Agent能访问的网络、文件、API全部显式声明中间一层是沙箱本身——用容器/微虚拟机限制系统调用和资源最内层是工具策略——Agent只能通过白名单内的接口操作不能直接裸跑任意命令。三层里面任何一层被突破另外两层还能把影响兜住。还有一件事千万不要给沙箱容器挂载宿主机的敏感目录尤其是/var/run/docker.sock这种。我见过有人为了调试方便把Docker socket挂进沙箱容器结果Agent只要执行一条命令就能直接控制整个Docker守护进程。这等于亲手把整个主机交出去了。5. 我踩过的坑与后面的调整思路5.1 默认容器当沙箱等于没设防我第一次做Agent沙箱的时候就是直接起一个默认容器什么参数都没配。当时想着反正容器和宿主隔开了总比裸跑安全吧。后来一个测试Agent执行了一个挂在宿主目录上的清理脚本把工作区里没提交的改动全删了。那一次之后我才认真去研究capabilities、seccomp、只读文件系统这些细节。现在回想默认容器的问题在于它把主机的很多能力带进去了而且运行时默认是root这让Agent的权限大得离谱。我的经验是每次新起沙箱环境默认只给三个东西——一个只读工作区、一个临时可写区、一个受控进程环境。其他一切能删就删能关就关。5.2 冷启动时间比想象中更咬人容器沙箱有个绕不开的成本冷启动。第一次启动一个新镜像可能要拉取几百兆的层即使本地已经有镜像每次创建容器也要几百毫秒到一两秒。在一个高频使用Agent的场景里这个延迟会被放大几十倍。我的解决办法是镜像预热加容器复用。镜像预热很简单构建完镜像之后立刻执行一次docker run --rm让它把层加载一遍后续启动就会快很多。容器复用则是维护一个空闲容器池里面常驻几个已经启动好的沙箱容器有新任务就从池子里取一个改一下工作目录和命令直接用用完放回池子而不是销毁。当然这个池子要配合前面的会话清理策略用不然容器会越积越多。5.3 网络开关别做半吊子我还犯过一个典型的半吊子错误一开始觉得完全断网太麻烦就给沙箱开了网络想着反正只是访问包管理器问题不大。结果某次Agent拿着这个网络去访问外部公告接口还尝试做了几次跳转请求虽然后来发现没什么实质危害但那一刻我意识到只要开了网络你就很难判断它会走到哪里。后来我干脆利落一点默认全部断网遇到确实需要网络的Agent任务单独配置一个旁路代理代理规则里写明允许访问的域名和IP白名单。这样网络能力被约束成一个显式的、可审计的列表而不是一个开着口子的默认通道。5.4 每改一个沙箱参数都要做回归验证最后一个想说的是流程层面的习惯。沙箱配置是那种改一下感觉没事但在生产里就出事的东西。我之前调整过内存限制从512m降到256m觉得大部分任务用不到那么多。结果某天一个数据处理任务在沙箱里被OOM杀掉了而它之前一直跑得好好的。问题倒不是这条配置本身而是我改配置的时候没有做一次全量回归。现在我的做法是任何沙箱参数调整都必须跑一遍预置的测试集。测试集里包含正常运行的任务、故意超时的任务、产生超大输出的任务、试图写系统目录的任务。这些用例全部通过之后参数才算真正能上生产线。这个习惯帮我挡掉了好几次潜在的事故。最后说一个我自己养成的习惯给沙箱加参数的时候一次只改一个改完立刻验证效果不要一次性堆一堆参数进去。因为一旦出了问题你会完全不知道是哪条规则在起作用排错效率极低。沙箱这种东西本质上是在跟不确定性打交道你能做的是把每个规则都当作一次实验小心验证再逐步收紧。