1. 为什么 AI Agent 的 Sandbox 设计值得单独拿出来聊
做 AI Agent 的人迟早会撞上同一堵墙:模型能规划、能调工具、能写代码,但你不敢让它真的在你机器上跑。原因很朴素——Agent 一旦拿到执行权限,它就是一个拥有你部分身份的可编程实体。它可能删错文件、把密钥写进日志、访问不该访问的接口、把临时数据永久留在磁盘上,甚至在多轮任务里把上下文越滚越大,最后把整个运行环境拖垮。
Sandbox(沙箱)就是解决这个问题的核心基础设施。它不是简单的“关进容器里”,而是一整套围绕隔离、权限、生命周期、持久化的运行时设计。你要决定它是 Hosted(托管)还是 Self-hosted(自托管),要决定文件系统怎么挂、Secret 怎么注入、网络怎么放行、状态怎么保存。这四个维度里任何一个设计错了,轻则 Agent 行为不稳定,重则直接变成安全事故。
这篇内容适合三类人看:正在从零搭 Agent 运行时的后端工程师、准备把 Agent 从 Demo 推向生产的团队负责人、以及想搞清楚“托管方案和自建方案到底差在哪”的技术决策者。我会把 Hosted 与 Self-hosted 的取舍逻辑、文件与 Secret 的隔离机制、网络出口的管控策略、持久化的分层设计全部拆开讲,并且给出可以直接抄的参数和配置思路。全文基于常见的工程实践补充细节,不涉及任何特定厂商的私有实现。
先说一个我踩过的坑:早期我图省事,直接让 Agent 在宿主机的一个临时目录里跑,用subprocess调命令。结果一次任务里 Agent 生成了一个递归删除的脚本,虽然最后被超时中断了,但那个目录里我放的一份测试数据全没了。从那以后我就明白,Sandbox 的第一原则不是“能跑”,而是“跑坏了也不影响别人”。
2. Hosted 与 Self-hosted 的取舍:先想清楚你要买的是什么
2.1 两种模式的本质差异
Hosted Sandbox 指的是你把 Agent 的执行环境交给第三方平台托管,你只负责提交任务和拿结果。Self-hosted 则是你自己在云主机、K8s 集群或者本地机器上把整套运行时搭起来。很多人一上来就问“哪个更好”,这个问题本身是错的,正确的问法是“我愿意用多少控制权换多少运维成本”。
Hosted 的核心价值在于把隔离和弹性这两件脏活外包出去。你不需要关心容器运行时怎么配、cgroup 怎么限制、网络策略怎么写,平台会给你一个 API,你传代码进去,它给你结果。代价是你对环境的控制粒度变粗,冷启动时间、文件系统行为、网络出口策略都由平台定义,你只能在其允许的范围内调整。
Self-hosted 的核心价值在于完全的可控性和数据主权。你的代码、你的 Secret、你的中间产物全部留在自己的网络边界内,网络策略可以精确到某个域名某个端口,持久化可以自己选对象存储还是块存储。代价是你得自己处理多租户隔离、资源配额、镜像预热、故障恢复这一整套东西。
我一般用下面这张表来帮团队做初筛:
| 维度 | Hosted | Self-hosted |
|---|---|---|
| 隔离实现 | 平台负责,通常基于 microVM 或强隔离容器 | 自己选,容器 / microVM / 进程级隔离 |
| 冷启动 | 通常 100ms~2s,取决于平台预热策略 | 可优化到 50ms 以内(预热池),也可慢到 10s+ |
| 网络控制 | 平台提供白名单/黑名单,粒度有限 | 可精确到域名、端口、协议 |
| Secret 管理 | 平台注入,需信任平台 | 自己接 KMS/Vault,链路完全自控 |
| 持久化 | 平台提供临时盘,长期存储需外接 | 完全自主,可挂任意存储 |
| 运维成本 | 低 | 高,需要专人维护 |
| 数据合规 | 依赖平台 | 完全自主 |
| 适合场景 | 快速验证、中小规模、无强合规要求 | 大规模、强合规、深度定制 |
2.2 选型时最容易忽略的三个成本
第一个是冷启动成本。Hosted 方案看起来省事,但如果你的 Agent 任务是高频短任务,每次都要等环境起来,累积的延迟会非常可观。Self-hosted 可以用预热池把冷启动压到几十毫秒,但预热池本身要占资源,空闲时也在烧钱。这个账要按你的任务 QPS 和单任务时长来算。
第二个是调试成本。Hosted 环境出问题时,你拿不到底层日志,只能看平台给你的那点输出。Self-hosted 虽然麻烦,但dmesg、strace、容器事件全都能看。Agent 这种不确定性极高的负载,可观测性往往比性能更重要。
第三个是迁移成本。一旦你的业务逻辑深度绑定了某个 Hosted 平台的 API 语义,想迁到 Self-hosted 就要重写一大层适配代码。我的建议是,哪怕先用 Hosted,也要在业务层和 Sandbox 层之间留一个抽象接口,把“提交任务、取文件、注入 Secret、配置网络”这几件事定义成自己的协议,后面换实现时只改适配层。
2.3 一个务实的混合策略
实际项目里我很少见到纯 Hosted 或纯 Self-hosted。更常见的做法是:开发阶段用 Hosted 快速迭代,生产阶段核心链路 Self-hosted,非核心的批处理任务继续用 Hosted。这样既保证了核心数据不出边界,又不用为所有边缘场景都维护一套重资产。
具体落地时,我会把 Sandbox 抽象成一个接口,类似这样:
class SandboxProvider: def create(self, spec: SandboxSpec) -> SandboxHandle: ... def exec(self, handle, cmd: str, timeout: int) -> ExecResult: ... def put_file(self, handle, path: str, data: bytes) -> None: ... def get_file(self, handle, path: str) -> bytes: ... def destroy(self, handle) -> None: ...Hosted 和 Self-hosted 各实现一份,业务代码只依赖这个接口。这个抽象层看起来多写了几百行,但它救过我至少两次——一次是平台限流导致必须临时切自建,一次是合规要求突然收紧必须把数据拉回内网。
3. 文件系统设计:临时盘、挂载点与生命周期
3.1 为什么不能直接用宿主机的文件系统
Agent 对文件系统的操作是不可预测的。它可能写一个 10GB 的日志把磁盘撑爆,可能创建几万个 inode 把目录搞崩,也可能在/tmp里留下敏感中间产物。所以 Sandbox 的文件系统设计要满足三个要求:隔离、限额、可回收。
隔离意味着 Agent 看到的根目录和宿主机完全无关。常见做法是给每个 Sandbox 实例挂一个独立的 overlay 文件系统,底层是只读的基础镜像,上层是可写的临时层。Agent 的所有写入都落在临时层,销毁实例时直接丢弃,宿主机毫发无损。
限额意味着你要给这个临时层设一个上限,比如 2GB。超过就报错,而不是让它无限增长。这个限制在容器里可以通过--storage-opt size=2G或者文件系统 quota 实现,在 microVM 里则直接限制虚拟磁盘大小。
可回收意味着实例销毁后,临时层必须被彻底清理。我见过因为清理逻辑有 bug,临时层堆积了几百 GB 的情况。所以销毁流程里一定要有校验步骤,确认磁盘空间真的释放了。
3.2 挂载点的分层设计
一个设计良好的 Sandbox 文件系统通常分三层:
- 只读基础层:包含运行时、常用工具、语言环境。这一层在所有实例间共享,用镜像的方式分发,保证一致性。
- 可写临时层:Agent 的工作目录,所有读写都在这。生命周期和实例绑定,实例销毁即回收。
- 持久化挂载层:需要跨实例保留的数据,比如用户上传的输入文件、任务产出的结果。这一层通过挂载外部存储实现,生命周期独立于实例。
这三层的挂载顺序和权限要仔细设计。基础层必须只读,否则一个实例的修改会污染其他实例。临时层要给足权限但设限额。持久化层要按最小权限原则挂载,Agent 只应该看到它这次任务需要的那部分数据,而不是整个存储桶。
3.3 文件传输的两种模式
Agent 和外部交换文件有两种模式:推模式和拉模式。
推模式是外部把文件写进 Sandbox,Agent 直接读本地路径。适合输入文件不大、任务启动前就确定的场景。实现上通常是通过一个 sidecar 或者初始化脚本,在实例启动后把文件put进去。
拉模式是 Agent 主动从对象存储或 API 拉取文件。适合输入文件很大、或者需要按需加载的场景。这种模式下 Sandbox 需要网络出口权限,要配合网络策略一起设计。
我个人的经验是,输入文件用推模式,输出文件用拉模式。输入推进去可以避免 Agent 启动时还要等网络,输出让 Agent 主动上传可以配合校验逻辑,确保结果完整落盘后再销毁实例。如果输出也靠外部拉,实例销毁时机就很难把握。
注意:文件路径一定要做规范化处理。Agent 生成的路径可能包含
..、软链接、绝对路径跳转,如果不做校验,可能写到挂载点之外。我一般会在 Sandbox 内部再套一层路径白名单,只允许在指定工作目录下操作。
4. Secret 管理:注入、隔离与防泄漏
4.1 Secret 为什么是 Sandbox 里最危险的部分
Agent 要调外部 API,就需要 API Key;要访问数据库,就需要连接串。这些 Secret 一旦进入 Sandbox,就面临三个泄漏风险:被 Agent 写进日志、被 Agent 写进产出文件、被同宿主机的其他实例侧信道读取。
第一个风险最普遍。Agent 在调试时经常会把环境变量打印出来,或者把请求头完整记录到日志。如果日志会被上传或展示,Secret 就泄漏了。第二个风险是 Agent 把 Secret 写进了它生成的文件里,比如配置文件、脚本。第三个风险在多租户场景下最致命,如果隔离不彻底,一个实例可能读到另一个实例的内存或文件。
4.2 三种注入方式的对比
| 注入方式 | 实现 | 优点 | 风险 |
|---|---|---|---|
| 环境变量 | 启动时注入 env | 简单,Agent 直接读 | 容易被打印、被子进程继承 |
| 文件挂载 | 挂载 Secret 文件 | 不占环境变量空间 | 文件可能被读取、复制 |
| 运行时 API | Agent 通过本地代理换取 | Secret 不落地 | 实现复杂,需代理常驻 |
环境变量方式最省事,但风险也最高。子进程会继承环境变量,Agent 调用的任何工具都可能把它带出去。文件挂载稍好,但 Agent 有读权限就能复制。运行时 API 方式最安全,Secret 存在 Sandbox 外的代理里,Agent 拿到的只是一个短期令牌,但实现成本高,需要维护一个常驻的本地代理。
我的建议是按 Secret 的敏感级别分层。低敏感度的(比如公开 API 的 key)可以用环境变量,中敏感度的用文件挂载加权限控制,高敏感度的(比如数据库写权限、支付接口)必须走运行时 API,且令牌要有短过期时间和使用范围限制。
4.3 防泄漏的实操要点
第一,Secret 永远不要进日志。在 Sandbox 里可以挂一个日志过滤器,把已知的 Secret 模式(比如特定前缀的字符串)替换成占位符。这个过滤器要放在日志离开 Sandbox 之前的最后一环。
第二,Secret 要有生命周期。任务开始时注入,任务结束立即失效。不要用长期有效的凭证。如果平台支持,用短期令牌换取机制,令牌有效期设成任务预期时长的 1.5 倍。
第三,Secret 的访问要审计。每次 Agent 读取 Secret,都应该有一条审计记录,包含实例 ID、时间、读取的 Secret 标识。这样出问题时能快速定位是哪个任务泄漏的。
第四,销毁时要清理内存和磁盘。环境变量在进程退出后通常会被系统回收,但文件挂载的 Secret 如果落在临时层,销毁实例时要确保临时层被彻底擦除,而不是简单删除文件(删除文件不等于数据不可恢复)。
提示:如果你的 Agent 需要调用多个外部服务,尽量给每个服务单独的 Secret,而不是一个万能 Key。这样即使某个 Secret 泄漏,影响范围也可控。
5. 网络设计:出口管控与依赖隔离
5.1 默认拒绝还是默认放行
网络策略的第一决策是默认姿态。默认拒绝意味着 Sandbox 初始没有任何网络访问,需要显式放行。默认放行则相反,初始可以访问任何地址,需要显式封禁。
从安全角度,默认拒绝是唯一正确的选择。Agent 的行为不可预测,默认放行等于给它开了一扇通往整个网络的门。但默认拒绝会带来配置成本,你需要为每个任务声明它需要访问哪些地址。
我的做法是维护一个域名白名单模板,按任务类型分类。比如“代码执行类”任务只需要访问包管理源,“数据查询类”任务需要访问特定 API 域名,“网页抓取类”任务需要访问目标站点。任务创建时选择模板,再按需微调。
5.2 出口流量的三种管控粒度
最粗的粒度是全放行或全封禁,适合对安全要求不高的内部工具。中等粒度是按域名/IP 段放行,通过 DNS 解析和防火墙规则实现。最细的粒度是按域名+端口+协议放行,甚至可以做 TLS 层面的 SNI 检查。
实现上,容器环境常用的是在 Sandbox 的 network namespace 里配 iptables 规则,或者用一个出口代理(egress proxy)做流量转发和审计。出口代理的好处是所有流量都经过它,可以做统一的日志、限流、内容检查。坏处是它成了单点,需要高可用设计。
5.3 内网访问的特殊处理
Agent 经常需要访问内网服务,比如公司的数据库、内部 API。这时候网络策略要特别小心,因为内网通常没有强认证,一旦 Sandbox 能访问内网,就等于绕过了边界防护。
我的原则是内网访问必须经过显式的服务网关,而不是让 Sandbox 直接连内网地址。网关负责认证、鉴权、限流、审计,Sandbox 只拿到网关的地址和短期凭证。这样即使 Sandbox 被攻破,攻击者也只能通过网关访问有限的服务,而不是整个内网。
另外,DNS 解析也要管控。Agent 可能通过 DNS 查询泄漏信息,或者被诱导解析到恶意地址。可以在 Sandbox 里配一个受控的 DNS 解析器,只解析白名单内的域名,其他一律返回失败。
5.4 网络与超时的配合
网络策略要和超时机制配合。一个任务如果卡在网络请求上,不能无限等待。我一般会设三层超时:单次请求超时(比如 10s)、任务总超时(比如 300s)、Sandbox 实例最大存活时间(比如 600s)。任何一层触发,都要能干净地中断任务并回收资源。
这里有个细节:中断网络请求时,要确保连接被正确关闭,而不是留下半开的 socket。否则大量半开连接会耗尽文件描述符。用支持 context cancellation 的 HTTP 客户端能省很多事。
6. 持久化设计:什么该留,什么该丢
6.1 持久化的三个层次
Sandbox 的持久化需求可以分成三层:任务内持久化、跨任务持久化、跨会话持久化。
任务内持久化指同一个任务执行过程中,多个步骤之间需要共享状态。比如 Agent 先下载文件,再处理文件,再上传结果。这层持久化用 Sandbox 的临时层就够了,实例不销毁数据就在。
跨任务持久化指不同任务之间需要共享数据。比如用户上传的原始文件,多个任务都要读。这层需要外部存储,比如对象存储或共享文件系统。关键是设计好命名空间和权限,避免任务之间互相干扰。
跨会话持久化指 Agent 需要记住历史交互,比如对话上下文、用户偏好。这层通常放在 Agent 框架的 memory 模块里,而不是 Sandbox 里。Sandbox 只负责执行,不负责记忆。
6.2 临时数据的清理策略
Sandbox 销毁时,临时层的数据应该全部丢弃。但有些数据需要保留一段时间用于调试,比如失败任务的现场。这时候可以做一个快照机制:任务失败时,把临时层打包上传到调试存储,保留 7 天,之后自动清理。
这个机制很有用,但要注意两点。一是快照里可能包含 Secret,上传前要过滤。二是快照会占存储空间,要有配额和清理策略,否则调试存储会无限增长。
6.3 持久化存储的选型
| 存储类型 | 延迟 | 适用场景 | 注意事项 |
|---|---|---|---|
| 本地 SSD | 极低 | 任务内临时数据 | 实例销毁即丢 |
| 网络块存储 | 低 | 需要跨实例共享的中间数据 | 挂载有开销,注意并发写 |
| 对象存储 | 中高 | 输入输出文件、快照 | 最终一致,注意读写时序 |
| 数据库 | 低 | 结构化状态、元数据 | 连接池要控制,避免打爆 |
选型的关键是匹配访问模式。频繁随机读写用块存储,大文件顺序读写用对象存储,结构化查询用数据库。不要用一种存储硬扛所有场景,那样迟早会遇到性能瓶颈。
6.4 持久化与幂等性
Agent 任务经常需要重试。重试时如果持久化状态没处理好,可能产生重复数据或状态不一致。所以持久化设计要配合幂等性:每个任务有一个唯一 ID,写入持久化存储时用这个 ID 做去重,重试时先检查是否已经写入过。
我一般会在持久化层加一个任务状态表,记录任务 ID、状态、产出文件路径、时间戳。任务开始时写入“进行中”,成功时更新为“完成”,失败时更新为“失败”。重试时先查这个表,如果已经完成就直接返回结果,避免重复执行。
7. 常见问题与排查技巧实录
7.1 高频问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 任务启动慢 | 镜像拉取慢、冷启动 | 检查镜像大小、预热池命中率 |
| 任务莫名失败 | 资源限额触发 | 查 cgroup 事件、OOM 记录 |
| 文件写入失败 | 临时层满了 | 查磁盘使用、quota 配置 |
| 网络请求超时 | 出口策略拦截 | 查 iptables 日志、代理日志 |
| Secret 读取失败 | 注入时机不对 | 查注入顺序、权限配置 |
| 实例销毁后磁盘没释放 | 清理逻辑 bug | 查挂载点、overlay 层残留 |
| 多任务互相干扰 | 隔离不彻底 | 查命名空间、共享挂载 |
7.2 几个我踩过的坑
坑一:临时层没设限额,Agent 写爆磁盘。一次任务里 Agent 生成了一个死循环写日志的脚本,几分钟就把 50GB 的盘写满了,导致宿主机上其他实例全部受影响。后来我给每个实例的临时层设了硬限额,超了直接 kill。
坑二:Secret 通过环境变量注入,被 Agent 打印到日志。Agent 在调试时执行了env命令,把所有环境变量打进了日志,日志又被上传到了公开的调试面板。虽然发现得早,但教训很深。现在我的日志过滤器会把所有已知 Secret 模式替换掉。
坑三:网络默认放行,Agent 访问了不该访问的地址。早期为了省事,Sandbox 网络默认放行。结果一次任务里 Agent 被提示词诱导,访问了一个外部地址并上传了数据。虽然数据不敏感,但暴露了策略缺陷。现在默认拒绝,白名单按任务类型配置。
坑四:持久化写入没有幂等,重试产生重复文件。任务超时后自动重试,但第一次执行其实已经写入了产出文件,重试又写了一遍,导致下游拿到两份数据。后来加了任务状态表和去重逻辑才解决。
7.3 排查工具与方法
排查 Sandbox 问题,我常用的工具组合是:容器事件(docker events或 K8s events)看生命周期,cgroup 统计看资源使用,网络抓包(tcpdump)看流量走向,审计日志看 Secret 访问和文件操作。
方法上,我习惯先复现再定位。Sandbox 问题往往和具体任务相关,脱离任务很难复现。所以我会把失败任务的输入、配置、环境完整记录下来,在隔离环境里重放,逐步缩小范围。
还有一个技巧是加一个健康检查端点。Sandbox 实例启动后,先跑一个自检脚本,检查文件系统可写、网络可达、Secret 可读。自检不过就直接标记实例不可用,避免把问题带到任务执行阶段。
8. 一套可落地的 Sandbox 配置参考
8.1 资源限额参数
以下是我在中等规模场景下常用的一组参数,供参考:
sandbox: cpu_limit: "2" # 2 核,按任务类型调整 memory_limit: "2Gi" # 2GB,代码执行类可降到 512Mi disk_limit: "2Gi" # 临时层上限 pids_limit: 256 # 防止 fork 炸弹 timeout: 300 # 任务总超时,秒 max_lifetime: 600 # 实例最大存活,秒 network: default: deny allow_domains: - "pypi.org" - "registry.npmjs.org" dns_servers: - "10.0.0.53" # 受控 DNSCPU 和内存的配比要看任务类型。纯 IO 密集型任务 CPU 可以低一些,计算密集型要高。磁盘限额要留出余量,因为 Agent 可能生成比预期大的中间文件。pids_limit 是防 fork 炸弹的关键,很多容器运行时默认不限制,一定要显式设。
8.2 生命周期管理
一个完整的 Sandbox 生命周期应该包含这些阶段:创建、初始化、执行、产出收集、销毁、清理。每个阶段都要有超时和失败处理。
创建阶段要检查资源配额,避免超卖。初始化阶段要注入 Secret、挂载文件、配置网络。执行阶段要监控资源使用,超限就中断。产出收集阶段要校验文件完整性,确保上传成功。销毁阶段要确保所有资源释放。清理阶段要确认磁盘、网络、进程全部回收。
我一般会用一个状态机来管理这个流程,每个状态转换都有日志和指标。这样出问题时能快速定位卡在哪个阶段。
8.3 监控指标
必须监控的指标包括:实例创建成功率、平均冷启动时间、任务成功率、平均任务时长、资源超限次数、网络拦截次数、Secret 读取次数、磁盘残留量。
这些指标能帮你发现很多问题。比如冷启动时间突然变长,可能是镜像仓库慢了。网络拦截次数突增,可能是 Agent 行为异常或者策略配错了。磁盘残留量持续增长,说明清理逻辑有 bug。
9. 我个人在实际操作中的几点体会
做 Agent Sandbox 这几年,最大的体会是不要追求一步到位。一开始就想设计一个完美的隔离方案,往往会陷入过度工程,最后连 Demo 都跑不起来。更务实的路径是:先用最简单的隔离(比如容器加基本限额)把流程跑通,然后根据实际遇到的问题逐步加固。
第二个体会是可观测性比隔离强度更重要。再强的隔离也可能有漏洞,但如果你能快速发现异常行为,就能及时止损。所以我在每个 Sandbox 里都会加详细的审计日志,记录每一次文件操作、网络请求、Secret 读取。这些日志平时看着冗余,出问题时就是救命稻草。
第三个体会是Secret 管理要当成一等公民。很多团队把 Secret 当成配置的一部分,随手写在环境变量里。等到出事才发现,Secret 已经散落在日志、快照、产出文件里,根本收不回来。正确的做法是从第一天就设计好 Secret 的注入、使用、轮换、审计全流程。
最后分享一个小技巧:给 Sandbox 加一个“干跑”模式。在这个模式下,所有写操作和网络请求都被拦截并记录,但不真正执行。这个模式用来验证 Agent 的行为边界特别有用,可以在不冒任何风险的情况下,看清楚 Agent 到底想干什么。我一般会在新任务类型上线前,先用干跑模式跑一批样本,确认行为符合预期后再放开真实执行。