
这两年做 AI agent 项目最大的感受不是模型能力不够而是“让 agent 能动手干活”这件事比想象中麻烦得多。模型推理环节已经很成熟可真到让它调用工具、执行代码、读写文件的时候安全和失控的问题就全冒出来了。我最近把一个内部项目从裸跑迁移到 opensandbox 加 ADK 的组合用 ADKAgent Development Kit编排 agent把工具执行和代码运行全部收进沙盒环境目前稳定跑了快一个月线上事故为零。这篇文章就把这套组合讲透为什么 agent 必须沙盒化、ADK 和 opensandbox 各自扮演什么角色以及从安装、建 agent 到调试的一次完整实操。适合正在做 AI agent 开发、尤其是关注工具调用安全性的工程师还在选型的朋友也值得从头看完。1. 这个项目到底解决了什么问题1.1 裸跑的 agent真的让人不放心Agent 本质上不是一个“聊天机器人”它是一个能自我决策的软件系统。LLM 只是决策引擎当你给它挂上工具——执行代码、文件系统读写、发网络请求、调外部 API、甚至发邮件——它就开始“动手”了。但问题在于LLM 没有人类那种边界感。它不知道哪些操作危险不知道一条命令的副作用有多大它只知道“用户让我做这件事我要尽力完成”。我在项目里遇到过两个特别典型的危险场景。第一个是研究助手类 agent用户让它“统计项目里所有包含敏感字段的文件”它真的会递归遍历整个宿主文件系统把配置文件、密钥文件、测试数据全读一遍。模型没有“这个文件不该碰”的意识它只会机械地执行任务。第二个是让 agent 写一段爬虫代码代码本身没毛病但陷入了一个死循环CPU 直接飙满整个开发机的服务都跟着卡顿。这还不是最坏的——如果 agent 挂在线上服务里一个失控的工具调用就可能导致数据被删、账单飙升、隐私泄漏。所以核心矛盾根本不是“模型不够聪明”而是缺少一个可控的执行框。你要做的不是禁止 agent 调用工具而是让它的所有危险动作都落在一个有资源限制、有网络策略、有超时控制的环境里执行。这就是沙盒环境存在的意义也是这套方案的出发点。opensandbox 在这里承担的就是“隔离执行”的职责而 ADK 负责把 agent 的业务逻辑编排清楚两者配合才真正解决了我前面遇到的那些问题。1.2 opensandbox 与 ADK 的分工很多人第一次听到“opensandbox 结合 ADK”会误以为这两个东西是重复的——不都是做 agent 的吗其实它们管的是完全不同的两层。ADK 面向“智能层”它负责多步推理、工具注册、模型接入、对话上下文管理opensandbox 面向“执行层”它负责代码运行、资源限制、网络策略、文件系统隔离和审计日志。可以这样理解ADK 是 agent 的大脑和神经opensandbox 是手脚的安全护具。层面ADK 负责opensandbox 负责业务编排多 agent 调度、工具注册、模型调用-运行隔离-指令级代码执行、资源限制、进程管理网络安全对话上下文中的工具参数传递出口网络策略、禁网/白名单控制审计工具调用链路日志沙盒内进程日志、文件变更记录、网络请求记录两者通过“工具调用边界”衔接。ADK 里注册一个工具工具的 Python 函数并不在本地直接执行而是把请求发给 opensandbox由沙盒创建隔离环境去执行再把 stdout、stderr 和退出码拿回来。agent 该有的灵活性和上下文能力完整保留但出格的动作全被挡在沙盒里面。这个分工让安全边界非常清晰业务代码怎么写都不会碰到宿主系统。1.3 哪些人最该看这篇如果你正在做 AI agent 开发而且 agent 涉及工具调用、代码执行或文件操作这篇文章就是为你准备的。具体来说你在用 LangChain、CrewAI 或自己手写 agent但一直担心工具执行不可控你准备把 agent 从 demo 推向真实业务需要一套可落地的沙盒方案你在比较 ADK 和其他 agent 框架想了解代码优先框架在安全隔离上的优势。另外一个容易被忽略的群体是平台工程师。如果你的团队要提供一个“agent 托管服务”那沙盒隔离几乎是必选项opensandbox 这种方案就能直接嵌入你的基础设施。后面几节的内容完全可以照着落地。2. 为什么选 ADK 加 opensandbox 这个组合2.1 相比 LangChain、Dify、CrewAIADK 赢在哪市面上的 agent 框架已经很多了LangChain 生态庞大Dify 可视化建 workflow 方便CrewAI 主打多角色协作每个都有自己的适用场景。但放到“要接外部沙盒做运行时隔离”这个具体目标下ADK 有两个优势非常突出。第一个是代码优先带来的自由度和可控性。Dify 这类平台把 agent 抽象成可视化节点做普通流程很方便可真到了需要接入 opensandbox 这种外部执行引擎、想在工具调用前做一层自定义策略、想精确控制每一个回调行为的时候可视化平台的抽象反而成了阻碍。ADK 里 agent 就是普通的 Python 类或对象工具注册、调用链、回调逻辑全部在代码里显式控制。接入 opensandbox 时我只需要在工具函数内部调用沙盒客户端没有任何框架层面的阻力。第二个是原生多 agent 支持但复杂度不高。ADK 从设计上就支持多 agent 编排不需要像 LangChain 那样自己拼凑图形结构或者依赖第三方工具。多 agent 场景下安全风险会成倍增加因为每个子 agent 都可能持有工具。ADK 允许你在 agent 层级统一绑定执行后端所有子 agent 的工具调用都走同一条沙盒路径配置一次全部生效。这一点对我这种懒人来说很关键——我不想给十个子 agent 各配一遍沙盒。当然我也要承认ADK 的生态相对年轻第三方集成不如 LangChain 丰富。但我的观点很直接选框架是为了把 agent 安全地放出去跑业务不是为了让集成的清单更长。模型可以随时换安全边界必须是稳定的。2.2 opensandbox 的沙盒能力我实际用到哪几层opensandbox 做的事情一句话概括给“执行”装了一个透明但严格的安全罩。它不是我见过最炫酷的技术但每一层能力都对应一个真实的事故场景。资源限制可以给 CPU、内存、进程数设置上限。agent 写的死循环代码、内存泄漏代码、甚至 fork 炸弹都会被限制在沙盒内不影响宿主。文件系统隔离沙盒内看到的是独立根目录读写都落在隔离层里宿主文件系统默认不可见。要实现数据交换必须显式挂载目录。网络策略可以完全禁止外网访问也可以配置内网白名单。对大多数 agent 任务来说默认禁网就能消掉大部分数据外泄风险。超时终止任何任务超过设定时间都会被强制杀掉。这条很实用agent 有时候会在工具里“跑偏”没有超时控制你根本不知道它到底卡在哪一步。审计日志沙盒里跑过的命令、读写的文件路径、发出的网络请求全部有执行记录。排查问题的时候这些日志就是事故现场的监控录像。我实际用下来觉得最容易被忽视的是“显式挂载目录”的设计。agent 经常需要处理输入数据比如让 agent 分析一份上传的 CSV。你既不能让它随便访问宿主文件又必须给它正确的数据。opensandbox 的做法是把输入目录以只读方式挂载进沙盒agent 能读但改不了输出则写到另一个独立挂载目录。这个模型非常干净输入归输入输出归输出想写出 bug 都难。2.3 整体架构与一次请求的走向很多人看架构图会头晕我尝试用文字把一条完整链路描述清楚。假设用户对 agent 说“帮我写个 Python 脚本把这份 CSV 里的销量按月份汇总。”用户请求进来后先到 ADK 的主 agent。主 agent 理解任务后决定需要调用工具——这个工具就是我在 ADK 里注册的沙盒执行工具。ADK 把参数代码或脚本路径交给工具函数但这个工具函数不会在本地执行而是把请求发给 opensandbox 服务。opensandbox 收到请求后按配置创建一次性沙盒容器在资源受限、网络受限的环境里执行指定的代码或命令。执行完了stdout、stderr 和退出码被返回给工具函数ADK 再把执行结果拼进对话上下文由模型输出最终答复。环节职责对应组件请求层接收用户输入维护会话应用前端 / API编排层多步推理、工具选择、上下文管理ADK执行层创建并管理隔离容器opensandbox安全层网络策略、资源限制、文件隔离opensandbox runtime观测层审计日志、调用链追踪opensandbox ADK 日志这个架构关键价值在于三个“可控”运行可控、网络可控、可观测。运行可控是指资源限制和超时保证不会拖垮服务网络可控是指默认禁网的策略掐掉了大部分数据泄漏路径可观测是指每一步都有日志出了问题能快速定位到是模型的决策问题还是沙盒执行问题。3. 环境准备与安装细节3.1 安装 ADK先说环境要求。ADK 的 Python 版本需要 3.10 以上我个人建议直接用 3.11 或 3.12别在旧版本上折腾兼容性。强烈建议用虚拟环境因为 agent 项目的依赖通常很杂模型 SDK、工具库、沙盒客户端堆在一起虚拟环境可以避免很多头疼的问题。python3 -m venv .venv source .venv/bin/activate pip install google-adk0.1.6我特意把版本写死是有原因的。ADK 迭代很快后一个小版本 API 可能就有调整写死在当前依赖里可以保证行为可复现。等项目稳定后再统一升级而不是每次 pip install 都抽盲盒。安装完以后可以做个简单验证python -c from adk.agents import Agent; print(adk ok)能打印出 adk ok 就说明环境没问题。这里有个小细节不同 ADK 版本导入路径可能有差异如果导入报错大概率是版本号对不上或者虚拟环境没激活。3.2 部署 opensandboxopensandbox 我建议作为独立服务来跑监听本地的一个端口。开发环境直接本机起一个实例就行团队协作时也可以部署到内网机器让大家共用一套沙盒服务。这么做的好处是沙盒的生命周期跟 agent 业务进程分开了沙盒更新、重启不会影响 agent 主服务。opensandbox init sandbox-code-exec cd sandbox-code-exec opensandbox start --port 63211初始化之后会生成一个沙盒配置文件我通常把它改成这样# sandbox.yaml name: sandbox-code-exec runtime: cpu_limit: 1.0 memory_limit: 1Gi pids_limit: 64 timeout: default: 30s network: policy: no-internet allow_private: true filesystem: workspace: /tmp/sandbox-workspace mount_input: true几个关键参数解释一下。cpu_limit 给 1 核对大多数代码执行任务足够机器学习训练那种另说。memory_limit 1Gi 是个比较安全的起步值。network 里的 policy 设成 no-internet 是默认禁网allow_private 为 true 表示内网服务可以访问这样 agent 能调内部测试接口但出不去外网。这里我踩过一个坑一开始把超时设成 20 秒结果稍微复杂一点的数据处理任务频繁超时agent 连续报错三次以后直接放弃说“我做不到”。后来改成 45 秒成功率才上来。安全不是把时间卡得越死越好而是给一个明确的上限。3.3 打通两边封装沙盒客户端ADK 和 opensandbox 之间的桥梁就是一个简单的沙盒客户端封装。我把所有对沙盒的调用都收敛到一个函数里agent 工具只依赖这个函数不直接接触 opensandbox API。这样即便沙盒服务以后换实现agent 业务代码也几乎不用改。from opensandbox.client import SandboxClient client SandboxClient(http://127.0.0.1:63211) def run_in_sandbox(code: str) - dict: task client.create_task( imagepython:3.11-slim, command[python, -c, code], timeout15, ) task.wait() return { stdout: task.stdout, stderr: task.stderr, exit_code: task.exit_code, duration_s: task.duration, }这个封装做了两件很重要的事。第一统一返回结构不管执行成功还是失败调用方拿到的都是一个 dict方便上层工具包装。第二把 timeout 限制放在调用参数里每个任务单独控制超时不会让所有任务共用沙盒的默认值。实际操作中我还习惯加一层日志记录每次沙盒调用花了多长时间、返回了什么东西这对后面对 agent 做性能分析很有帮助。4. 创建并测试第一个沙盒 agent4.1 用 ADK 定义带工具的 agentAgent 的定义很直接我写一个名为 code_sandbox_agent 的最小化例子。它只暴露一个工具 code_run功能是接收用户传来的 Python 代码在沙盒里执行并返回结果。from adk.agents import Agent from adk.tools import tool tool def code_run(code: str) - str: 在隔离沙盒中执行python代码返回执行结果。失败时包含stderr信息。 result run_in_sandbox(code) if result[exit_code] ! 0: return fERROR({result[exit_code]}): {result[stderr]} return result[stdout] agent Agent( namecode_sandbox_agent, modelgemini-2.0-flash, instruction你是一个代码执行助手。当用户要求运行代码时你必须调用code_run工具 不得在本地直接执行任何操作。, tools[code_run], )这里有几个设计细节值得说。工具函数的 docstring 写清楚“失败时包含 stderr 信息”是有讲究的因为模型看不到函数内部实现它的唯一信息来源就是 docstring 和返回的字符串。你写清楚了模型才知道这个工具会返回什么。还有我把 stdout 和 stderr 合并成一个字符串返回而不是拆成两个字段。原因很简单模型只能看到一串返回值如果你把错误单独传一个字段有些模型会选择性忽略合并之后它会意识到执行出错了至少会尝试解释一下。4.2 工具内部怎么把执行交给沙盒上一节的 code_run 已经给出了整体结构真正关键的其实是 run_in_sandbox 内部的细节。除了直接执行命令我还经常需要处理“文件挂载”的场景。比如让 agent 处理一个用户上传的文件那沙盒执行时就需要挂载目录。task client.create_task( imagepython:3.11-slim, command[python, process.py], mounts[ { source: /path/to/input, target: /work/input, mode: ro, }, { source: /path/to/output, target: /work/output, mode: rw, }, ], )注意这里 input 目录模式是 ro只读output 目录是 rw可写。为什么要分开因为如果只挂一个目录并且可读可写agent 极有可能把中间结果覆盖到原始输入上数据一旦被污染很难追溯。输入只读输出单独放两者物理隔离才能保证“沙盒外数据不被破坏”这条底线。Instruction 里的“必须调用 code_run不得在本地直接执行”也不是废话。agent 毕竟是概率模型你明确告诉它边界在哪它走偏的概率就小一些。我在测试中还真遇到过 agent 试图在对话里直接输出 shell 命令而不是调用工具的情况加了这句约束之后就再没出现了。4.3 测试沙盒环境的三个典型场景创建完 agent真正的沙盒测试要分场景来跑。我会跑三类用例代码执行、文件隔离、网络策略。这三类都过了才敢说这个环境配置是对的。第一个测试是简单的代码执行。我让 agent“算一下 123 乘以 456”它会调用 code_run 工具然后回给我 56088。看起来平淡无奇但我其实更关心 opensandbox 的审计日志我会看到类似这样的记录task_id: 5201a9, image: python:3.11-slim, command: [python, -c, print(123*456)], network: blocked, exit_code: 0看到 network: blocked 这一项我才放心。它证明这个执行确实是在沙盒隔离环境里完成的而且网络是被拦截的。第二个测试是文件隔离。我让 agent“写一个包含三行 Hello 的 txt 文件到 /tmp/a.txt”。如果我的沙盒配置正确这个文件在宿主机上是找不到的因为沙盒里的 /tmp 和宿主的 /tmp 完全隔离。这个测试能让我确认文件系统隔离是真的生效了而不是假隔离。如果业务上确实需要文件结果落回宿主那必须通过挂载目录把输出写到挂载位置宿主才能看到。第三个测试是网络策略。我在内网跑了一个测试服务让 agent“访问 api.test.internal/status 并告诉我返回值”。因为配置里 allow_private 为 true所以这个请求会被放行agent 能拿到内网服务的数据。同时再让它访问一个外网地址比如公开的测试 API结果会被 opensandbox 拦截。这一步验证的是“内网可用、外网禁行”的策略是否严格按照配置执行。这三个测试本质上测的都不是模型智商而是“边界能力”哪些动作被允许、哪些被拒绝、日志是否完整可追溯。把这些边界测清楚agent 的安全上线才算有底气。5. 常见问题与排查实录5.1 agent execution terminated due to error这个报错我在开发初期几乎天天见。现象是 agent 跑着跑着突然中断终端只留一句 “agent execution terminated due to error”。说实话这条信息的帮助不大更像是一个总括性的状态提示真正的问题隐藏在日志深处。我的排查路径是固定的。第一步先看模型调用本身有没有问题比如超时、配额耗尽、返回空内容。这种情况通常会在 ADK 的日志里留下模型层的 error。第二步看是不是工具函数里抛了未捕获的异常。如果工具执行出错后直接把异常往上抛ADK 的运行循环会被打断然后就会出现这条终止信息。经验是工具函数内部一定要做兜底所有可能出错的逻辑都用 try/except 包住确保工具永远返回字符串而不是把异常抛给 agent 框架。我写工具的时候会在最外层统一捕获异常返回一个以 “ERROR:” 开头的字符串。这样模型不仅能拿到可读的错误原因agent 循环也不会被异常打断。5.2 RPC 通信类错误empty sid and service name还有一类报错看起来特别唬人叫 “agent rpc error (-1): empty sid and service name”。我在第一次遇到的时候花了不少时间才定位到问题。这个错误本质上出现在 agent 主进程和沙盒服务之间的 RPC 通信层。沙盒客户端发起调用时会话标识sid是空的服务端不知道这个请求属于哪个会话、哪个实例于是直接拒绝或返回错误码 -1。排查按三步走。第一步确认 opensandbox 服务本身是健康的看它的健康检查接口是否能正常返回。第二检查客户端初始化是否传了正确的 API Token 或 sid很多新接入的同事就是漏了这一步。第三看看是不是有多实例部署导致 sid 失效——如果客户端连接的是沙盒实例 A但实际请求被路由到了实例 B而 B 没有同步会话上下文就会报出这种空 sid 的 RPC 错误。排查项检查方式修复建议沙盒服务健康opensandbox 健康检查接口确认服务进程存活端口可访问客户端凭证检查初始化代码是否传 token/sid在连接配置中补充凭证信息多实例路由查看请求实际访问的沙盒实例统一接入负载均衡或固定实例绑定会话上下文检查 RPC 头里 sid 是否为空重建会话后重试这类问题在本地开发环境不太容易遇到因为单实例跑着不会串但一旦上了内网服务多客户端共用一个沙盒集群就很容易暴露出来。5.3 沙盒里缺依赖、目录不对还有一类很常见的问题执行本身没有被错误终止但返回结果明显不对。比如 agent 调用 code_run 执行了一个需要第三方库的代码返回的 stderr 里写着 ModuleNotFoundError: No module named requests。这是因为沙盒里的镜像是一个精简环境默认只带 Python 标准库并不包含 agent 业务需要的所有依赖。解决办法有两个思路。一个是在创建任务时指定一个更完整的镜像把我的项目依赖提前打进镜像里另一个是在执行前先跑一步安装命令。我目前的做法是维护一个带依赖的专用镜像构建好后在沙盒配置里指定它这样能省掉每次执行都装依赖的开销。目录相关的问题也常遇到。agent 在沙盒里运行时工作目录可能不是你以为的路径导致文件找不到或者写到错误的位置。我会在 command 里显式指定工作目录比如cd /work/input python process.py避免依赖沙盒的默认工作目录。调这类问题的时候多看一遍 stderr里面通常会有非常明确的文件路径信息。5.4 问题速查表把上面这些经验整理成一张速查表遇到问题先对着查一遍能省不少时间。报错情况重现时的线索最可能的根因agent execution terminated due to error日志在工具调用前后中断工具函数抛异常未捕兜底empty sid and service name沙盒服务集群化部署后出现RPC 会话标识未传或实例错配ModuleNotFoundError沙盒内代码运行报错沙盒镜像缺依赖FileNotFoundError挂载目录路径不对工作目录与挂载 target 不一致工具返回“无输出”stdout 为空但退出码为 0代码本身没输出内容需提示模型任务频繁超时agent 执行复杂任务时失败沙盒 timeout 参数设置过窄6. 实操经验与可复用的配置6.1 沙盒资源参数怎么定资源参数没有万能答案但我总结了一套比较稳的起步配置。CPU 限制从 1 核开始绝大多数 agent 工具调用场景都不需要更多。内存用 1Gi 起步如果 agent 经常做数据处理提到 2Gi 也问题不大。进程数限制建议给 64 左右别开太宽这个参数是防 fork 炸弹的。超时时间我前面已经说过是按“同类任务平均耗时的 1.5 到 2 倍”来设不是越小越安全而是要给足合理波动空间。我后来反思之前把超时设太紧的教训得出的结论是沙盒参数的设计目标不该是“把所有任务都卡得很快”而是“把失控任务挡在门外”。正常任务跑 20 秒超时设 20 秒等于天天在误杀。改成 40 到 45 秒之后误杀率大幅下降真正死循环的任务到点一样被干掉安全效果并没有打折扣。6.2 测试数据与沙盒生命的周期沙盒默认生命周期很短任务执行完容器大概率会被回收。这在生产环境是好事资源不浪费但调试的时候就很痛苦因为错误现场没了。如果你需要复现一个沙盒里的问题或者想把失败的现场保留下来给同事分析建议打开 opensandbox 的失败任务保留选项。我一般会设置失败任务保留 24 小时保留期到了再自动清理。输出数据同理。agent 处理完的结果如果只在沙盒里任务结束也就没了。所以凡是需要留痕的产物我都走挂载目录把输出写到宿主机持久化路径上。这个习惯帮我避免过好多次“agent 说跑完了但结果无处可寻”的尴尬。6.3 审计日志不只是安全威慑审计日志这个东西很多时候被当成合规摆设。但实际用下来它是我调试 agent 最有力的工具。当 agent 行为不符合预期时我先不看模型的思考过程直接去翻沙盒日志看它到底执行了什么命令、访问了什么地址、读写了哪些文件。模型说得再好听日志不会骗人。给大家一条我经常用的日志解读思路。看到命令正常执行、退出码 0但模型回答却跟实际结果对不上那八成是模型在整理答案时出了问题属于逻辑层 bug。看到沙盒拒绝了某个网络请求或某个文件读取但模型还假装成功那就要考虑是工具返回值没讲清还是模型的指令约束没落实。日志把执行层和逻辑层分开定位问题就能在一个很浅的深度完成。这套 opensandbox 加 ADK 的组合我在内部稳定跑了将近一个月最深的一点体会是agent 开发里的安全问题在设计阶段解决了才是真省心。ADK 把 agent 业务写得简洁opensandbox 把危险动作兜住接好之后你在业务代码里不需要时刻提心吊胆。最后再分享一个小技巧刚开始接沙盒时别一次性把所有工具都挂给 agent先只挂一个“允许执行 Python 代码”的最小工具集跑通了再按需增加。这个做法帮我避开了很多调试灾难希望你也能少走这段弯路。