文章目录引言一、新增依赖二、DockerSandbox容器生命周期管理2.1 安全配置2.2 容器启动2.3 命令执行2.4 执行结果封装三、SandboxTool面向Agent的工具封装3.1 多语言代码执行3.2 自动启动机制四、注册工具并更新系统提示词五、完整执行流程总结引言在上一篇中我们搭建了AI Agent的基础架构实现了文件读写能力。但一个真正强大的Agent不应只能操作文件——它应该能执行代码。想象这样的场景用户说写一个Python脚本计算斐波那契数列并把结果保存到文件。Agent需要先让大模型生成Python代码然后实际执行这段代码得到结果最后将结果写入文件。这就需要一个安全的代码执行环境。直接在宿主机执行用户代码是极其危险的——恶意代码可能删除文件、窃取数据、耗尽资源。因此我们选择Docker容器作为沙箱实现安全隔离的代码执行。本文将实现两个核心组件DockerSandboxDocker容器的生命周期管理SandboxTool面向Agent的沙箱工具封装一、新增依赖!-- Docker Java客户端 --dependencygroupIdcom.github.docker-java/groupIdartifactIddocker-java-core/artifactIdversion3.3.6/version/dependencydependencygroupIdcom.github.docker-java/groupIdartifactIddocker-java-transport-httpclient5/artifactIdversion3.3.6/version/dependencydependencygroupIdorg.apache.httpcomponents.client5/groupIdartifactIdhttpclient5/artifactIdversion5.0.3/version/dependency二、DockerSandbox容器生命周期管理2.1 安全配置沙箱的安全性是第一要务我们通过内部类定义默认配置publicstaticclassSandboxSettings{publicstaticStringimagepython:3.12-slim;// 轻量级Python镜像publicstaticStringworkDir/workspace;// 容器内工作目录publicstaticStringmemoryLimit512m;// 内存限制512MBpublicstaticdoublecpuLimit1.0;// CPU限制1核publicstaticinttimeout300;// 超时5分钟publicstaticbooleannetworkEnabledfalse;// 禁用网络访问}networkEnabled false是最关键的安全决策——容器无法访问网络防止恶意代码外传数据或发起攻击。2.2 容器启动publicclassDockerSandbox{privatefinalDockerClientdockerClient;privateStringcontainerId;privatebooleanisRunningfalse;publicDockerSandbox(){DefaultDockerClientConfigconfigDefaultDockerClientConfig.createDefaultConfigBuilder().withDockerHost(unix:///var/run/docker.sock).build();DockerHttpClienthttpClientnewApacheDockerHttpClient.Builder().dockerHost(config.getDockerHost()).build();this.dockerClientDockerClientImpl.getInstance(config,httpClient);}publicvoidstart()throwsException{if(isRunning)return;// 如果镜像不存在则自动拉取pullImageIfNeeded();// 创建容器配置资源限制HostConfighostConfignewHostConfig().withMemory(parseMemoryLimit(SandboxSettings.memoryLimit)).withCpuQuota((long)(SandboxSettings.cpuLimit*100000)).withCpuPeriod(100000L).withNetworkMode(SandboxSettings.networkEnabled?bridge:none).withAutoRemove(true);// 容器停止后自动删除CreateContainerResponsecontainerdockerClient.createContainerCmd(SandboxSettings.image).withWorkingDir(SandboxSettings.workDir).withHostConfig(hostConfig).withAttachStdout(true).withAttachStderr(true).withTty(true).exec();containerIdcontainer.getId();dockerClient.startContainerCmd(containerId).exec();isRunningtrue;}}2.3 命令执行在运行的容器中执行命令捕获stdout和stderr并支持超时控制publicSandboxExecutionResultexecuteCommand(Stringcommand)throwsException{if(!isRunning){thrownewIllegalStateException(Sandbox is not running);}// 创建exec实例ExecCreateCmdResponseexecResponsedockerClient.execCreateCmd(containerId).withAttachStdout(true).withAttachStderr(true).withCmd(/bin/sh,-c,command).exec();StringexecIdexecResponse.getId();// 执行并捕获输出ByteArrayOutputStreamstdoutnewByteArrayOutputStream();ByteArrayOutputStreamstderrnewByteArrayOutputStream();ExecStartResultCallbackcallbacknewExecStartResultCallback(stdout,stderr);dockerClient.execStartCmd(execId).exec(callback);// 等待完成带超时控制booleanfinishedcallback.awaitCompletion(SandboxSettings.timeout,TimeUnit.SECONDS);if(!finished){callback.close();returnnewSandboxExecutionResult(null,Command timed out,124,true);}// 获取退出码InspectExecResponseexecInfodockerClient.inspectExecCmd(execId).exec();IntegerexitCodeexecInfo.getExitCode();returnnewSandboxExecutionResult(stdout.toString(StandardCharsets.UTF_8),stderr.toString(StandardCharsets.UTF_8),exitCode!null?exitCode:0,false);}2.4 执行结果封装publicstaticclassSandboxExecutionResult{privatefinalStringstdout;privatefinalStringstderr;privatefinalintexitCode;privatefinalbooleantimedOut;publicbooleanisSuccess(){returnexitCode0!timedOut;}publicStringgetCombinedOutput(){StringBuildersbnewStringBuilder();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);}returnsb.toString();}}三、SandboxTool面向Agent的工具封装SandboxTool将Docker沙箱封装为Agent可以调用的工具支持四种操作publicclassSandboxToolextendsBaseTool{privateDockerSandboxsandbox;publicSandboxTool(){super(sandbox,Execute commands safely in a Docker container sandbox);}OverridepublicMapString,ObjectgetParametersSchema(){returnbuildSchema(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语法将代码安全地传递给解释器避免复杂的字符转义privateStringbuildCodeExecutionCommand(Stringcode,Stringlanguage){switch(language.toLowerCase()){casepython:returnbuildHeredocCommand(code,python3);casebash:returncode;// Bash直接执行casenode:returnbuildHeredocCommand(code,node);casejava:// Java需要写文件 - 编译 - 执行StringjavaHeredocbuildHeredocToFile(code,/tmp/Main.java);returnjavaHeredoc cd /tmp javac Main.java java Main;default:thrownewIllegalArgumentException(Unsupported language: language);}}privateStringbuildHeredocCommand(Stringcode,Stringinterpreter){// 使用时间戳生成唯一分隔符确保不与代码冲突StringdelimiterOPENMANUS_CODE_EOF_System.currentTimeMillis();returnString.format(%s %s\n%s\n%s,interpreter,delimiter,code,delimiter);}为什么用Heredoc相比其他方式传递代码Heredoc可以原样保留代码中的引号、换行、特殊字符避免转义地狱。3.2 自动启动机制执行代码时沙箱会自动启动无需用户手动操作privateToolResulthandleExecute(MapString,Objectparameters){// 自动启动沙箱if(sandboxnull){sandboxnewDockerSandbox();sandbox.start();}elseif(!sandbox.isRunning()){sandbox.start();}StringcommandgetString(parameters,command);StringcodegetString(parameters,code);StringlanguagegetString(parameters,language);StringactualCommand;if(command!null){actualCommandcommand;}elseif(code!nulllanguage!null){actualCommandbuildCodeExecutionCommand(code,language);}else{returnToolResult.error(Either command or both code and language must be provided);}DockerSandbox.SandboxExecutionResultresultsandbox.executeCommand(actualCommand);if(result.isSuccess()){returnToolResult.success(result.getCombinedOutput());}elseif(result.isTimedOut()){returnToolResult.error(Command timed out);}else{returnToolResult.error(Command failed with exit code result.getExitCode():\nresult.getCombinedOutput());}}四、注册工具并更新系统提示词在ManusAgent中注册沙箱工具并添加使用规则// ManusAgent构造函数中toolCollection.addTool(newSandboxTool());// 系统提示词新增规则privatefinalstaticStringSYSTEM_PROMPT # 角色定义 你是Manus一个多功能的AI代理能够使用可用的工具处理各种任务。 # 规则 - 工作目录{workspace} - Sandbox里面不使用工作目录 - 利用Sandbox执行代码时直接把代码内容传给Sandbox而不是把代码脚本文件传给Sandbox - 一次只能执行一个工具 ;注意两条新增的规则“Sandbox里面不使用工作目录”——容器内有自己的文件系统不需要使用宿主机路径“直接把代码内容传给Sandbox”——让大模型使用codelanguage参数而非先写文件再执行五、完整执行流程用户输入“写一个计算前10个斐波那契数的Python脚本然后把结果保存到fibonacci.txt文件中”Step1:LLM推理-调用sandbox工具 action:executelanguage:pythoncode:|fib[0,1]fori inrange(8):fib.append(fib[-1]fib[-2])print(fib)-Docker自动启动执行Python代码-返回:[0,1,1,2,3,5,8,13,21,34]Step2:LLM推理-调用write_file工具 file_path:workspace/fibonacci.txtcontent:[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]-文件写入成功Step3:LLM推理-finish_reasonstop-任务完成文件系统 (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_reasonstop)总结通过Docker沙箱我们的Agent获得了安全执行任意代码的能力安全隔离内存限制、CPU限制、网络禁用、自动清理多语言支持Python、Bash、Node.js、JavaHeredoc传输优雅地将代码传入容器避免转义问题自动管理容器按需启动无需用户关心底层细节结合之前的文件读写工具Agent已经可以完成思考-编程-执行-保存的完整工作流。