Python 沙箱和 Docker 沙箱,你选对了吗?
在做 RAG / Agent 项目时,"让大模型执行 Python 脚本"几乎是必经之路------技能(Skill)系统要跑数据分析脚本、知识库处理要跑转换脚本。随之而来的问题是:脚本在什么环境里执行? Python 沙箱还是 Docker 沙箱?很多人第一反应是"Docker 更安全,直接上 Docker",也有人觉得"就是个执行脚本,Python 沙箱够了"。这篇文章从一个真实项目(WeKnora 风格的知识库 Agent)的选型过程出发,把两者的本质讲清楚。
一、先看场景:为什么需要沙箱
大模型 Agent 的工具集里有一个很常见的能力:执行技能脚本。比如:
- 用户说"帮我统计知识库里的销售数据" → Agent 检索到数据 → 调用
analyze.py做统计 - 用户说"把这份 JSON 转成 CSV" → 调用
format_converter.py - 用户说"把检索结果生成一份课程需求" → 调用
rag-to-requirement.py
这些脚本是技能(SKILL.md)的一部分,由模型决定何时调用、传什么参数。代码不是我们预先审核后固定执行的,而是模型按需驱动的------这就有了安全隐患:脚本可能读到服务器上的敏感文件、可能外传数据、可能死循环吃光资源。
于是有了"沙箱":把脚本执行隔离在一个受限环境里。
二、两种沙箱的本质:同一执行体,不同厚度的壳
先说结论,避免误区:
Python 沙箱和 Docker 沙箱,执行方式完全一样------都是
python script.py+ 标准输入传参。区别只在于外面套的隔离壳有多厚。
纯 Python 沙箱(薄壳)
python
# 伪代码:进程级薄壳
result = subprocess.run(
["python", script_path],
input=data_json, # stdin 传参
capture_output=True,
timeout=60, # 超时限制
text=True,
)
if len(result.stdout) > MAX_OUTPUT: # 输出大小限制
raise OutputTooLarge()
隔离能力:
| 维度 | 能力 |
|---|---|
| 超时限制 | ✅ 可限制 |
| 输出/输入大小 | ✅ 可限制 |
| 文件系统 | ❌ 脚本可读宿主机文件 |
| 网络 | ❌ 脚本可联网 |
| 资源配额(CPU/内存) | ⚠️ 依赖系统级限制 |
一句话:只管"跑不死",不管"看不见"。
Docker 沙箱(厚壳)
bash
# 伪代码:容器级厚壳
docker run --rm \
--network none \ # 禁网
--memory 512m \ # 内存配额
--cpu-shares 256 \ # CPU 配额
--read-only \ # 只读文件系统
-i python:3.11-slim \
python script.py <<< "$data_json"
隔离能力:
| 维度 | 能力 |
|---|---|
| 超时限制 | ✅ 可限制 |
| 输出/输入大小 | ✅ 可限制 |
| 文件系统 | ✅ 容器内只读,宿主机文件不可见 |
| 网络 | ✅ 默认禁网(--network none) |
| 资源配额 | ✅ 内存/CPU/磁盘全可配 |
| 权限 | ✅ 容器内 root 也是受限的 |
一句话:既管"跑不死",也管"看不见、连不上、写不进"。
关系图
纯 Python 沙箱(薄壳) Docker 沙箱(厚壳)
┌─────────────────────┐ ┌─────────────────────┐
│ subprocess 进程 │ │ Docker 容器 │ ← 隔离壳
│ └ python script.py│ │ └ python script.py│ ← 执行体相同
│ 限制:超时/输出大小 │ │ 限制:超时/输出/文件/网络/资源 │
└─────────────────────┘ └─────────────────────┘
所以准确的说法是:不是"在 Python 沙箱外套一层 Docker",而是同一个执行方式,选择了不同强度的隔离壳。
三、Docker 沙箱到底防什么
很多人以为 Docker 沙箱是防"脚本崩溃"------其实超时、资源限制这两件事 Python 沙箱也能做。Docker 壳多防的是信息安全和越权:
- 文件系统隔离 :脚本只能看到容器内的文件,读不到宿主机的
config.yaml、.env、数据库凭证。恶意脚本想偷 token?门都没有。 - 网络隔离:默认无网络,脚本无法把数据外传到攻击者服务器,也无法扫描内网。
- 资源配额:内存/CPU/磁盘配额,脚本失控也不会拖垮整个服务。
- 权限收口:容器内即使拿到 root,权限也被限制在容器边界内。
一句话:Docker 壳防的是"不可信的代码",Python 壳防的是"失控的进程"。
四、选型:什么时候够用,什么时候必须上 Docker
判断标准一:代码可信度(最关键)
- 单租户 / 内部系统 :脚本输入是服务端可控的(比如 Agent 的检索结果),不是用户上传的任意代码 → Python 沙箱够用
- 多租户 / 开放技能市场 :任何人都能上传脚本给平台执行 → 必须 Docker(甚至要加更多加固:seccomp、无 root、镜像签名)
判断标准二:脚本的依赖与行为
- 脚本只用标准库(json / re / csv / sys...),纯数据处理 → Python 沙箱够
- 脚本需要第三方库(pandas、numpy...)→ Python 沙箱需要预装依赖;Docker 可以让每个技能自带 requirements,互不污染
- 脚本需要联网 → 需要明确的网络策略;更好的做法是脚本不联网,网络能力由 Agent 工具(如 MCP)提供,沙箱保持无网
判断标准三:部署复杂度
- 本地开发阶段装 Docker、拉镜像、容器编排,成本不小;Python 沙箱零依赖、即写即用
- 生产环境 Linux 服务器上,两者的能力与操作系统无关,选择只取决于前两个标准
决策表
| 场景 | 推荐 |
|---|---|
| 本地开发 / 单租户 / 标准库脚本 | ✅ Python 沙箱 |
| 生产部署 / 单租户 / 标准库脚本 | ✅ Python 沙箱 |
| 多租户 / 用户上传脚本 | ⚠️ Docker 沙箱 |
| 脚本需第三方库且互相冲突 | ⚠️ Docker 沙箱(或预装依赖的 Python 环境) |
| 需要网络隔离审计 | ⚠️ Docker 沙箱 |
五、工程实践:接口抽象,换壳不换芯
无论选哪种,上层接口都要抽象成同一个,这样未来升级隔离强度时一行业务代码都不用改:
go
// 沙箱接口:与实现无关
type Sandbox interface {
Execute(ctx context.Context, code string) (*SandboxResult, error)
}
// 实现一:纯 Python(进程级薄壳)
type PythonSandbox struct{ ... }
// 实现二:Docker(容器级厚壳)------接口不变,换实现即可
type DockerSandbox struct{ ... }
工具层(如 execute_skill_script)只依赖 Sandbox 接口;技能(SKILL.md)只描述"调哪个脚本、传什么参数"。将来从 Python 沙箱切换到 Docker 沙箱,对上层完全透明------这就是"换壳不换芯"。
六、总结
- Python 沙箱和 Docker 沙箱执行的是同一件事 (
python script.py),区别只是隔离壳厚度:薄壳管超时和输出大小,厚壳管文件、网络、资源、权限。 - 不是"套一层"的关系,而是同一执行方式的两档隔离强度,接口抽象好就可以随时换档。
- 选型看代码可信度:单租户、脚本输入服务端可控、纯标准库------Python 沙箱一劳永逸;多租户、开放脚本上传------Docker 沙箱是底线。
- Docker 沙箱防的是不可信的代码,Python 沙箱防的是失控的进程------先想清楚你防的是哪种,再决定用哪档。
项目实践参考:我们的知识库 Agent(技能脚本均为纯标准库 JSON 处理、单租户架构、网络能力由 MCP 工具提供)选择了 Python 沙箱,本地开发与 Linux 部署均适用;Docker 沙箱作为多租户场景的升级选项,接口已预留。