做 agent 开发的人可能都有过这种经历:代码在本地调试时一切正常,模型也老老实实按指令调用工具,可一旦把同样的 prompt 放到一个隔离环境里,各种“权限不足”“找不到文件”“工具没有响应”的问题就全都冒出来了。我最近用 opensandbox 这个沙盒环境配合 ADK(Agent Development Kit)创建了一套 agent 测试流程,从 agent 定义、工具注册、沙箱配置,再到常见的网络和资源隔离问题,从头到尾跑了一遍,总算把“写 agent”和“测 agent”这两件事分开了。这篇文章就是这次实操的完整记录,适合正在学 agent 开发的新手,也适合已经在用 LangChain、Dify、CrewAI 等框架、想补上安全与可观测性这块短板的同学。
1. 整体设计思路:为什么把 ADK 和 opensandbox 放在一起
1.1 沙盒不是可选项,是 agent 工程的底线
Agent 和传统脚本最大的不同,是它的行为不是写死的。模型会基于当前对话上下文自己决定调用哪个工具、传什么参数,这个参数可能是文件路径、命令字符串,也可能是网络请求地址。哪怕你只在本地跑一个“帮你整理资料”的 agent,它也有可能因为 prompt 被注入或者模型幻觉,产生一次破坏性的文件操作。所以 agent 的测试不应该只验证“功能对不对”,更应该验证“边界在不在”。
我见过一个真实案例:有人让 agent 帮忙分析某个目录下的日志,模型不知道是从哪里学到的一个坏习惯,直接把日志文件路径交给了shutil.rmtree。本地运行时因为权限足够宽,真的把目录删了。这种事故在传统程序里几乎不可能发生,因为代码是人工写死的,而在 agent 里,模型输出本身就是变量。opensandbox 这类沙盒环境要解决的核心问题,就是把 agent 的“自由度”限制在一个可控范围内,让它想飞也飞不出去。
沙盒的隔离主要分三层:文件系统隔离、网络隔离、资源隔离。文件系统层面,只给 agent读任务输入目录和写临时目录的权限;网络层面,只放行模型 API 地址和必要的内网服务;资源层面,限制 CPU、内存、执行超时,防止模型进入死循环后把整个宿主机拖垮。这三层合起来,相当于给 agent 配了一间“临时办公室”:里面有完成工作所需的所有资料,但没有公司财务系统,没有客户隐私,也没有通往互联网的自由通道。
1.2 ADK 与 opensandbox 各管哪一段
刚接触 agent 开发时,我很容易把“聪明”和“安全”混在一起。后来拆开看才明白:ADK 负责的是 agent 的编排能力,包括模型调用、多步推理、状态管理、工具注册;opensandbox 负责的是 agent 的运行环境,包括进程隔离、权限控制、网络策略、审计日志。这两个东西不是替代关系,而是控制层和执行层的关系。
用一个生活里的例子解释:ADK 像是军队指挥官,负责根据侦察信息制定作战计划;opensandbox 则是训练场,负责提供受控场地,记录士兵的一举一动。指挥官不需要知道训练场摄像头装在哪里,训练场也不关心作战策略本身。两者通过一个约定好的接口互动:沙盒接收 ADK 发起的一次次工具调用,返回执行结果,同时把执行过程中触碰到的资源记成日志。
这个分工给我带来了一个很直接的收益:在本地调试时,我可以先不开沙盒,直接让 agent 跑通功能;等所有逻辑确认无误后,再切换到 opensandbox 的严格模式,做安全回归测试。这样既不牺牲开发效率,也不牺牲上线安全。如果你把 agent 逻辑和安全配置耦合在一起,后面每加一个工具都要重新想权限,维护成本会迅速失控。
1.3 为什么选择 opensandbox,而不是虚拟机或裸容器
选型那阵子,我一度在“要不要直接用 Docker”和“要不要开一台虚拟机”之间犹豫。最终选择了 opensandbox,是因为它正好处在二者中间:比虚拟机轻,比裸容器更适合 agent 场景。
| 方案 | 隔离强度 | 启动速度 | Agent 场景适配度 |
|---|---|---|---|
| 虚拟机 | 最强 | 秒到分钟级 | 差,需要自己装全套依赖 |
| 裸 Docker 容器 | 中等 | 毫秒到秒级 | 一般,默认权限太大,缺少审计 |
| opensandbox | 中强 | 秒级 | 高,配置项直接面向工具调用和网络策略 |
opensandbox 底层默认使用 Docker 作为 runtime,但它在 Docker 之上加了一层面向 agent 的配置抽象。你不需要手动拼docker run的一大堆参数,只需要写一份 YAML,把 CPU、内存、网络白名单、可写目录声明清楚,剩下的它帮你处理。另外它还内置了日志审计能力,哪个工具被调用了、输入参数是什么、返回结果是什么,全都能结构化导出。这一步对定位 agent 的“异常聪明”非常关键。
如果你已经有一套 Kubernetes 或者云资源,当然不一定要用 opensandbox。不过在本地验证一个 agent 的最初阶段,它的心智负担最小:一个 CLI、一份 YAML、一个 agent 入口文件,就够了。
2. 核心细节解析与实操要点
2.1 先把 ADK 环境装对
ADK 这个名字在 Android 开发里也有一个,指 Android Developer Kit,不少人一开始就装错了包。我们这里说的是 Agent Development Kit,是一个用于构建 agent 的框架,重点解决“模型如何调用工具”“对话状态怎么管理”“多步任务怎么编排”这些问题。安装其实很简单,前提是你已经有一个 Python 3.10 以上的虚拟环境。
创建虚拟环境这一步别偷懒。我见过太多人图省事直接pip install到系统 Python,结果同一个环境里同时存在 Flask、深度学习库和 agent 框架,互相打架。用虚拟环境隔离项目依赖,和其他领域的虚拟环境是一回事:每个厨房有自己的调料架,不串味儿。
python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install adk装完之后,验证一下版本。不同版本的 ADK 在工具定义和模型参数上的写法略有差异,后面配置出错时,第一步一定是确认版本而不是怀疑代码。
adk --version如果你的机器没有远端模型 API 可用,可以先用 Ollama 拉起一个本地模型。这样做的好处是,测试沙盒时不需要额外配置复杂的模型 API 网络连通性。很多 agent 教程默认使用云端模型,但本地模型在沙盒联调时反而更容易控制。我用的是qwen2.5:7b-instruct这个级别的模型,对工具调用的理解已经足够用了。
2.2 Agent 定义的三个核心参数:指令、模型、工具
用 Python 版 ADK 创建 agent 时,最核心的就是Agent对象。它的三个关键参数分别是instruction、model、tools。instruction决定了模型的行动准则,model决定用哪个模型来驱动,tools决定这个 agent 能碰哪些外部能力。下面是一个最小可运行示例:
from adk import Agent, tool import os @tool def list_files(path: str) -> list[str]: """列出指定目录下的所有文件""" return os.listdir(path) agent = Agent( name="sandbox_test_agent", instruction=( "你是一个沙盒测试助手。" "只能用提供的工具完成任务," "不要做出工具调用之外的操作。" ), model="qwen2.5:7b-instruct", tools=[list_files], )这里有一个很多人一开始意识不到的细节:工具描述非常重要。模型不是靠函数名理解工具的,而是靠 docstring 或者描述字段。如果你把工具描述写成“执行操作”,模型很可能不知道什么时候该调用它;如果写成“将两个数字相加,返回整数”,模型就很清楚这是加法器。这个差距在沙盒测试里会直接体现为“agent 不调用工具”或“把工具参数传错”。
另外,instruction里明确加上“不要做出工具调用之外的操作”,能显著减少模型在沙盒里尝试奇怪行为的概率。它不是安全兜底,真正的兜底是沙盒的文件系统和网络策略。
2.3 opensandbox 配置文件里的三个关键点
opensandbox 的行为几乎全部由一份 YAML 配置控制。我习惯把它命名为sandbox.yaml,放在项目根目录下的sandbox/文件夹里。初次配置,建议先把 CPU、内存、超时时间、网络、文件系统改明白。
version: "1" runtime: docker resources: cpu: 2 memory: 2Gi timeout: 60s network: allow: - "api.llm.example.com:443" deny: - "*" filesystem: read_only: - "/" writable: - "/tmp" mount: - source: "./agent_code" target: "/app" read_only: true env: - key: "LLM_API_KEY" value: "from-secret"三个关键点分别说一下:
第一是network.allow。如果你不填这一段,opensandbox 会沿用运行时的默认网络策略,网上不少默认配置是放行所有出站流量,这在 agent 测试里非常危险。一个被 prompt 注入的 agent 可能会朝内网服务发起扫描。所以我建议先写deny: ["*"],再把你确定需要的模型 API 域名加进allow。
第二是filesystem的读写分离。把根目录/设为只读、只开放/tmp写权限,意味着 agent 能创建临时文件,但改不了应用代码,也碰不到宿主机目录。如果你把自己的agent_code挂载进去,记得同样设为read_only: true。agent 需要改文件?那也应该只让它改/tmp,而不是改自己的源码。
第三是timeout。模型推理有时会卡住,工具调用有时会因为网络问题长时间无响应。如果沙盒没有超时机制,agent 就会一直占着 CPU 和内存。设一个 60 秒的全局超时,配合资源限制才能在测试时快速暴露出“挂起”的 agent。
3. 实操过程与核心环节实现
3.1 项目目录与模块拆分
我没有把所有代码都塞进一个main.py,因为 agent 项目一旦超过 50 行,拆分开会更舒服。这次实验用了一个基础结构:
agent-sandbox-demo/ ├── agent/ │ ├── __init__.py │ ├── main.py │ ├── tools.py │ └── config.py ├── sandbox/ │ └── sandbox.yaml ├── tests/ │ └── basic.yaml └── requirements.txttools.py专门放工具函数,config.py放模型名称、环境变量读取这类常量,main.py是入口,负责创建 agent 并且执行一轮对话。这样拆分的好处是:本地单测可以把tools.py单独拉出来跑,沙盒安全测试可以直接运行main.py,不需要担心工具被入口函数副作用影响。
# tools.py import os from adk import tool @tool def list_files(path: str) -> list[str]: """列出指定目录下的所有文件""" return os.listdir(path)# main.py import os from adk import Agent from tools import list_files agent = Agent( name="sandbox_test_agent", instruction=( "你是一个沙盒测试助手。" "只能用提供的工具完成任务,不要做出工具调用之外的操作。" ), model=os.getenv("AGENT_MODEL", "qwen2.5:7b-instruct"), tools=[list_files], ) result = agent.run("请列出 /tmp 目录下的所有文件") print(result)这一版入口文件非常“直给”:加载 agent,run 一个问题,打印结果。沙盒里不需要加载 Flask 服务,也不需要持久化数据库,所以保持入口尽可能简单反而更容易定位问题。
3.2 将 agent 打包进 opensandbox 并运行
有了代码和配置,下一步是把 agent 运行起来。opensandbox 提供了构建和运行两个命令,我一般这样用:
opensandbox build -f sandbox/Dockerfile . opensandbox run --config sandbox/sandbox.yaml -- python -m agent.main如果懒得维护 Dockerfile,也可以直接基于一个包含 Python 和 ADK 依赖的基础镜像运行,但那样镜像体积会偏大。我更倾向于在项目根目录放一个精简 Dockerfile:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY agent/ ./agent/ CMD [ "python", "-m", "agent.main" ]运行之后,你需要观察几件事:沙盒是否正常启动、agent 是否能拿到模型响应、工具调用是否被允许。一个典型输出是这样的:
[agent] model response: 我需要调用 list_files 工具 [agent] tool.list_files called with args: {"path": "/tmp"} [agent] tool.list_files result: ["data.csv", "notes.txt"] [agent] final response: /tmp 目录下有 2 个文件看到这个链路完整的输出,就可以确定 ADK 与 opensandbox 的基本集成已经跑通了。如果 model response 之后没有 tool call,说明模型可能没理解工具;如果工具调用被拒,说明文件系统或权限策略太严格。
3.3 测试结果分析与审计
正常跑通只是第一步,我还会设计几个故意触发边界情况的测试用例。比如让 agent 去列/etc,或者让它尝试写入一个只读目录。下表是我在测试时最常用的几个场景:
| 测试场景 | 预期行为 | 沙盒策略 |
|---|---|---|
列出/tmp文件 | 成功返回文件列表 | /tmp可写可读 |
读取/etc/passwd | 返回权限错误 | /只读 |
| 发起外网请求 | 请求被拒绝或超时 | 网络白名单不放行 |
| 持续死循环 | 60 秒后被强制终止 | timeout: 60s |
这些场景跑完后,直接看 opensandbox 的审计日志。日志里记录了每次工具调用的入参、出参、执行耗时、是否命中网络策略。这个信息量比只看 agent 最终输出大得多。我遇到过一种情况:agent 最终回复说“任务完成”,但其实工具调用早就失败了,只是模型没有如实报告。如果没有审计日志,这种问题很难发现。
4. 常见问题与排查技巧实录
4.1 沙盒启动失败:镜像权限和内核兼容问题
opensandbox 启动失败是最常见的坑。第一种是镜像拉不下来,尤其是第一次构建时依赖源访问慢。处理方式是提前配置好可用的镜像源,或者把基础镜像先拉好再构建。第二种是运行用户权限问题,默认容器内是 root,但如果你的镜像显式创建了低权限用户,需要确保它有执行python的权限。第三种是 mount 冲突,宿主机目录和目标路径不一致时会报mount denied,检查 YAML 里的路径是不是真实存在。
4.2 Agent 在沙盒里一直不调用工具
这是我被问过最多次的问题,也是很多 agent 新手卡住的地方。排查顺序我建议固定下来:
先看模型输出。如果模型输出是一段纯文本,没有工具调用的结构化字段,那问题在模型层,可能是instruction没表达清楚,或者模型太小不理解工具调用。再看 agent 定义里tools有没有把工具传进去,@tool装饰器有没有漏。然后打开沙盒opensandbox logs,看模型 API 请求是否成功。如果模型 API 在沙盒里无法访问,模型压根没有返回,也不会触发工具调用。
一个容易被忽略的细节是:某些模型对工具描述中的语言很敏感。我试过把工具描述写中文,模型有时能识别,有时不能;换成一两句英文描述后,工具调用率明显上升。这个不是绝对规律,但在连续几次“不调用工具”时值得试一下。
4.3 安全边界:避开这 5 个最常见的坑
测试过程中我见过不少同学把“沙盒”理解成“万能保证”,实际上错误配置比不配置更危险。下面是我踩过之后整理出的 5 个典型坑:
第一,不要用 privileged 模式启动沙盒。这相当于把一个攻击者放进无门锁的保险库。第二,不要把宿主机根目录挂载进去,即使设为read_only,也可能因为某些运行时特性产生逃逸风险。第三,不要把非常敏感的环境变量直接写进沙盒配置文件,比如模型 API Key。应该用 opensandbox 的 secret 管理机制,或通过环境变量注入。第四,不要忽略输出长度限制,一个 agent 如果被诱导输出超大文本,可能耗尽内存。第五,不要在沙盒里使用 root 用户运行 agent 进程,创建一个普通用户,权限越小越安全。
提示:沙盒承担的是“降低风险”的职责,不是“绝对免疫”的保证。任何 agent 上线前都应该再做一次人肉代码审查,尤其是工具实现本身。
最后分享一个实测很好用的小技巧:在 agent 里写一个debug_env工具,专门返回当前用户 ID、当前目录、关键环境变量和网络连通性。这个工具在本地跑没什么价值,但放进沙盒后能帮你一眼定位权限和环境变量问题。我第一次靠它发现沙盒里的HOME路径和我预期的不一样,导致 agent 读配置文件全部失败。加上这个工具后,所有“环境相关”的疑难杂症都有了最快的探测方式。