大模型开发 - 手写Manus之Sandbox执行代码:03 用Docker为AI Agent打造安全沙箱
本文介绍了如何为AI Agent构建安全的代码执行环境。通过Docker容器实现沙箱隔离,设计了DockerSandbox组件管理容器生命周期,包括安全配置(禁用网络、限制资源)、容器启动和命令执行功能。该方案采用轻量级Python镜像,提供内存/CPU限制和超时控制,确保恶意代码无法危害主机系统。代码执行结果封装为包含标准输出、错误输出和状态码的结构体,为上层工具提供统一接口。
文章目录

引言
在上一篇中,我们搭建了AI Agent的基础架构,实现了文件读写能力。但一个真正强大的Agent不应只能操作文件——它应该能执行代码。
想象这样的场景:用户说"写一个Python脚本计算斐波那契数列,并把结果保存到文件"。Agent需要先让大模型生成Python代码,然后实际执行这段代码得到结果,最后将结果写入文件。这就需要一个安全的代码执行环境。
直接在宿主机执行用户代码是极其危险的——恶意代码可能删除文件、窃取数据、耗尽资源。因此,我们选择Docker容器作为沙箱,实现安全隔离的代码执行。
本文将实现两个核心组件:
DockerSandbox:Docker容器的生命周期管理SandboxTool:面向Agent的沙箱工具封装
一、新增依赖
<!-- Docker Java客户端 -->
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-core</artifactId>
<version>3.3.6</version>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java-transport-httpclient5</artifactId>
<version>3.3.6</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.0.3</version>
</dependency>
二、DockerSandbox:容器生命周期管理
2.1 安全配置
沙箱的安全性是第一要务,我们通过内部类定义默认配置:
public static class SandboxSettings {
public static String image = "python:3.12-slim"; // 轻量级Python镜像
public static String workDir = "/workspace"; // 容器内工作目录
public static String memoryLimit = "512m"; // 内存限制512MB
public static double cpuLimit = 1.0; // CPU限制1核
public static int timeout = 300; // 超时5分钟
public static boolean networkEnabled = false; // 禁用网络访问!
}
networkEnabled = false是最关键的安全决策——容器无法访问网络,防止恶意代码外传数据或发起攻击。
2.2 容器启动
public class DockerSandbox {
private final DockerClient dockerClient;
private String containerId;
private boolean isRunning = false;
public DockerSandbox() {
DefaultDockerClientConfig config = DefaultDockerClientConfig
.createDefaultConfigBuilder()
.withDockerHost("unix:///var/run/docker.sock")
.build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
.dockerHost(config.getDockerHost())
.build();
this.dockerClient = DockerClientImpl.getInstance(config, httpClient);
}
public void start() throws Exception {
if (isRunning) return;
// 如果镜像不存在则自动拉取
pullImageIfNeeded();
// 创建容器,配置资源限制
HostConfig hostConfig = new HostConfig()
.withMemory(parseMemoryLimit(SandboxSettings.memoryLimit))
.withCpuQuota((long) (SandboxSettings.cpuLimit * 100000))
.withCpuPeriod(100000L)
.withNetworkMode(SandboxSettings.networkEnabled ? "bridge" : "none")
.withAutoRemove(true); // 容器停止后自动删除
CreateContainerResponse container = dockerClient
.createContainerCmd(SandboxSettings.image)
.withWorkingDir(SandboxSettings.workDir)
.withHostConfig(hostConfig)
.withAttachStdout(true)
.withAttachStderr(true)
.withTty(true)
.exec();
containerId = container.getId();
dockerClient.startContainerCmd(containerId).exec();
isRunning = true;
}
}
2.3 命令执行
在运行的容器中执行命令,捕获stdout和stderr,并支持超时控制:
public SandboxExecutionResult executeCommand(String command) throws Exception {
if (!isRunning) {
throw new IllegalStateException("Sandbox is not running");
}
// 创建exec实例
ExecCreateCmdResponse execResponse = dockerClient
.execCreateCmd(containerId)
.withAttachStdout(true)
.withAttachStderr(true)
.withCmd("/bin/sh", "-c", command)
.exec();
String execId = execResponse.getId();
// 执行并捕获输出
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
ExecStartResultCallback callback = new ExecStartResultCallback(stdout, stderr);
dockerClient.execStartCmd(execId).exec(callback);
// 等待完成,带超时控制
boolean finished = callback.awaitCompletion(SandboxSettings.timeout, TimeUnit.SECONDS);
if (!finished) {
callback.close();
return new SandboxExecutionResult(null, "Command timed out", 124, true);
}
// 获取退出码
InspectExecResponse execInfo = dockerClient.inspectExecCmd(execId).exec();
Integer exitCode = execInfo.getExitCode();
return new SandboxExecutionResult(
stdout.toString(StandardCharsets.UTF_8),
stderr.toString(StandardCharsets.UTF_8),
exitCode != null ? exitCode : 0,
false
);
}
2.4 执行结果封装
public static class SandboxExecutionResult {
private final String stdout;
private final String stderr;
private final int exitCode;
private final boolean timedOut;
public boolean isSuccess() { return exitCode == 0 && !timedOut; }
public String getCombinedOutput() {
StringBuilder sb = new StringBuilder();
if (stdout != null && !stdout.trim().isEmpty()) {
sb.append(stdout);
}
if (stderr != null && !stderr.trim().isEmpty()) {
if (sb.length() > 0) sb.append("\n");
sb.append("STDERR: ").append(stderr);
}
return sb.toString();
}
}
三、SandboxTool:面向Agent的工具封装
SandboxTool将Docker沙箱封装为Agent可以调用的工具,支持四种操作:
public class SandboxTool extends BaseTool {
private DockerSandbox sandbox;
public SandboxTool() {
super("sandbox", "Execute commands safely in a Docker container sandbox");
}
@Override
public Map<String, Object> getParametersSchema() {
return buildSchema(
Map.of(
"action", enumParam("Sandbox action",
List.of("start", "stop", "execute", "status")),
"command", stringParam("Command to execute in sandbox"),
"language", enumParam("Programming language",
List.of("python", "bash", "node", "java")),
"code", stringParam("Code to execute"),
"working_dir", stringParam("Working directory in container")
),
List.of("action")
);
}
}
3.1 多语言代码执行
关键在于buildCodeExecutionCommand方法——它使用Heredoc语法将代码安全地传递给解释器,避免复杂的字符转义:
private String buildCodeExecutionCommand(String code, String language) {
switch (language.toLowerCase()) {
case "python":
return buildHeredocCommand(code, "python3");
case "bash":
return code; // Bash直接执行
case "node":
return buildHeredocCommand(code, "node");
case "java":
// Java需要写文件 -> 编译 -> 执行
String javaHeredoc = buildHeredocToFile(code, "/tmp/Main.java");
return javaHeredoc + " && cd /tmp && javac Main.java && java Main";
default:
throw new IllegalArgumentException("Unsupported language: " + language);
}
}
private String buildHeredocCommand(String code, String interpreter) {
// 使用时间戳生成唯一分隔符,确保不与代码冲突
String delimiter = "OPENMANUS_CODE_EOF_" + System.currentTimeMillis();
return String.format("%s << '%s'\n%s\n%s", interpreter, delimiter, code, delimiter);
}
为什么用Heredoc? 相比其他方式传递代码,Heredoc可以原样保留代码中的引号、换行、特殊字符,避免转义地狱。
3.2 自动启动机制
执行代码时,沙箱会自动启动,无需用户手动操作:
private ToolResult handleExecute(Map<String, Object> parameters) {
// 自动启动沙箱
if (sandbox == null) {
sandbox = new DockerSandbox();
sandbox.start();
} else if (!sandbox.isRunning()) {
sandbox.start();
}
String command = getString(parameters, "command");
String code = getString(parameters, "code");
String language = getString(parameters, "language");
String actualCommand;
if (command != null) {
actualCommand = command;
} else if (code != null && language != null) {
actualCommand = buildCodeExecutionCommand(code, language);
} else {
return ToolResult.error(
"Either 'command' or both 'code' and 'language' must be provided");
}
DockerSandbox.SandboxExecutionResult result = sandbox.executeCommand(actualCommand);
if (result.isSuccess()) {
return ToolResult.success(result.getCombinedOutput());
} else if (result.isTimedOut()) {
return ToolResult.error("Command timed out");
} else {
return ToolResult.error("Command failed with exit code " + result.getExitCode()
+ ":\n" + result.getCombinedOutput());
}
}
四、注册工具并更新系统提示词
在ManusAgent中注册沙箱工具,并添加使用规则:
// ManusAgent构造函数中
toolCollection.addTool(new SandboxTool());
// 系统提示词新增规则
private final static String SYSTEM_PROMPT = """
# 角色定义
你是Manus,一个多功能的AI代理,能够使用可用的工具处理各种任务。
# 规则
- 工作目录:{workspace}
- Sandbox里面不使用工作目录
- 利用Sandbox执行代码时,直接把代码内容传给Sandbox,而不是把代码脚本文件传给Sandbox
- 一次只能执行一个工具
""";
注意两条新增的规则:
- “Sandbox里面不使用工作目录”——容器内有自己的文件系统,不需要使用宿主机路径
- “直接把代码内容传给Sandbox”——让大模型使用
code+language参数,而非先写文件再执行
五、完整执行流程
用户输入:“写一个计算前10个斐波那契数的Python脚本,然后把结果保存到fibonacci.txt文件中”
Step 1: LLM推理 -> 调用sandbox工具
action: "execute"
language: "python"
code: |
fib = [0, 1]
for i in range(8):
fib.append(fib[-1] + fib[-2])
print(fib)
-> Docker自动启动,执行Python代码
-> 返回: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Step 2: LLM推理 -> 调用write_file工具
file_path: "workspace/fibonacci.txt"
content: "[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]"
-> 文件写入成功
Step 3: LLM推理 -> finish_reason="stop"
-> 任务完成
总结
通过Docker沙箱,我们的Agent获得了安全执行任意代码的能力:
- 安全隔离:内存限制、CPU限制、网络禁用、自动清理
- 多语言支持:Python、Bash、Node.js、Java
- Heredoc传输:优雅地将代码传入容器,避免转义问题
- 自动管理:容器按需启动,无需用户关心底层细节
结合之前的文件读写工具,Agent已经可以完成"思考->编程->执行->保存"的完整工作流。

更多推荐


所有评论(0)