AI Agent 从 Demo 走向生产环境,最先卡住大多数团队的不是模型能力,而是"它到底在哪跑、能碰什么、跑完留下什么"。我见过太多项目在本地跑得风生水起,一上生产就出问题:Agent 把宿主机上的配置文件读了个遍、两个用户的会话互相串数据、任务跑到一半容器被回收导致状态全丢。这些问题的根子都在 Sandbox 设计上。这篇内容围绕 AI Agent Sandbox 的完整设计展开,重点拆解 Hosted 与 Self-hosted 两条路线的取舍逻辑、文件系统的隔离与挂载策略、Secret 的注入与防泄漏、网络出口的管控,以及持久化该怎么做。适合正在把 Agent 往生产推的工程师、平台架构师,以及需要给团队定技术方案的技术负责人参考。不管你是用 Python 还是 Rust 写 Agent,是自建还是托管,这里面的设计思路都能直接对照落地。
1. 先想清楚 Sandbox 到底在防什么
很多人一上来就讨论用哪个容器运行时、要不要上 gVisor,但没想明白威胁模型。Sandbox 的设计起点不是技术选型,而是"你要防谁、防什么"。这个前提不搞清楚,后面所有决策都是拍脑袋。
1.1 Agent 和传统代码执行的根本差异
传统后端服务执行的是你自己写的、经过 Code Review 的代码,输入输出边界清晰。Agent 完全不一样:它执行的是模型在运行时生成的代码或工具调用,这些内容你事先看不到、无法逐行审计,而且每次都不一样。这意味着 Sandbox 面对的是一个"半可信"的执行体——它可能因为模型幻觉去删文件、可能因为 Prompt 注入去读环境变量、可能因为工具描述不清去调用不该调的接口。
我通常把 Agent 的风险归成四类,设计时逐条对照:
- 越权访问:Agent 读到了它不该读的文件、环境变量、其他用户的会话数据。
- 资源滥用:死循环、无限递归、疯狂创建子进程,把宿主机 CPU 和内存吃干。
- 网络外联:Agent 主动往外发数据,或者被诱导去访问内网服务。
- 状态污染:上一个任务的残留文件、缓存、进程影响了下一个任务。
这四类风险决定了 Sandbox 必须同时具备隔离、限额、网络管控、生命周期管理四种能力,缺一不可。只做容器隔离不做网络管控,等于门锁了但窗户开着。
1.2 威胁模型决定了隔离强度
隔离强度不是越高越好,越高越贵、越慢。我的经验是按 Agent 的可信程度分档:
| Agent 类型 | 典型场景 | 建议隔离级别 | 理由 |
|---|---|---|---|
| 内部可信 Agent | 团队内部数据处理 | 进程级 + 命名空间 | 成本低,够用 |
| 半可信 Agent | 用户自定义 Prompt | 容器级(Docker/containerd) | 隔离与成本平衡 |
| 不可信 Agent | 开放平台、多租户 | 微虚拟机(Firecracker/Kata) | 强隔离,防逃逸 |
| 高危操作 Agent | 执行任意代码 | 微虚拟机 + 独立网络命名空间 | 最严,接受性能损耗 |
注意:如果你的 Agent 会执行用户提交的任意代码,容器级隔离是不够的。容器共享内核,内核漏洞一旦被利用就是逃逸。这种情况必须上微虚拟机。
我踩过的一个坑:早期用 Docker 跑多租户 Agent,觉得--read-only加--cap-drop=ALL就安全了。结果有个 Agent 通过/proc读到了宿主机的部分信息。后来才补上--pid=host的反向配置和 seccomp 白名单。隔离是层层叠加的,没有单一开关能一劳永逸。
2. Hosted 与 Self-hosted 的取舍不是非黑即白
这是被问得最多的问题,但答案从来不是"哪个更好",而是"你的约束条件是什么"。我把它拆成几个维度来对比,你会发现很多团队其实适合混合方案。
2.1 Hosted 方案省了什么、又让渡了什么
Hosted Sandbox(比如各类云端代码执行服务)最大的价值是把运维复杂度外包出去。你不需要管容器编排、不需要管宿主机补丁、不需要管资源超卖。对于小团队或者想快速验证产品的场景,这是最优解。
但它让渡的东西也很明确:
- 数据主权:Agent 处理的文件、Secret 要传到第三方环境,合规敏感的行业直接出局。
- 网络策略:出口规则由平台定,你想让 Agent 访问某个内网服务基本没戏。
- 运行时定制:想装个特殊系统依赖、想用特定内核参数,平台不支持就是不支持。
- 成本模型:按调用次数或时长计费,高频短任务场景下成本可能远超自建。
我见过一个团队用 Hosted 方案做数据分析 Agent,前期很爽,后来发现每次任务都要把几百 MB 的数据集上传上去,光传输时间和流量成本就受不了,最后还是转自建。
2.2 Self-hosted 的真实成本在哪
Self-hosted 不是"省了钱",而是"把钱换成了人力"。真实成本分布大概是这样的:
- 初期搭建:容器运行时选型、编排、镜像管理,2-4 周。
- 安全加固:seccomp、AppArmor、网络策略、Secret 管理,持续投入。
- 日常运维:宿主机补丁、容量规划、故障处理,长期占用人力。
- 弹性伸缩:冷启动优化、预热池、调度策略,这是最容易被低估的部分。
如果你的团队没有专职的平台工程师,Self-hosted 很容易变成"能跑但不敢上量"的状态。我的建议是:先用 Hosted 验证产品逻辑,等调用量和合规要求到了临界点再自建,而不是一开始就追求全自建。
2.3 混合架构:把两类任务分开跑
实际生产里我推荐一种混合模式:低敏感、突发性的任务走 Hosted,高敏感、稳定量的任务走 Self-hosted。比如代码解释、格式转换这类不碰敏感数据的走托管;涉及内部数据、需要访问内网服务的走自建。
这种架构的关键是统一抽象层。你需要定义一个 Sandbox 接口,把 Hosted 和 Self-hosted 都实现成它的后端,上层 Agent 代码不感知底层差异:
class SandboxBackend: def create(self, spec): ... def exec(self, sandbox_id, cmd): ... def upload(self, sandbox_id, path, data): ... def destroy(self, sandbox_id): ... class HostedBackend(SandboxBackend): ... class SelfHostedBackend(SandboxBackend): ...这样切换后端只改配置,不改业务代码。我吃过没有这层抽象的亏——早期业务代码直接调了托管平台的 SDK,后来要迁自建,改了几十个文件。
3. 文件系统:隔离、挂载与临时目录的边界
文件系统是 Agent Sandbox 里最容易出问题的地方,因为它同时涉及隔离、性能和数据传递三个诉求,而这三者经常互相冲突。
3.1 根文件系统必须只读
这是铁律。Agent 的根文件系统(/)应该挂载为只读,所有需要写入的地方单独挂可写卷。原因很简单:Agent 生成的代码可能往任意路径写文件,如果根可写,它可能覆盖系统库、篡改工具二进制,甚至写入持久化后门。
具体做法:
# 容器启动时 --read-only --tmpfs /tmp:rw,noexec,nosuid,size=512m --tmpfs /run:rw,noexec,nosuid,size=64m -v /workspace:/workspace:rw/tmp用 tmpfs 挂载,noexec防止 Agent 往/tmp写可执行文件再运行。/workspace是唯一的工作目录,Agent 的所有产物都落在这里。
提示:
noexec不是万能的,Agent 可以用解释器执行脚本(python /tmp/x.py)。所以/tmp还要配合大小限制,防止被塞满。
3.2 工作目录的挂载策略
工作目录怎么挂,取决于任务模型。我总结了几种常见模式:
- 一次性任务:每次任务创建独立目录,任务结束即销毁。适合无状态 Agent。
- 会话级目录:同一会话的多次任务共享目录,会话结束销毁。适合多轮对话 Agent。
- 持久卷:跨会话保留,需要显式清理。适合需要长期积累数据的 Agent。
关键点是目录归属要能追溯到会话和用户。我见过因为目录命名用随机 UUID 但没记录映射关系,导致清理时不知道该删哪个,最后磁盘爆掉的事故。建议目录结构直接体现归属:
/workspace/{tenant_id}/{session_id}/{task_id}/这样清理逻辑简单,审计也方便。
3.3 文件上传下载的坑
Agent 和外部交换文件时,有几个细节容易翻车:
- 路径穿越:用户上传的文件名带
../,如果不做校验,可能写到工作目录外。必须对文件名做规范化,拒绝包含..和绝对路径的输入。 - 大文件:Agent 处理大文件时可能 OOM,上传接口要有大小限制和流式处理。
- 符号链接:Agent 可能创建指向工作目录外的软链,读取时要做 realpath 校验。
- 权限位:上传的文件默认权限要收紧,避免 Agent 意外执行。
import os def safe_join(base, user_path): base = os.path.realpath(base) target = os.path.realpath(os.path.join(base, user_path)) if not target.startswith(base + os.sep): raise ValueError("path traversal detected") return target这段代码看着简单,但能挡掉绝大多数路径穿越问题。我建议把它做成所有文件操作的统一入口,而不是散落在各处。
4. Secret 管理:注入、隔离与防泄漏
Secret 是 Agent Sandbox 里最敏感的部分。Agent 需要 Secret 去调用外部 API、访问数据库,但 Agent 又是"半可信"的,一旦 Secret 泄漏,影响面很大。
4.1 Secret 绝不能进环境变量
很多人图省事,把 API Key 通过环境变量注入容器。这在 Agent 场景下是危险的,因为:
- Agent 可以执行
env命令,直接打印所有环境变量。 - 子进程会继承环境变量,Agent 起的任何进程都能读到。
- 环境变量容易被打进日志、错误堆栈。
正确做法是用文件挂载 + 按需读取,或者更彻底的Secret 代理。文件挂载的话,Secret 放在 tmpfs 上,权限设为0400,只有特定用户能读:
--mount type=tmpfs,destination=/secrets,tmpfs-mode=0700 # Secret 文件权限 0400,属主为运行 Agent 的用户4.2 Secret 代理模式
更安全的做法是 Agent 根本不接触 Secret 明文。你跑一个本地代理,Agent 请求代理,代理去注入真实凭证:
Agent -> localhost:8080/proxy/api.openai.com -> 代理注入 Authorization -> 真实 API这样 Agent 的代码里只有代理地址,没有真实 Key。即使 Agent 被 Prompt 注入诱导去打印凭证,也拿不到。代价是代理本身要维护,且要处理各种认证方式(Bearer、HMAC 签名等)。
我实测下来,代理模式对 HTTP API 类调用很友好,但对需要复杂签名的场景(比如某些云服务的 v4 签名)实现成本较高,需要权衡。
4.3 Secret 的生命周期
Secret 不是注入完就完事了,还要管生命周期:
- 按任务注入:每个任务用独立的短期凭证,任务结束即失效。这需要你的凭证系统支持动态签发。
- 定期轮换:长期凭证要定期换,换的时候不能中断运行中的任务。
- 审计:记录哪个任务用了哪个 Secret,出问题时能追溯。
注意:短期凭证是防泄漏的最有效手段。即使 Agent 把凭证写进了日志,凭证几分钟后就失效了,损失可控。
5. 网络出口:默认拒绝,按需放行
网络管控是 Sandbox 设计里最容易被忽视、但出事最严重的一环。Agent 一旦能自由外联,数据外泄、内网探测、被当作跳板都可能发生。
5.1 默认拒绝是唯一正确的起点
Sandbox 的网络策略必须是默认拒绝所有出口,白名单放行。不要用"默认允许,黑名单封禁"的思路,因为黑名单永远列不全。
实现上,容器网络用独立的 network namespace,配合 iptables 或 eBPF 做出口过滤:
# 默认 DROP iptables -P OUTPUT DROP # 放行 DNS iptables -A OUTPUT -p udp --dport 53 -j ACCEPT # 放行特定 API iptables -A OUTPUT -d api.example.com -p tcp --dport 443 -j ACCEPT注意 DNS 也要管控,否则 Agent 可以通过 DNS 隧道外泄数据。生产环境建议用内部 DNS 解析器,只解析白名单域名。
5.2 域名白名单 vs IP 白名单
域名白名单更灵活,但有个坑:域名解析出的 IP 可能变化,而且存在 DNS 重绑定攻击(第一次解析到合法 IP,第二次解析到内网 IP)。稳妥做法是解析后校验 IP,拒绝内网地址段:
import ipaddress BLOCKED = [ipaddress.ip_network("10.0.0.0/8"), ipaddress.ip_network("172.16.0.0/12"), ipaddress.ip_network("192.168.0.0/16"), ipaddress.ip_network("169.254.0.0/16")] def is_blocked(ip): addr = ipaddress.ip_address(ip) return any(addr in net for net in BLOCKED)这样即使 DNS 被操纵,也连不到内网。
5.3 出站流量的可观测性
光管控不够,还要能看见。我建议记录所有出站连接的目标、时间、任务 ID,出问题时能快速定位是哪个 Agent 在往外发数据。日志量大的话可以只记录被拒绝的连接和异常模式(比如短时间内大量不同目标)。
6. 持久化:什么该留、什么该扔
持久化设计不好,要么状态丢失导致任务失败,要么数据堆积导致成本失控。核心原则是区分"任务状态"和"任务产物",分别处理。
6.1 任务状态 vs 任务产物
- 任务状态:Agent 执行过程中的中间状态,比如对话历史、工具调用记录、临时变量。这类数据生命周期短,任务结束就可以清理,但任务执行期间必须可靠。
- 任务产物:Agent 最终输出的文件、报告、代码。这类数据需要保留,供用户下载或后续任务使用。
两者的存储策略完全不同。状态可以用 Redis 这类内存存储,快但可能丢;产物必须用对象存储或持久卷,慢但可靠。
我见过把两者混在一起存的,结果要么状态丢了任务重跑,要么产物被当临时数据清了,用户投诉。
6.2 持久卷的挂载与回收
如果 Agent 需要跨任务保留数据(比如长期运行的研究 Agent),就要用持久卷。关键是回收策略要明确:
| 数据类型 | 保留策略 | 存储介质 |
|---|---|---|
| 会话状态 | 会话结束后 24h | Redis |
| 任务产物 | 用户显式删除或 30 天 | 对象存储 |
| 长期知识库 | 永久,定期归档 | 持久卷 + 冷备 |
| 临时缓存 | 任务结束即删 | tmpfs |
回收策略要写成代码,不能靠人工。我建议给每个持久卷打上 TTL 标签,由后台任务定期扫描清理。
6.3 快照与恢复
对于长任务,Sandbox 可能因为宿主机故障、扩容等原因被中断。这时候快照能力就很重要。做法是定期把工作目录和状态做快照,任务恢复时从最近的快照继续。
快照的粒度要权衡:太频繁影响性能,太稀疏丢失工作多。我的经验是按检查点做快照,Agent 每完成一个明确的步骤就触发一次,而不是按时间。
7. 几个容易翻车的实操细节
前面讲的是设计框架,这里补充几个我在实际项目里踩过的具体坑,都是文档里不会写的。
7.1 冷启动时间被严重低估
Self-hosted 方案里,容器冷启动动辄几秒到十几秒。如果 Agent 任务本身只跑几百毫秒,冷启动就是主要开销。解决办法是预热池:维护一批已启动的 Sandbox,任务来了直接分配,用完回收再预热。
预热池的难点是容量规划:池子太小,高峰期要等冷启动;池子太大,闲置资源浪费。我的做法是按历史 QPS 的 P95 来定池子大小,配合动态扩缩。
7.2 资源限额要设,但不能设死
CPU 和内存限额必须设,否则一个死循环 Agent 能拖垮整个宿主机。但设太死会导致正常任务被 OOM Kill。建议:
- 内存设软限制(触发告警)和硬限制(触发 Kill)两档。
- CPU 用 quota 而不是 limit,允许突发。
- 磁盘 IO 也要限,否则 Agent 疯狂写文件会拖慢同宿主机其他任务。
7.3 日志和产物要分开存
Agent 的 stdout/stderr 日志量可能非常大(尤其是调试模式),如果和产物存在一起,会污染产物目录,也会让存储成本失控。建议日志走独立的日志系统,产物走对象存储,两者用任务 ID 关联。
7.4 超时和取消要能真正生效
Agent 任务超时后,光标记状态没用,必须真正杀掉进程、释放资源。这里有个坑:Agent 可能起了子进程,杀父进程不会自动杀子进程。要用进程组或者 cgroup 来管理,确保整个进程树被清理。
# 用进程组启动,超时时杀整个组 setsid python agent.py & kill -TERM -$PGID7.5 多租户下的资源隔离
如果多个租户共享 Sandbox 集群,要防止一个租户的任务影响另一个。除了资源限额,还要注意:
- 网络命名空间隔离,防止租户间互相访问。
- 存储路径隔离,防止越权读写。
- 调度上做亲和性控制,避免同一租户的任务集中到同一宿主机(防止单点故障影响面过大)。
这些细节单看都不复杂,但组合起来就是生产可用和不可用的差距。我在实际项目里的体会是,Sandbox 的设计没有银弹,每个决策都是隔离强度、性能、成本、运维复杂度之间的权衡。先把威胁模型想清楚,再逐层叠加隔离能力,最后用可观测性兜底,这套思路比纠结具体用哪个技术更重要。