1. 沙箱不是新东西,但"给大模型用的沙箱"是另一回事
沙箱这个概念在软件工程里确实不算新鲜。从最早的 Java Applet 沙箱、浏览器同源策略,到后来的 Docker 容器隔离、gVisor、Firecracker microVM,再到支付宝沙箱支付这种业务层面的模拟环境,"沙箱"两个字在开发者语境里已经存在了二十多年。所以当 DeepSeek 传出要自研沙箱的消息时,很多人的第一反应是:这东西不是早就成熟了吗?直接拿现成的容器方案套一层不就行了?
我一开始也是这么想的。直到我自己动手把几个主流方案拼起来跑了一遍代码执行场景,才发现问题根本不在"隔离"这一层。传统沙箱解决的核心问题是安全隔离——别让一段不可信的代码把宿主机搞崩、把别人的数据读走。但大模型驱动的代码执行场景,它要解决的问题清单完全不一样:它要的是低延迟的冷启动、可预测的资源计量、对模型输出的容错、多轮对话中的状态保持,以及执行结果能被模型"读懂"的结构化回传。这几件事,传统沙箱方案要么不关心,要么做得很别扭。
这篇文章我想聊的就是这个错位。不是复述 DeepSeek 官方说了什么,而是从一个实际搭过、踩过坑的从业者角度,把"为什么现成沙箱不够用"这件事拆开讲清楚。如果你正在做 AI Agent 的代码执行、正在给企业微信或内部系统接一个能跑代码的助手、或者单纯好奇 deepseek harness 这类工具背后的执行层是怎么设计的,这篇应该能给你一些可以直接参考的判断依据。核心关键词就三个:代码沙箱、DeepSeek、执行隔离,但我会把重点放在"为什么重造"而不是"怎么调用 API"上。
先说结论,免得你看到一半觉得我在绕:传统沙箱优化的是"隔离强度 vs 性能"这条曲线,而大模型沙箱优化的是"启动速度 vs 状态可管理性 vs 结果可解释性"这条完全不同的曲线。目标函数变了,最优解自然就变了。下面我分几层把这个判断展开。
2. 传统沙箱的成熟,成熟在哪个维度上
要理解"为什么还要重造",得先诚实地承认传统沙箱到底成熟在哪。不然容易陷入"什么都得自研"的民科思维。我梳理了一下,现成方案的成熟度主要集中在三个地方,而这三个地方恰好都不是大模型场景的痛点。
2.1 隔离强度:从 namespace 到 microVM 的完整光谱
Linux 的 namespace + cgroup 提供了进程级的隔离,Docker 把它包装成了易用的镜像和网络模型。再往上,gVisor 用用户态内核拦截系统调用,Firecracker 用轻量级虚拟机做到接近硬件级的隔离。这条光谱非常完整,你可以根据威胁模型选不同强度的方案。这是十几年工程积累的结果,自研很难在短时间内达到同等成熟度。
但问题在于,大模型执行代码的威胁模型和"运行陌生人的代码"不完全一样。模型生成的代码通常是短小的、功能明确的片段——排序、字符串处理、调个 API、画个图。它很少是恶意的,更多是有 bug 的。真正的风险不是"这段代码要攻击我",而是"这段代码死循环了""这段代码把内存吃光了""这段代码 import 了一个不存在的包然后抛异常"。这些是可用性问题,不是安全问题。传统沙箱在安全隔离上做到 99 分,但在"优雅地处理一个笨拙的、会犯错的执行者"这件事上,可能只有 60 分。
2.2 资源计量:cgroup 给的是"用了多少",不是"该收多少"
cgroup 能告诉你一个容器用了多少 CPU 时间、多少内存峰值。这在传统场景够用了——你是按容器收费的,或者你只是想知道谁在偷跑。但在大模型场景里,资源计量要服务于一个更细的需求:按次执行的配额控制和成本归因。一次代码执行可能只跑 200 毫秒,你要能精确地知道这一次花了多少、属于哪个会话、哪个用户、哪个 Agent 的哪一步。cgroup 的统计粒度是容器级的、持续累积的,你要把它拆成"每次调用"的账单,中间得自己做一层映射,而且这层映射在容器复用时很容易串味。
我实测过一个方案:每个会话起一个容器,会话结束销毁。结果是冷启动开销把整个体验拖垮了,用户等三秒才看到结果。改成容器池复用之后,资源计量就开始互相污染——上一个会话的残留内存算到了下一个会话头上。这不是 cgroup 的错,是它本来就不是为这种"高频、短命、需要精确归因"的场景设计的。
2.3 镜像与依赖:一次构建,处处运行的代价
Docker 镜像的哲学是"一次构建,处处运行",依赖在构建期就固化好。这在部署服务时是巨大优势。但大模型执行代码时,依赖是动态的、不可预知的。模型可能今天想用 pandas,明天想用 requests,后天想画个 matplotlib 图。你不可能预装所有库——镜像会大到离谱,而且大部分用不上。你也不可能每次执行都 pip install——那延迟没法看。
现成方案里,有的用"预装常用库 + 禁止安装"来妥协,有的用"允许安装但每次重建环境"来妥协。前者限制了模型的能力边界,后者牺牲了响应速度。这个矛盾在传统沙箱的框架里没有优雅解,因为传统沙箱假设依赖是构建期确定的。大模型场景把这个假设直接推翻了。
这里有个容易被忽略的点:模型生成的代码经常带
import语句,但它不一定知道你的环境里装了什么。如果环境里没有这个包,代码直接抛ModuleNotFoundError。好的执行层应该能捕获这个错误,并且把"缺哪个包"这个信息结构化地回传给模型,让模型有机会自己修正。传统沙箱的报错是给运维看的,大模型沙箱的报错是给模型看的,这两者的信息设计目标完全不同。
3. 大模型执行场景真正卡脖子的四个点
上一节说了传统沙箱"成熟但不匹配"。这一节我把不匹配的地方具体化成四个卡点。这四个点是我在实际搭 deepseek harness 类执行链路时,反复撞墙撞出来的,不是纸上谈兵。
3.1 冷启动:从"秒级可接受"到"百毫秒级才及格"
传统容器冷启动,从docker run到进程真正跑起来,视镜像大小和存储驱动,通常在 300 毫秒到 2 秒之间。对部署服务来说,这个开销摊薄到长期运行上可以忽略。但对"用户发一句话,模型生成代码,执行,返回结果"这种交互,每一次执行都是一次冷启动的话,用户感知到的就是明显的卡顿。
我做过一个对比测试,同一个简单的 Python 计算任务:
| 方案 | 冷启动耗时 | 执行耗时 | 用户总等待 |
|---|---|---|---|
| 每次新建 Docker 容器 | 约 800ms | 约 50ms | 约 850ms |
| 容器池预热复用 | 约 20ms | 约 50ms | 约 70ms |
| 进程级隔离 + 预热解释器 | 约 5ms | 约 50ms | 约 55ms |
差距是数量级的。用户对 850ms 和 55ms 的感知完全不同,前者是"卡了一下",后者是"秒回"。这就是为什么大模型沙箱必须在预热和复用上做文章,而传统沙箱的默认模型是"用完即弃"。
但预热复用带来新问题:状态污染。上一个执行留下的全局变量、文件、环境变量,会不会影响下一个执行?传统沙箱靠"销毁重建"天然规避了这个问题,大模型沙箱靠"复用"就必须自己解决。常见的做法是在复用前做一次状态快照回滚,或者把执行限制在一个干净的命名空间里,只复用底层的解释器进程。这两种做法各有取舍,后面会细说。
3.2 状态保持:多轮对话里,代码执行不是孤立的
这是最容易被低估的一点。传统沙箱假设每次执行是独立的、无状态的。但大模型的多轮对话天然是有状态的。用户第一轮说"帮我读一下这个 CSV",第二轮说"把上一轮读的数据按第二列排序"。如果每次执行都是全新的环境,第二轮就找不到第一轮读进来的数据了。
我见过两种处理方式。一种是把状态显式地序列化——第一轮执行完,把变量 dump 成文件,第二轮执行前再 load 回来。这种方式可控,但需要模型配合,而且序列化本身有开销和兼容性问题。另一种是保持一个长活的执行会话——同一个对话对应同一个解释器进程,变量自然保留。这种方式体验好,但资源占用高,而且会话超时、进程崩溃的处理很麻烦。
DeepSeek 这类方案倾向于后者,因为对话式交互的体验优先级最高。但这就意味着沙箱不能是"一次性的",它得是一个有生命周期的会话对象,能创建、能挂起、能恢复、能销毁。传统沙箱的 API 里没有"挂起"这个概念,你得自己在上面包一层。
3.3 结果回传:模型要的不是 stdout,是结构化的观察
传统沙箱执行完,返回的是 stdout、stderr、exit code。这对人来说够用了。但模型消费这个结果时,纯文本的 stdout 是很低效的——它得自己解析、自己判断哪部分是结果、哪部分是日志、哪部分是警告。
好的大模型沙箱会把执行结果结构化。比如:
{ "status": "success", "stdout": "...", "stderr": "", "exit_code": 0, "artifacts": [ {"type": "image", "path": "/tmp/plot.png", "mime": "image/png"}, {"type": "dataframe", "preview": "...", "shape": [100, 5]} ], "duration_ms": 52, "memory_peak_mb": 38 }模型拿到这个结构,能直接知道"哦,生成了图片,我该把它展示给用户",而不是从一堆 print 里猜。这个结构化层是传统沙箱完全没有的,因为它假设消费者是人。当消费者变成模型时,输出的信息设计目标就变了。
3.4 容错与自愈:模型会写错代码,沙箱得接得住
模型生成的代码出错是常态,不是异常。语法错误、运行时错误、超时、内存溢出,这些在传统沙箱里都是"失败",需要人工介入。但在大模型场景里,这些失败应该被自动转化为模型的下一轮输入,让模型自己修。
这就要求沙箱的错误信息是可操作的。比如IndexError: list index out of range这种报错,模型看到之后大概率能自己改。但如果沙箱返回的是Container exited with code 1,模型就懵了。所以大模型沙箱在错误捕获和格式化上要下功夫,把底层错误翻译成模型能理解的、带上下文的提示。
我踩过的一个坑:早期我用 subprocess 跑模型生成的代码,超时直接 kill 进程,返回一个笼统的 "timeout"。模型收到之后不知道是哪里超时,经常原样再生成一遍同样的死循环代码。后来改成在超时前捕获执行栈,返回"卡在哪一行、哪个函数",模型修正的成功率立刻上去了。这个细节传统沙箱文档里不会写,因为它的目标用户是运维,不是模型。
4. 重造一遍,具体重造了哪些东西
前面讲了"为什么现成的不够用",这一节讲"那到底重造了什么"。我不掌握 DeepSeek 内部的实现细节,所以这里讲的是基于公开信息和常见工程实践,一个合格的大模型沙箱应该具备的架构特征。你可以拿这个清单去对照任何号称"支持代码执行"的方案,看它是不是真的为大模型场景设计的。
4.1 执行单元从"容器"下沉到"进程 + 轻量隔离"
为了把冷启动压到百毫秒以内,很多方案会把执行单元从容器下沉到进程。具体做法是:预先启动一批 Python 解释器进程,每个进程加载好常用库,处于待命状态。执行请求来了,直接往空闲进程里投喂代码,跑完清理状态,进程回到待命池。
隔离强度靠什么保证?靠语言层面的限制 + 系统调用过滤 + 资源限制的组合。比如限制可用的模块白名单、拦截危险的系统调用、用 rlimit 限制内存和 CPU 时间。这种隔离强度不如 microVM,但对于"模型生成的、非恶意的、功能性的代码"来说,够用了。这是一个典型的威胁模型驱动的取舍:你不需要防住国家级攻击者,你只需要防住模型的手滑。
注意:这个取舍有前提。如果你的场景是"执行用户上传的任意代码",那进程级隔离是不够的,必须上容器或 microVM。DeepSeek 的场景是"执行模型生成的代码",威胁模型不同,所以可以下沉。别把这个结论无脑套到你的场景里。
4.2 会话生命周期管理:创建、挂起、恢复、销毁
前面提到多轮对话需要状态保持。实现上,一个会话通常对应一个执行上下文,这个上下文包含:解释器进程、工作目录、环境变量、已导入的模块、全局变量。会话可以处于几种状态:
- 活跃:正在执行或刚执行完,进程驻留内存
- 挂起:一段时间没活动,进程被冻结或序列化到磁盘,释放内存
- 恢复:收到新请求,从挂起状态唤醒
- 销毁:会话结束或超时,资源回收
这套生命周期管理是传统沙箱没有的。Docker 容器的生命周期是"创建-运行-停止-删除",没有"挂起-恢复"这个中间态(docker pause有,但语义不同,它冻结的是进程而不是释放资源)。大模型沙箱需要的是一个能低成本挂起、快速恢复的会话模型,这更接近 Jupyter kernel 的管理方式,而不是容器编排。
4.3 依赖解析:按需加载而不是全量预装
依赖问题前面提过。实际方案里,常见的做法是分层:
- 第一层:预装高频库(numpy、pandas、requests 等),保证大部分执行零等待
- 第二层:维护一个本地包缓存,模型请求的包如果在缓存里,秒级安装
- 第三层:缓存没有的,走网络安装,但加超时和降级策略
这个分层的关键是第一层的选品。选哪些库预装,直接决定了命中率。选多了镜像大、启动慢,选少了命中率低、经常走安装。这是一个需要根据实际流量数据持续调优的事情,没有一劳永逸的答案。我见过有团队用"最近 30 天执行日志里 import 频率 Top 50"来动态调整预装列表,效果比拍脑袋选好得多。
4.4 结果结构化与产物管理
执行产生的图片、文件、数据表,需要被收集、存储、生成可访问的引用,然后结构化地回传给模型。这涉及几个子问题:
- 产物识别:怎么知道执行产生了哪些文件?常见做法是给每次执行分配一个独立的工作目录,执行前后 diff 目录内容。
- 产物存储:图片存对象存储,返回一个 URL 或引用 ID。
- 产物预览:数据表给个前几行的预览,图片给个缩略图,方便模型判断内容。
- 产物清理:会话结束后清理,避免存储泄漏。
这套东西传统沙箱基本不管,因为它假设执行结果是文本。大模型场景里,产物管理是核心功能之一,因为模型经常需要"画个图给用户看"。
5. 自己搭一套时,我踩过的那些坑
这一节是纯经验分享。如果你打算自己搭一套类似的执行层,或者基于 deepseek harness 这类工具做二次开发,下面这些坑大概率会撞上。我按踩坑的先后顺序讲。
5.1 进程池的"僵尸状态"问题
用进程池复用解释器,最开始的实现是:进程跑完代码,清理全局变量,回到池子。但 Python 的全局状态清理没那么干净——已导入的模块还在sys.modules里,C 扩展的全局状态可能没重置,matplotlib 的 figure 可能还挂在内存里。跑了几百次之后,进程内存涨到几个 G,而且行为开始诡异。
解决办法是定期回收:每个进程跑 N 次之后强制销毁重建,N 根据内存增长曲线定。我实测下来 N=50 到 100 之间比较平衡,既摊薄了冷启动,又避免了状态累积。另外,清理时用gc.collect()强制垃圾回收,对 matplotlib 这类库还要显式plt.close('all')。
5.2 超时处理的"杀不干净"
模型生成的死循环代码,你用signal.alarm或线程超时去中断,经常中断不干净——因为 Python 的信号处理只在主线程的字节码边界生效,如果代码卡在 C 扩展里(比如 numpy 的大矩阵运算),信号根本进不去。最后只能上SIGKILL强杀进程,但强杀之后进程池就少了一个,得补。
更麻烦的是,强杀之后工作目录里可能留下半截文件,下次复用这个目录会读到脏数据。所以每次执行前要清空工作目录,而不是假设它是干净的。这个细节不注意,会出现"上一次执行的残留文件被这一次读到"的诡异 bug,排查起来很费时间。
5.3 内存限制的"软硬之分"
用resource.setrlimit限制内存,有软限制和硬限制。软限制触发时进程收到信号,可以自己处理;硬限制触发时直接 OOM kill。我一开始只设了硬限制,结果模型生成的代码一超内存就被杀,返回一个笼统的 "killed",模型完全不知道发生了什么。
后来改成软限制略低于硬限制,软限制触发时捕获MemoryError,返回结构化的错误信息:"内存超限,当前限制 256MB,建议优化数据结构或分批处理"。模型收到这个提示,修正率明显提高。这个技巧在传统沙箱文档里基本不会提,因为传统场景下 OOM 就是 OOM,运维去看日志就行。但大模型场景下,错误信息是给模型看的,得写得让它能行动。
5.4 网络访问的"开还是关"
模型生成的代码可能想访问网络——调个 API、下载个数据。开网络吧,安全风险大,而且可能被用来做坏事;关网络吧,很多有用的场景做不了。
我的做法是默认关闭,按需开启,且只允许白名单域名。具体实现是在网络层做拦截,而不是在代码层做限制(代码层的socket禁用很容易被绕过)。白名单根据实际业务配置,比如只允许访问公司内部的 API 网关。这个策略的代价是模型有时候会因为网络被拒而失败,但相比安全风险,这个代价可以接受。
这里有个判断标准:如果你的执行场景是"内部工具、模型生成的是数据处理代码",网络可以关。如果是"模型需要调外部 API 完成任务",那网络得开,但必须配白名单和审计日志。没有通用答案,看你的威胁模型。
5.5 文件系统的"共享还是隔离"
每次执行的工作目录,是共享一个还是各自独立?共享的话,会话之间会互相看到文件,有隐私问题;独立的话,多轮对话里上一轮写的文件下一轮读不到。
我的方案是会话级隔离,会话内共享。每个会话一个独立的工作目录,会话内的多次执行共享这个目录,会话之间互不可见。这样既保证了多轮对话的状态连续性,又避免了跨会话污染。实现上,工作目录的路径里带上会话 ID,执行前 chdir 进去,执行后不清理(留给下一轮),会话销毁时统一清理。
6. 这套东西对你的项目意味着什么
聊了这么多"为什么重造"和"怎么重造",最后落到实际:如果你正在做 AI Agent、正在接 deepseek 这类模型的代码执行能力,这些经验对你有什么用?
6.1 别自己从零造,但也别随便套
如果你只是想要一个"能跑代码"的能力,直接用现成的执行服务是最省事的。但如果你对延迟、状态保持、结果结构化有要求,现成方案大概率需要你包一层。包这一层的成本,取决于你对上面那些坑的认知程度。认知到了,可能几天就能搭出可用的;认知不到,可能几周都在填坑。
6.2 威胁模型决定架构,别抄错作业
最重要的一点:先想清楚你的威胁模型。是执行模型生成的代码,还是执行用户上传的代码?前者可以用进程级隔离 + 白名单,后者必须上容器或 microVM。抄别人的架构之前,先确认你们的威胁模型一样。我见过有团队照搬了进程池方案,结果他们的场景是执行用户上传的脚本,出了安全事故。这个教训很贵。
6.3 把"给模型看的错误信息"当成一等公民
这是最容易被忽略、但投入产出比最高的一点。花时间设计错误信息的格式,让模型能读懂、能行动。具体来说:错误类型要明确、出错位置要具体、修复建议要给到。这件事不需要多高深的技术,但需要你站在模型的角度想问题。做好了,你的 Agent 自愈能力会明显上一个台阶。
6.4 资源计量要能拆到"每次调用"
如果你要做成本控制或者配额管理,资源计量必须能精确到每次执行。这意味着你不能只依赖容器级的 cgroup 统计,得在执行层自己做埋点。每次执行记录:开始时间、结束时间、CPU 时间、内存峰值、网络流量。这些数据积累起来,既能做账单,也能做容量规划,还能发现异常执行(比如某个会话突然吃很多资源)。
我在实际项目里的体会是,这套执行层看起来是"基础设施",但它直接决定了上层 Agent 的能力边界和用户体验。沙箱成熟不成熟,取决于你拿它干什么。给传统服务用的沙箱确实成熟了,但给大模型用的沙箱,还在快速演化的阶段。DeepSeek 要重造一遍,不是重复造轮子,是因为轮子的规格变了。你要是也在做类似的事,希望上面这些踩坑记录能帮你少走点弯路。