在这里插入图片描述

引言

在上一篇中,我们搭建了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"
  -> 任务完成
文件系统 (write_file) Docker 容器 Sandbox 工具 大语言模型 (LLM) 用户 文件系统 (write_file) Docker 容器 Sandbox 工具 大语言模型 (LLM) 用户 Step 1: 代码执行 Step 2: 结果保存 Step 3: 任务总结 调用 execute (python) 启动并执行代码 返回 [0, 1, ..., 34] 返回执行结果 调用 write_file (fibonacci.txt) 文件写入成功 任务完成 (finish_reason="stop")

总结

通过Docker沙箱,我们的Agent获得了安全执行任意代码的能力:

  • 安全隔离:内存限制、CPU限制、网络禁用、自动清理
  • 多语言支持:Python、Bash、Node.js、Java
  • Heredoc传输:优雅地将代码传入容器,避免转义问题
  • 自动管理:容器按需启动,无需用户关心底层细节

结合之前的文件读写工具,Agent已经可以完成"思考->编程->执行->保存"的完整工作流。


在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐