上个季度,我把公司内部的 Agent 项目从“裸奔”状态彻底搬进了隔离执行环境:模型推理层挂到 DeepSeek Elastic Compute 这类弹性算力服务上,工具调用与代码执行全部收敛到 UCloud Agent Sandbox 里。整个过程踩了不少坑,也把“Agent 安全”从口号变成了可落地的架构方案。这篇就顺着这条迁移路径,聊聊为什么 Agent 执行环境必须隔离开、沙箱选型怎么考虑、以及落地时最容易翻车的几个细节。适合正在做 Agent 开发,尤其是被“agent execution terminated due to error”和并发问题折磨过的大模型应用工程师参考。
1. 为什么 Agent 执行环境必须“隔离开来”
1.1 Agent 的失败模式比普通服务多得多
很多人第一次写 Agent 时,想得很简单:把大模型 API 一接,写个循环让模型决定下一步调什么工具,然后就在一台云主机上把代码跑起来。这个状态看起来“能跑”,但它离“可控”非常远。
普通后端服务通常输入输出都是结构化数据,逻辑确定、边界清楚。Agent 本质上是个“不确定执行器”:大模型输出的下一个动作,可能是调一个工具,可能是执行一段代码,可能是向某个外部地址发起请求。这中间每一步都可能出问题。比如测试时模型“突发奇想”生成了一个清理命令,如果执行环境没有隔离,后果就是直接作用于宿主机文件系统;比如 Agent 读取了一个外部网页,网页里嵌入恶意指令,通过提示词注入引导模型向某个内网地址发请求;再比如某个工具调用返回了大量内容,污染了上下文,模型开始循环重试同一个失败动作,白白耗掉大量算力和 token。
类比一下,普通服务像是在流水线上操作固定机器,Agent 则像一个你刚招来的实习生,能力很强但没有经验,你不可能让他直接接触生产库,而是会给他一个受控环境、一套操作规范和一本“什么不能做”的清单。“隔离执行环境”就是这个受控空间。
1.2 从“同机部署”到“分而治之”
我们项目早期的架构,现在回头看相当粗糙:一台带公网 IP 的云主机,上面同时跑着模型调用代码、Agent 循环、工具执行逻辑,甚至数据库也在这台机器上。好处是简单,坏处是所有的故障和安全风险都耦合在一起。
模型推理本身是极吃资源的。我们刚开始用的是通用 CPU 实例跑小模型,后来切到 DeepSeek Elastic Compute 这类弹性算力服务,把推理负载放到单独的算力集群上,才发现原来那种“模型一推理,Agent 就卡顿,CPU 被打满,偶发的内存抖动直接拖垮整个服务”的状态有多难受。再后来,工具执行也被要求单独收敛,不能继续放在那个有公网 IP、能访问内网数据库的主机里。整个演进的思路就是“分而治之”:模型推理归模型推理,Agent 编排归 Agent 编排,代码/工具执行归执行沙箱。
这三层分离之后,每一层都可以独立扩容、独立审计、独立设限。算力不够,扩 DeepSeek Elastic Compute 那边的实例;执行任务太多,给沙箱层加并发;安全上出了问题,可以基于沙箱的任务级快照回滚,而不是重装一台机器。
1.3 隔离层到底要解决哪几件事
一个真正可用的 Agent 隔离层,至少要覆盖以下五件事。
第一,文件系统隔离。Agent 读写到的文件必须是虚拟出来的独立空间,不能直接落在宿主机路径上。即使 Agent 发了删除操作,也只是删掉临时目录里的数据,不会影响系统文件和相邻租户的数据。
第二,网络隔离。默认禁止 Agent 访问内网、访问云元数据接口,出网请求走白名单或代理审查。这一点能挡住绝大多数“拿到凭证后乱扫内网”的自动化攻击。
第三,进程与权限隔离。Agent 进程必须以低权限身份运行,不能有 root 权限,不能随意挂载敏感目录。沙箱内禁止创建特权容器或切换用户。
第四,资源配额。CPU、内存、磁盘、文件句柄、进程数都要有上限。没有配额的 Agent 一旦进入死循环,能把整台宿主机的资源吃干。
第五,生命周期管理。每次 Agent 任务执行完,环境应该被清理或重置。一个长期运行的 Agent 会话如果一直保留在同一个沙箱里,内部积累的临时文件、环境变量残留、被污染的会话状态会越滚越大,很难排查。
2. Sandbox 选型:为什么从“虚拟机”走到“任务级沙箱”
2.1 虚拟机的问题:粒度太粗、成本太高
如果只是要隔离,最朴素的想法自然是虚拟机:给每个 Agent 开一台 ECS,装上环境,用完销毁。但实际跑一段时间就会发现,虚拟机是“为了倒杯热水开了一间厨房”,太重了。
虚拟机粒度太粗,一个 Agent 任务往往只需要几百 MB 内存,却要为一个完整操作系统付钱。创建速度也慢,Agent 这种短则几秒、长则几分钟的任务,不可能每次都等一台新机器完成系统初始化,因此只能做实例池常驻。可是如果做常驻池,又回到了“环境复用带来污染”的老问题:上一个任务的凭证、临时文件、环境变量可能被下一个任务读到。
更严重的是,虚拟机的安全边界虽然强,但运维负担完全不匹配。每个镜像要打补丁,每个实例要管理 SSH,每台机器上的 Agent 运行日志要单独采集。我们的运维资源很有限,这套方案在任务量上来之后很快就撑不住了。
2.2 容器和函数计算的优势与盲区
容器确实比虚拟机轻很多,启动快、资源开销小,是大家最容易想到的方案。但容器的隔离性弱于虚拟机,多个容器共享宿主机内核,一旦内核层出现漏洞,“沙箱逃逸”就不是理论问题。对内部工具类 Agent 还好,如果需要运行外部第三方代码或处理不可信数据,容器方案需要额外叠加 gVisor、Kata 之类的手段,部署复杂度立刻上升。
函数计算则是另一个极端。它对短任务非常友好,按调用计费,天然弹性,但 Agent 任务的典型特征恰恰是“长尾”:一次深度分析可能要跑十几分钟,中间还要多次等待模型响应;很多时候还有状态,比如浏览器自动化会话、数据处理中间态。函数计算的时长上限、内存上限和冷启动延迟,在这些场景里都不太顺手。
容器做轻量隔离但不够安全,函数计算弹性好但不适合有状态的长任务,虚拟机安全但太重。要兼顾这些,只能找面向 Agent 场景设计的执行沙箱服务,而不是继续在通用基础设施上叠方案。
2.3 UCloud Agent Sandbox 这类托管沙箱的不同
我们最终落到 UCloud Agent Sandbox 上,核心原因是它把“Agent 场景”的语义直接做进了隔离机制,而不是让我自己用底层工具包一个个手搓。
它在隔离层面仍然是轻量沙箱,但与通用容器不同的是,它会按“任务”来管理生命周期:提交一个执行请求,沙箱创建并运行,拿到结果后整个环境销毁。没有常驻实例,没有环境残留,你不需要操心镜像补丁和实例池,每个任务之间天然相互隔离。
对网络策略、密钥注入这类 Agent 常用能力,它是原生支持的。我可以直接声明“这个 Agent 可以访问哪些域名”“哪些环境变量允许注入”“最大执行时长多少”,而不是自己在容器启动脚本里拼 iptables 和 env 配置。审计日志也是可选的,每次执行的输入输出、退出码、网络行为都会被记录下来,这对排查 Agent 被攻击或误操作非常关键。
同时它和 DeepSeek Elastic Compute 这类弹性算力服务是天然互补的。推理服务负责“思考”,沙箱服务负责“动手”,两边通过 API 或队列异步衔接,Agent 编排层只做调度,不需要关心具体脚本在哪台机器上执行。我们的架构在切换后,安全性和运维效率同时上了一个台阶。
3. 构建安全可控执行环境的关键设计
3.1 网络策略:先默认拒绝,再按需放行
Agent 沙箱的网络策略是我现在最看重的配置项。很多人会把白名单设得很宽,“先跑起来再说”,这个心态是隐患。沙箱的价值恰恰在于你敢把 Agent 放进一个默认没有网络的环境里,然后一条一条把必要的能力加回来。
举例来说,一个做“网页信息整理”的 Agent,通常只需要访问目标网页所在的域名和搜索接口;一个做表格分析的 Agent,可能根本不需要出网;一个接外部 API 的 Agent,只需要访问那个 API 的域名。这些都应该在白名单里写清楚,而不是直接给沙箱开一个 NAT。
另一个重要原则:禁止沙箱访问云元数据服务地址。很多云平台都有一个固定的内网地址可以拿到临时凭证,如果 Agent 被提示词注入,第一步往往就是去访问这个地址偷凭证。在网络策略里显式阻断这类访问,等于拆掉了一个高危弹药。
网络策略本身要支持变更审计。我在实际运维中发现,Agent 功能迭代很快,今天要接一个新 API,明天要换个数据源,白名单会经常动。如果每次变更都记录下来,出问题时能知道是哪个策略调整导致了执行失败,排查效率会高很多。
3.2 凭证与密钥管理:别把密钥写进镜像
另一个高发问题是把密钥直接塞进 Agent 镜像或启动脚本里。镜像会被复用、被拉取、被留存,密钥一旦进镜像,就相当于把保险柜密码刻在门外面。
正确做法是运行时注入,并通过最小权限来控制。UCloud Agent Sandbox 这类服务通常支持把密钥作为环境变量或挂载文件在启动时注入,只在这个任务的执行周期内有效。密钥的权限范围也要遵循最小化原则:这个 Agent 只需要读对象存储,就只给读权限;只需要调用某个 API,就只给该 API 的访问密钥,不要图省事直接给一个拥有大权限的凭证。
另外一个很容易被忽略的问题:Agent 可能会把密钥泄露进对话上下文中。例如某个工具返回了错误信息,里面带着完整的访问密钥,模型看到后可能把它写进回答或者写进日志文件。因此密钥不仅要保护不被外部拿走,还要防止它在沙箱内部被“打印”出来。我们会在审计日志里做脱敏处理,对看起来像密钥的字符串做掩码,避免密钥在日志里裸奔。
内存型和文件型密钥也要区分。临时凭证尽量通过沙箱注入,不要依赖全部 Agent 都能读到的私有镜像。如果必须写在文件里,也要确保文件权限是 600 且仅对该任务用户可读。
3.3 资源配额与超时控制
没有配额限制的 Agent 沙箱,就像一个没有刹车的车。我们的实践是给每个执行任务设三档限制:单次工具调用的超时、单个任务的执行总时长、资源配额(CPU/内存/磁盘)。
单次工具调用一般设 30 到 60 秒比较合理。很多 Agent 卡死不是模型问题,而是某个工具调用直接挂起,比如 HTTP 请求没有设置超时、子进程等待输入。给每步加超时,才能避免一个工具调用拖死整个任务。
任务级超时要根据业务来定。简单查询任务可以给 5 到 10 分钟,复杂分析任务给 30 分钟甚至更久。超时之后不要默认重试,要先拿到错误日志判断是偶发还是必现。很多团队在 Agent 反复超时之后习惯性调大超时时间,其实只是在掩盖底层问题。
资源配额方面,内存是最容易发生 OOM 的。例如网页处理类任务,如果页面太大或读取的文本太长,内存会被迅速耗光。建议给任务设内存上限,让沙箱执行器直接把超限任务杀掉,同时记录当时的内存占用,保留现场。磁盘也是一样,Agent 写临时文件的量可能超出预期,不设磁盘上限会把宿主机磁盘打满。
最后是“什么情况下允许重试”。Agent 天然不擅长度量自己的失败,编排层需要有政策约束。对于幂等操作,超时可以重试,但最多两三次;对于非幂等操作,比如支付、发消息、写数据库,绝不能自动重试。这一点必须在沙箱外的编排层做好检查。
3.4 并发模型:AI Agent 怎么扛住高并发
很多团队第一次把自己 Agent 项目放到线上,都会遇到同类问题:并发一上来,执行环境直接被打崩。这背后往往是同步等待模型响应导致的线程/进程耗尽。
模型推理通常需要几秒到几十秒,如果 Agent 编排层是同步调用,一个请求就会占住一个执行单元很久,并发自然上不去。需要把执行过程拆成异步流水线:Agent 编排层规划好下一步后,把任务扔进队列,沙箱执行器消费队列并执行,执行完再把结果回传到编排层。DeepSeek Elastic Compute 那边的推理请求本身也应该做队列削峰,不然突发流量会直接打爆限流。
另外要区分“推理并发”和“执行并发”。推理并发取决于算力服务的配额,执行并发取决于沙箱实例池的规模和配额。二者都会成为瓶颈,但通常是不同时间爆的,需要分别监控和扩容。实际中我们常用的指标是两类:推理侧的 P95 延迟和 token 消耗速率,执行侧的排队长度和任务失败率。队列长度飙升,先看看是不是执行沙箱消费太慢;token 消耗异常增长,再回去看是不是模型陷入了重复调用。
4. 从 DeepSeek Elastic Compute 到 UCloud Agent Sandbox 的落地实操
4.1 架构总览
我们现在跑的生产架构大致分四层。最外层是 Agent 编排服务,负责会话管理、规划调度、流程控制;它不直接执行代码,只负责“决定做什么”。第二层是推理层,挂在 DeepSeek Elastic Compute 这类弹性算力服务上,接收编排服务发来的推理请求并返回结果;这里只看 token 和算力,不接触业务执行环境。第三层是执行层,也就是 UCloud Agent Sandbox,负责真正跑代码、调工具、解析文件;每次执行都是新的沙箱实例,执行完销毁。最底层是数据层,包含向量库、对象存储、消息队列和数据库,沙箱通过受控的网络策略去访问它们。
这个架构里,Agent 编排服务相当于大脑,DeepSeek Elastic Compute 负责思考,沙箱执行层是手脚,数据层是仓库。每一层各司其职,安全边界和资源配额才能在每一层单独施展开。
4.2 准备推理服务
如果是从零接入,首先要把模型推理服务独立出来。以 DeepSeek Elastic Compute 为例,核心工作是确认三件事:API 地址与模型版本的对应关系、token 用量监控的开启情况、限流阈值的设定。
API 地址最好放在编排服务的配置中心,不要硬编码在代码里,因为模型版本升级时地址或参数可能有变化。Token 用量监控一定要提前开,很多人一开始不看这个,直到月底对账单才发现某个测试 Agent 循环调用了几十万次,费用超支。
限流阈值方面,按照业务预估的峰值流量再上浮 20% 通常比较稳妥。设得太低会在业务高峰期误伤正常请求,设得太高则失去保护意义。另外还要确认推理服务返回的错误码语义,比如限流返回什么、超时返回什么,编排层要根据不同的错误码决定是退避重试还是快速失败。
4.3 创建沙箱与提交执行
接入 UCloud Agent Sandbox 时,先理解它处理任务的粒度。一次沙箱执行对应一个可独立运行的操作,例如“运行一段 Python 脚本”“执行一个命令行工具”“调用一个浏览器自动化任务”。这和传统“开一台虚拟机跑服务”的思路不同,如果你还在用启动一个长驻进程的思路去设计沙箱接口,就会觉得非常别扭。
一个典型的提交请求包含下列信息:执行类型、执行的代码或命令、资源配额、网络白名单、环境变量注入、超时时间。提交后,服务返回一个任务 ID,编排层用这个 ID 轮询或订阅执行状态,直到拿到结果。
我们还会在编排层做一层统一封装,把沙箱执行包装成一个“安全工具”。Agent 在定义工具时,本质上就是描述“这个工具会执行什么、允许访问什么网络、可以用多少资源”。这样模型看到的工具列表是受限的,它只能在编排层暴露出的工具集合里选择和调用,而不是直接写任意代码。
4.4 配置放行规则和密钥示例
下面给一个我们实际用过的配置片段(做脱敏处理),可以帮助你理解沙箱层面的配置粒度:
sandbox: name: "web-content-snapshot" runtime: "python3.11" resources: cpu: 1 memory_mb: 1024 disk_mb: 2048 timeout_sec: 120 network: egress_allow: - "api.example.com" - "www.example.org" deny: - "169.254.169.254" # 云元数据地址,必须阻断 env: - name: "OUTPUT_OBJECT_STORE" value_from_secret: "obj_store_key" audit: log_input_output: true log_network_requests: true mask_sensitive: true注意几个细节。网络白名单里只放了业务必需的域名,没有放通所有 HTTPS 出站;元数据地址显式阻断,这是安全红线。密钥通过 secret 引用注入,而不是直接在配置里写明文。审计日志默认打开输入输出和网络请求,同时做敏感信息掩码。
实测下来,这种“严格默认 + 按需放行”的配置对排错很友好。Agent 执行失败时,先看审计日志里网络请求是否被拦截,如果被拦截就说明白名单配置漏了,补上即可;如果是代码错误,再到执行日志中定位。大多数 agent execution terminated due to error 都是配置层面的小问题。
4.5 日志与审计的落地方式
日志是沙箱方案的命脉。没有日志,所有隔离和安全策略都等于盲人摸象。
我们为每条执行任务生成一个 trace ID,贯穿编排层、推理层、沙箱执行层。你在沙箱日志里看到 trace_id,就能回到编排层找同一个 ID 的规划记录、模型调用记录、工具选择记录。这样定位一次失败,要么是模型选错了工具,要么是沙箱执行中途失败,不会再出现“不知道是哪一层出的问题”的情况。
沙箱侧的审计日志要保留一段时间,最好能满足至少 30 天的追溯需求。如果合规要求更高,把日志接入对象存储做冷备。日志的存储本身也要注意安全:审计日志里可能包含部分敏感信息,虽然做了掩码,但日志文件本身的读取权限依然要收缩到运维组,不能对所有人开放。
5. 常见问题与排查技巧实录
5.1 “agent execution terminated due to error”到底是谁触发的
这是 Agent 开发群里出现频率最高的一条报错。它本身只是一个笼统的“执行被终止”提示,并不直接告诉你原因。我见过有人对着这个提示折腾了两天才发现,就是资源配额里 memory 设置太小,脚本加载了一个大文件后直接 OOM 被杀。
建议排查路径是固定的:先看任务级错误码,区分出是超时、OOM、被安全策略拦截,还是执行代码自身异常;然后看审计日志中该任务的资源使用曲线,确认是否触碰了配额上限;再看网络策略拦截记录,确认是否某个请求命中了 deny 规则;最后才回到代码本身看是否有异常堆栈。
最容易忽略的是“被主动终止”和“代码出错”的区别。代码出错是执行进程自己退出;被主动终止则是安全沙箱按策略杀掉的。前者的修复方式是改代码,后者的修复方式是调整沙箱配置。千万别一上来就盲目加内存或加超时。
5.2 并发一上来就崩:瓶颈到底在哪
并发问题的第一个坑是盲目扩沙箱实例数。如果你上游的模型推理层限流没放开,沙箱扩再多,也只是把任务从“排队执行”变成“排队等模型”,整体吞吐没有本质提升。正确做法是先做容量测试,固定一个并发值,分别压测推理层和执行层,画出每一层的吞吐曲线,找到真正的瓶颈后再扩容。
第二个坑是任务重试风暴。当一个慢任务超时后,编排层自动重试,结果所有任务几乎同时超时,同时重试,直接把推理层打爆。缓解办法是给重试加抖动和退避,同时限制全局最大并发重试数。可以设置一个执行队列长度阈值,高于阈值时新请求进入“待调度”而不是直接触发沙箱创建。
第三个坑是沙箱任务与编排线程的耦合。如果编排层是同步等待沙箱返回结果的,那么沙箱变慢会让编排层的线程全部被占住。我们后来把沙箱提交改成了 fire-and-callback 模式:提交任务后立即返回,结果通过回调或轮询方式取。这样编排层的线程利用率大幅提高,同一批实例能承载的会话数翻了数倍。
5.3 沙箱里的数据到底怎么保存
一个常见的误解是:沙箱有磁盘,那 Agent 的中间结果先写到沙箱本地,等任务结束后再读取。实际上沙箱是任务级生命周期,执行完环境就销毁了。如果中间产物只写在沙箱本地磁盘,任务结束后数据就丢了。
我们的实践是:所有需要留存的数据在任务执行期间就直接写回对象存储。例如网页抓取任务,原始 HTML 可以压缩后直接上传到对象存储,沙箱本地只保留正在处理的部分;向量化后的内容写入向量库;结构化结果写入数据库。沙箱本地磁盘只放临时文件,用完即焚。
这样做还有一个好处:沙箱自身变成完全无状态的。无状态的执行环境更适合大规模横向扩容,也更安全,因为你不需要担心一个沙箱残留的数据被下一个任务意外读到。
Agent 的记忆也不应该依赖沙箱存储。记忆要放在编排层能访问的向量库或会话存储中,沙箱每次拿到的是“当前任务的上下文切片”,而不是整个会话历史。要记住:沙箱是执行空间,不是存储空间。
5.4 密钥在沙箱里泄露了怎么办
密钥泄露在 Agent 环境里非常隐蔽。有一次我们通过审计日志发现,一个 Agent 在调用外部 API 时,服务端返回了错误信息,其中包含了完整的访问密钥,模型错误地把这段信息写入了日志。日志系统做掩码处理后,运维才注意到这个异常。
处理方式是三层防护:第一层,密钥本身要最小化授权,即使泄露,损失可控;第二层,审计日志要对疑似密钥的字符串做脱敏,防止二次扩散;第三层,定期轮换密钥。同时还有一条非常有效但容易被忽略的规则:开发环境和生产环境的密钥必须完全隔离,且生产环境密钥永远不能出现在测试日志和本地调试日志里。
如果你的沙箱服务支持秘密审计(记录某个密钥被哪些任务使用过),建议开通。出问题时,能快速定位泄露面到底有多大,而不是靠猜。
5.5 Agent 框架/Harness 与沙箱到底是什么关系
可能有人会疑惑:我用的 Agent 框架里已经有工具调用封装了,还需要单独上沙箱吗?需要搞清楚一个概念:框架解决的是“怎么编排”的问题,沙箱解决的是“在哪里执行”的问题。
一个 Agent 框架(或者说 harness)负责的是控制循环:模型选择工具、参数填充、上下文管理、结果回填。这些流程跑在编排服务里,不需要沙箱。但你实际执行工具调用、运行代码、访问外部系统时,就进入执行域了,这一步需要沙箱保护。
如果把两者混淆,很容易出现“框架里直接调了一个本地函数去删除文件”这种操作。框架层可以允许模型选择“删除文件”这个工具,但沙箱决定了“删除哪个目录下的文件”:如果执行环境是沙箱,模型只能删除临时目录下的文件;如果没有沙箱,模型可能直接操作宿主机文件。
所以我的建议是:不管用什么框架,都不要让工具执行逻辑直接落在宿主机进程里;凡是可能产生副作用的操作,统一路由到沙箱执行器。框架负责聪明地决策,沙箱负责安全地执行,二者各司其职。
6. 迁移之后的一些个人体会
这套架构跑了几轮迭代之后,我最大的感受是:Agent 开发的复杂度并不在于“让模型做对事”,而在于“让模型在不该犯错的时候不至于造成不可逆的影响”。沙箱解决不了模型选错工具的问题,但它能确保模型选错工具之后,错误被限制在一个可以随时丢掉的临时环境里。
具体到执行环境的选择,如果你目前还只是本地试验或者单用户小规模试用,用容器隔离也许够了;但只要你开始接外部数据、跑第三方代码、或者要面对陌生访问者,我建议尽早切换到任务级沙箱的方案。安全这件事,等出了问题再补,代价会比一开始就设计隔离高很多。
另外想提醒一点:DeepSeek Elastic Compute 负责推理、UCloud Agent Sandbox 负责执行,这套组合不是终点,而是给 Agent 上了第一道护栏。沙箱内的执行安全只是整个 Agent 安全链条的一环,输入侧的提示词注入防护、输出侧的敏感信息过滤、编排层的权限控制,每一层都不能缺。不要觉得有了沙箱就万事大吉,真正上线之前,最好自己写几个攻击性测试用例,比如故意让 Agent 读取一个带恶意指令的网页,或者尝试让它访问内网地址,看看沙箱能不能扛住。
最后再分享一个小技巧:从最简单的隔离策略开始,不要上来就把网络白名单开得很全。先把执行环境限定在“最窄可用”,跑通最小场景,然后逐步放开口子。每一步放行都记录在案,这样你的 Agent 执行环境会一直保持在一个可控、可解释、可回溯的状态。这对后续审计和合规也很有帮助。