ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

代码执行成了 Agent 标配:沙箱隔离怎么落到工程上

代码执行成了 Agent 标配:沙箱隔离怎么落到工程上 说明本文讨论的是 Agent 让模型写代码并实际执行时的执行环境隔离属于工程架构话题不涉及具体模型版本与价格。AI 领域版本迭代极快凡涉及版本号、价格、可用性请以你阅读时的官方页面为准。文中代码为结构示意请按自己的技术栈调整后再上生产。一、隔离的前提默认不信任每一个执行到 2026 年代码执行几乎成了 Agent 的标配能力。模型不再只输出一段文字或一次工具调用的参数而是直接写一段脚本由运行时执行、拿回结果、再决定下一步。这件事把 Agent 的能力边界往上抬了一截同时把一个原本属于系统安全的题目塞进了每个应用工程师的待办里。一种常见的自我安慰是模型写的不过是普通脚本能出什么事。这个假设站不住因为它把「模型大概率写对」当成了「每次都对」。代码执行的失控来源至少有四类性质完全不同。其一模型写的代码本身就有破坏性。不是它想搞破坏而是它按指令写字面意思。让它「清理一下临时文件」它可能写出对某个目录递归删除的语句让它「把结果写回配置」它可能覆盖掉你没备份的原件。这类风险的特点是意图正确、执行范围错误。损害大小取决于运行环境把什么暴露给了它而不取决于它是否怀有恶意。其二依赖供应链。一句 pip install 或 npm install 拉下来的不只是声明里的包还有包的安装脚本、传递依赖、以及构建阶段执行的代码。这个环节里任何一环被投毒运行时就等于把第三方代码请进了进程而你并没有读完那棵依赖树。其三无限循环与资源耗尽。一个循环条件写反了就能把 CPU 吃满一次性读入超大文件、递归展开能把内存打爆高频写日志能把磁盘写满。这类问题在模型侧根本看不出来——它写的语法完全正确只是运行时没有停下来。其四被执行代码读到了不该读的东西。这是最容易被忽略、后果又最重的一类。执行进程继承的环境变量里往往躺着访问密钥、数据库连接串、云账号凭据挂载进来的目录可能是宿主机的家目录它还能访问内网的元数据服务与内部接口。模型也许只是「顺手打印一下环境」密钥就离开了边界。把这四类放在一起看结论就清楚了隔离不是用来防「模型变坏」而是保证在模型偶尔写错、或被带偏时损害被限制在一个可接受的范围内。换句话说隔离是一个工程假设默认不信任每一次执行而不是假定每一次都对。二、三层边界文件系统、网络、进程与凭据边界要在创建执行环境时就定死而不是在执行过程中靠检测去拦。检测是兜底配置才是边界。三层里任何一层漏了另外两层都会被绕过去。文件系统。第一件事是决定挂什么。可写的位置通常只有一个临时工作目录宿主目录一律不挂需要输入的大文件走对象存储或只读挂载。这里有两个容易踩的细节一是临时目录的生命周期必须绑定到这次执行执行结束就销毁不能跨任务复用否则上一轮留下的文件会变成下一轮的输入二是共享的临时目录在多租户下等价于一条隐蔽的通信通道要不要共享要想清楚。只读挂载也要留意挂进来的不只是一份数据可能还有它的元信息权限、属主、软链软链指向宿主路径时只读边界会被绕开。网络。默认拒绝还是默认放行是这一层唯一但关键的决定。默认放行意味着任何一次执行都能连出去、把数据带走白名单就形同虚设默认拒绝则要求你把「这次执行允许访问哪些域名或网段」写成显式声明由环境去强制。白名单要跟着任务走不能做成一份全局大名单——全局名单会随着项目变多而失控最后退化成默认放行。出站的域名解析也要一起管否则解析本身就能泄露意图也能被当作隧道。进程与凭据。执行进程不要用 root也不要用能访问宿主资源的高权限用户能力集按需裁剪能不给就不给。这一层最常被忽视的是环境变量很多团队把密钥放在应用的配置里启动执行环境时又顺手把整个环境继承过去于是沙箱里随手打印一下环境变量就能拿到密钥。环境变量里不能有长期密钥这是硬约束不是建议。边界层要定的事典型错误安全侧默认文件系统挂载白名单、可写目录、临时目录生命周期挂宿主机家目录、临时目录跨任务复用只挂临时工作目录网络默认策略、出站白名单、域名解析默认放行、白名单做成全局大表默认拒绝、按任务声明进程与凭据运行用户、能力集、环境变量用 root 跑、继承全量环境变量非 root、能力裁剪、无长期密钥把这三层写成一份显式的执行规格比在代码各处写零散判断要可靠得多。规格是数据能被审阅、能被固化、能被强制执行⚠️ 代码待验证fromdataclassesimportdataclass,fielddataclassclassExecSpec:image:strworkdir:str/workspacemounts:listfield(default_factorylist)# 只挂白名单路径默认空writable:listfield(default_factorylambda:[/workspace])# 仅此处可写net:strdeny# deny | allowlistegress_allow:listfield(default_factorylist)# 仅当 netallowlist 生效env:dictfield(default_factorydict)# 显式注入不继承宿主环境user:strsandbox# 非 rootttl_seconds:int300# 环境存活上限结束即销毁defdefault_spec(image:str)-ExecSpec:# 安全侧默认无可写额外路径、无网络、无密钥、短生命周期returnExecSpec(imageimage)这段的重点不在字段多少而在默认值全部取安全侧不传参数得到的是能力最小的一份规格。要放开哪一项就显式写哪一项而放开这个动作本身应该留下记录。三、四道资源限为什么限时长必须同时限输出隔离解决的是「能碰什么」限额解决的是「能消耗多少」。两者独立一个文件系统边界收得很紧的沙箱照样能用一段死循环把宿主机的 CPU 占满。要限的有四样CPU、内存、磁盘、执行时长。它们各自的失控方式不同超限后的处置也不该一样。CPU。限的是核数与总时长不是瞬时占用。给足核数同时设总时长上限是更常见的做法——不少任务本身就是短时高并发的。这里要区分 CPU 时间与墙钟时间一个等待型的任务墙钟耗时长但 CPU 时间几乎为零用 CPU 时间做限会把它误杀用墙钟做限才符合「这次执行不能占着位置太久」的诉求。内存。内存超限最常见的表现不是报错是被操作系统直接杀掉进程收到信号就没了日志可能都来不及刷。所以内存上限要配一个接近上限的预警阈值在真正被杀之前先返回一个可读的错误否则你拿到的只有一句「进程退出了」。磁盘。既要限工作目录的写入总量也要限单文件大小。日志是最容易写爆磁盘的一类——模型写了个循环每轮都往日志里追加磁盘在几分钟内就满了而满盘影响的不只是这一个沙箱。执行时长。这一道最容易被单独使用也最容易出问题。只限时长、不限输出体积会留下一个窗口任务在超时前的一瞬间把一份几十兆的结果写到标准输出或写入文件。你以为限住了它的运行时间其实放它带出了一大坨东西接入方还要为这坨东西付解析与存储成本。所以限制执行时长的同时必须限制输出体积两者是一对。超限之后怎么处置也不止「杀掉」一种。把处置分清楚后续的排障和指标才有意义。资源限什么超限处置备注CPU核数 总时长墙钟杀进程返回可读错误用 CPU 时间会误杀等待型任务内存上限 预警阈值先预警超限再杀直接被杀往往来不及留日志磁盘写入总量 单文件大小拒绝写入返回错误日志循环是最常见的写爆来源时长墙钟上限 输出体积上限终止并截断输出不限输出体会抵消时长限制外部调用次数上限拒绝调用任务可降级防止沙箱内高频探测外网⚠️ 代码待验证sandbox_limits:cpu:cores:2wall_clock_seconds:300# 用墙钟而非 CPU 时间避免误杀等待型任务memory:limit_mb:2048warn_mb:1536# 接近上限先告警争取返回可读错误disk:workspace_mb:512single_file_mb:64output:stdout_bytes:262144# 输出体积必须与时长一起限artifact_total_mb:128on_exceed:kill_process:truereturn_readable_error:truecount_as_task_failure:false# 超时可降级重试不必一律记失败最后一项值得单独说一句把超限一律计为任务失败会让「这个任务本身就跑不完」和「这次分配的资源偏小」两类情况混在一起指标上分不开也就没法据此调参。可重试的超限和不可重试的失败要分开记。四、凭据不下发要联网取数据时怎么办上一章把出站默认设成了拒绝。但很多任务天然需要联网查一份文档、拉一批数据、调用一个内部接口。一放开网络凭据问题就跟着来了——沙箱里跑的是一段模型写的代码你不希望它拿到能长期使用的密钥。这里有三条路线代价各不相同。编排层代跑受控请求。沙箱执行模型代码时只允许它声明「我要请求哪个资源」真正的请求由编排层带着凭据去发结果再回给沙箱。这样沙箱里从头到尾没有凭据暴露面最小。代价是灵活度下降模型不能自己拼请求、不能处理分页与重定向这类逻辑这些都得在编排层预设。适合接口固定、调用方式可枚举的场景。临时短时凭据。给这次执行一枚作用域受限、有效期很短的凭据执行结束即失效。灵活度比上一条高模型可以自己写请求逻辑。代价是凭据仍然进了沙箱暴露窗口虽然小但存在而且短时凭据的签发本身要有一个可信入口这个入口不能又变成长期密钥的新去处。出站代理白名单。所有出站流量走一个受控代理代理持有凭据、按域名与路径做校验沙箱侧只看到代理地址。它把「凭据」和「网络策略」合在一起管运维上最集中。代价是代理成了单点要处理它的容量与可用性而且代理规则一旦写松「白名单」就名不副实。做法凭据是否进沙箱灵活度主要代价适用编排层代跑否低请求逻辑要预先枚举接口固定、调用可预测临时短时凭据是短窗中需可信签发入口调用方式多变、可容忍短窗出站代理白名单否中代理是单点规则易写松出站需要集中治理三种做法不互斥常见组合是「代理管通用出站编排层代跑高价值接口短时凭据留给确实需要自建请求链路的场景」。无论选哪种有一条底线不变长期密钥不进沙箱。把「代跑」落到代码上大致是这样的分工——沙箱只提交请求描述编排层校验后执行⚠️ 代码待验证ALLOWED{docs.read:(https://docs.internal.example,[GET]),metrics.read:(https://metrics.internal.example,[GET]),}defproxy_request(handle:str,path:str,method:str,bodyNone):ifhandlenotinALLOWED:return{ok:False,error:handle_not_allowed}base,methodsALLOWED[handle]ifmethod.upper()notinmethods:return{ok:False,error:method_not_allowed}# 凭据只在这一层出现沙箱侧永远拿不到它resphttp_call(basepath,method,bodybody,authload_secret(handle))return{ok:True,status:resp.status,body:truncate(resp.body)}这里的 handle 是编排层预先登记的别名不是由沙箱自由拼接的完整地址。让沙箱能自由指定地址等于把白名单交还给被隔离的一方白名单也就不再是边界。五、产物怎么回传三条通道别把大文件塞回上下文执行完结果怎么带出沙箱是一个经常被欠考虑的问题。最容易犯的错误是让沙箱把一切结果都打到标准输出再由编排层原样塞进模型上下文。这条路在结果小的时候没问题一大就崩。产物分三类通道不同处理方式也不同。文件产物。报告、图表、生成的数据文件、改动后的代码。这类不该走上下文而应该从沙箱的临时工作目录直接搬到对象存储或制品目录编排层只拿一个可访问的引用地址加大小加校验值。往上下文里塞一份几兆的文本除了消耗 token还会把真正重要的结论挤到不显眼的位置。搬运时要校验大小与摘要防止「沙箱说文件写好了、其实没写完」。结构化产物。让模型输出的关键结论、状态、下一步建议应当要求它写成一个约定字段名的 JSON而不是一段自然语言。约定字段名的好处是编排层能直接读、能校验、能落库自然语言则要再解析一次又多一层不确定性。结构化产物的体积要有上限超过就截断并标记不要把截断当成无事发生。日志。执行日志分两类用途给模型看的少量诊断信息和给人看的完整运行记录。前者要精简只留出错位置与关键参数后者走另外的通道直连日志系统不进上下文。把完整的运行日志塞回模型是最常见的 token 浪费来源之一。产物类型回传通道编排层拿到什么常见错误文件工作目录 → 对象存储引用地址/大小/摘要把大文件内容塞进上下文结构化约定字段的 JSON可直接读与校验的对象用自然语言代替结构化字段诊断日志精简后进上下文出错位置与关键参数把全量日志喂给模型完整日志直连日志系统供人排查的链路与诊断日志混在同一条通道回传这一步要顺手把体积管住。下面这段把「结构化产物 文件搬运 输出截断」放在一起⚠️ 代码待验证MAX_STDOUT32*1024# 超出即截断并标记不静默丢弃MAX_ARTIFACT128*1024*1024defcollect(result):out{}structuredresult.get(report)# 约定字段直接可用ifstructuredisnotNone:out[report]structured artifacts[]forfinresult.get(files,[]):iff.sizeMAX_ARTIFACT:artifacts.append({name:f.name,skipped:too_large,size:f.size})continueuriupload_to_store(f)# 文件不进上下文只回引用artifacts.append({name:f.name,uri:uri,size:f.size,sha256:f.sha})out[artifacts]artifacts rawresult.get(stdout,)iflen(raw.encode())MAX_STDOUT:out[stdout]raw.encode()[:MAX_STDOUT].decode(errorsignore)out[stdout_truncated]True# 截断必须显式标记else:out[stdout]rawreturnout标记截断这件事不能省。一段被悄悄截断的输出模型会当成完整结果继续往下推理错误会一路传下去而且中途没有任何地方会报错。完整版资料清单本文用到的沙箱边界配置与限额模板都整理在里面了扫码即可获取六、自建容器还是用托管沙箱边界和限额想清楚之后会落到一个采购题自己用容器技术搭一套执行环境还是用托管沙箱。这不是立场问题是五个维度的取舍。隔离强度。自建若只做命名空间隔离、共用宿主内核逃逸面要比基于独立内核或虚拟化的方案大。托管沙箱通常在这一层做得更重但你要接受它的具体实现不透明。判断标准不是「谁更安全」而是「你的执行内容有多不可信」——如果跑的是模型随机生成的代码就该更偏向强隔离。冷启动。每次执行都新建环境最干净但冷启动会占掉可观的时间。自建可以通过预热池缓解代价是池子里的环境可能被复用隔离性下降。托管方案一般帮你处理了这层但冷启动表现要按自己的负载实测不能只看文档。并发成本。隔离越强单实例成本越高。并发量大的场景要把「峰值并发乘单实例成本」和「平均并发乘单实例成本」分开算用平均值得出的结论会低估峰值时的开销。可观测性。自建的好处是你能把它接进自己的 trace 与日志体系出问题能定位到具体的环境实例托管方案通常只暴露有限的接口超限、被杀这类事件要确认它是否回传、回传什么字段。运维责任。自建要自己养一套「创建、隔离、限额、回收、巡检」的流程还要跟内核补丁托管把这部分转成了服务方的责任但引入了对服务方可用性与策略变更的依赖。维度自建容器托管沙箱隔离强度取决于内核隔离方案共享内核时较弱通常更强但实现不透明冷启动可控靠预热池缓解复用会降隔离性由服务方处理需按自身负载实测并发成本峰值成本需单独核算通常按用量计费弹性较好可观测性可接入自有体系定位到实例字段有限需确认超限事件回传运维责任补丁、巡检、回收全自担转由服务方承担依赖其可用性有两种场景两边都不合适。一是执行内容几乎完全不可信、又不接受任何外部依赖——自建难在隔离强度托管难在不能自控这时候只能缩小能力面不联网、只给临时目录、只跑纯计算把风险压到能力层面而不是指望隔离方案兜住一切。二是执行延迟要求在毫秒级——任何「新建环境再执行」的模型都太慢可行做法是把执行范围压到一组预先审定过的表达式或纯函数而不是通用代码执行。不要为了保住「能跑任意代码」这个能力去牺牲隔离强度。⚠️ 代码待验证需要一个执行环境 ├─ 执行内容是否高度不可信 │ ├─ 是 → 优先强隔离若同时要求不依赖外部服务 → 缩小能力面别硬上通用执行 │ └─ 否 → 进下一步 ├─ 并发是否大且峰谷差异明显 │ ├─ 是 → 倾向托管按用量走 │ └─ 否 → 自建预热池即可 └─ 是否必须接入自有 trace 与审计 ├─ 是 → 自建的收益更大 └─ 否 → 按成本与团队运维能力定七、落地顺序与观测这套东西不适合一次全开。放开的能力越多出问题的面越大所以顺序本身也是一种安全措施。第一阶段只读环境跑通。沙箱不给网络、不给可写目录只让模型写的代码能读输入、算出结果、把结论以结构化形式带出。这一步验证的是「代码能不能跑通、产物通道通不通」与能力放开无关。很多团队卡在这里才发现问题往往不是沙箱而是产物回传的字段没对齐。第二阶段放开写临时盘。允许在临时工作目录里写文件、生成中间产物仍然不给网络。这一步要同时把前文的四道限额配上观察超限终止的比例据此调参。临时目录的生命周期在这一阶段最容易出问题要确认它确实随执行销毁。第三阶段才考虑联网。先只对少数已登记的资源放开走出站代理或编排层代跑不要一上来就开全量白名单。联网是风险最高的一步也是收益最直接的一步把它放在最后是为了让前两阶段建立的观测能力先就位。四个指标要一起看。单看任何一个都会误导。沙箱创建耗时按分位看均值会被大量小任务拉平。它直接决定交互式体验也是判断该不该上预热池的依据。超限终止率按资源类型拆分。CPU 超限多说明模型写的逻辑容易失控内存超限多往往指向输入数据过大要分开处理。出站拦截次数这个数字突然上升通常意味着有任务在尝试访问未登记的资源。它是要看的信号不是要压下去的错误。产物回传成功率它掉下去多半是文件没写完就搬走或大小校验没过。这一项低前面跑得再顺结果也传不出来。⚠️ 代码待验证sandbox_metrics:-name:sandbox_create_durationunit:secondsslice:[image,pool_warm]note:看分位不看均值决定是否上预热池-name:limit_terminated_rateunit:ratioslice:[cpu,memory,disk,wall_clock]note:按资源拆分不同资源指向不同根因-name:egress_blocked_countunit:countnote:上升是信号说明有任务在访问未登记资源-name:artifact_delivery_rateunit:rationote:掉下去多为搬运时文件未写完或校验未过把「只读跑通、放开临时盘、最后联网」这个顺序和这四个指标固定下来代码执行就从一个看起来很酷的能力变成了一个可以被限、被观察、被回退的工程组件。至于读到不可信的内容之后该怎么约束动作那是另一条线上的问题不在这里展开。完整版资料清单本文用到的沙箱边界配置与限额模板都整理在里面了扫码即可获取附表 A关键取舍一览本文涉及的所有工程判断集中在这里方便按需回看。取舍本文结论判断依据位置隔离的前提默认不信任每一次执行模型偶尔写错的损害要靠边界兜住第一章依赖安装视为引入第三方代码安装脚本与传递依赖都会执行第一章边界何时定创建环境时定死检测是兜底配置才是边界第二章文件系统只挂临时工作目录临时目录跨任务复用会污染下一轮第二章网络默认默认拒绝、按任务声明全局大名单会退化成默认放行第二章环境变量不放长期密钥继承宿主环境等于把密钥送进沙箱第二章CPU 限额用墙钟而非 CPU 时间CPU 时间会误杀等待型任务第三章内存限额配预警阈值直接被系统杀掉往往不留日志第三章输出体积与执行时长一起限不限输出会抵消时长限制第三章超限记账可重试的超限不记任务失败混记会让指标分不开根因第三章凭据路线长期密钥一律不进沙箱三条路线的底线一致第四章代跑请求沙箱只提交 handle自由拼接地址等于交回白名单第四章大文件产物只回引用不回内容塞进上下文既费 token 又淹没结论第五章截断处理必须显式标记静默截断会被模型当成完整结果第五章托管与自建按执行内容的不可信程度定隔离强度与冷启动互相牵制第六章不合适的场景毫秒级延迟不做通用执行新建环境再执行的模型太慢第六章放开顺序只读、临时盘、再联网顺序本身也是一种安全措施第七章附表 B术语速查表术语含义沙箱把不可信的代码限制在受限资源与权限内运行的执行环境执行规格描述一次执行挂载、网络、用户、限额的显式配置数据安全侧默认不传参数时得到能力最小的一份配置放开项需显式声明出站白名单仅允许访问显式登记域名或网段的网络策略能力裁剪削减进程可用的系统能力只保留任务必需的部分临时短时凭据作用域受限、有效期很短执行结束即失效的访问凭据编排层代跑由编排层持凭据发起受控请求沙箱侧不接触凭据出站代理统一承载出站流量、集中持有凭据与校验规则的中间层墙钟时间从开始到结束的实际流逝时间区别于 CPU 占用时间预警阈值接近资源上限时先触发告警争取返回可读错误结构化产物以约定字段名输出的 JSON可直接读取与校验产物引用指向对象存储中产物的地址与摘要替代把内容塞回上下文预热池预先创建好的执行环境池用于降低冷启动耗时写在最后这篇用到的资料写这篇文章时我把几个代码执行方案的隔离配置、限额参数和实测记录都对了一遍顺手整理成几份配套的东西大模型学习路线图从 LLM 基础到 Agent 开发各阶段该学什么、用什么资料大模型全套教程按主题分好的视频与文档清单大模型实战好书24 本附每本适合的阶段资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「大模型」优先通过。拿到之后建议先看学习路线图那一份先定位自己在哪个阶段再决定学什么比一上来就啃框架效率高得多。
返回列表