Typography

学习笔记

Deep Agents 学习05 Sandboxes

发布于 # Deep Agents

一旦 Agent 真正开始改文件、装依赖、跑命令,安全边界就不再是“以后再补”的附加能力,而会直接进入系统设计核心。沙箱的价值也不只是把执行能力做强,而是把这些能力装进一个隔离、可回收、可治理的环境里。理解沙箱,本质上也是在理解 Deep Agents 怎样从实验演示走向工程落地。

为什么值得关注

只要任务开始带来真实副作用,沙箱就会变成一条非常关键的基础设施线。它一方面让 Agent 继续拥有读写文件和执行命令的能力,另一方面又尽量避免这些动作直接落到宿主机上。也就是说,你不是在“给 Agent 更多权限”,而是在“给执行能力加边界”。

接入沙箱之后,Agent 往往就能安全地承担这些动作:

沙箱的核心价值,不是“让 Agent 更强”,而是“让 Agent 在具备执行能力时不直接碰你的宿主机”。


沙箱是什么

在 Deep Agents 里,沙箱本质上是一类 backend。

和普通 backend 的区别是:

因此,Agent 一旦接入沙箱,就会同时拥有:

其中 execute 是关键差异,因为它允许 Agent 在隔离环境里直接运行命令。

沙箱在整体结构中的位置

先看这张图,会更容易理解为什么沙箱不是一个零散外挂,而是 Deep Agents 执行体系里的一个正式 backend。

graph LR
    subgraph Agent
        LLM --> Tools
        Tools --> LLM
    end

    Agent <-- backend protocol --> Sandbox

    subgraph Sandbox
        Filesystem
        Bash
        Dependencies
    end

这张图说明了沙箱不是“附加插件”,而是 Agent 的一种 backend。 Agent 仍在上层做规划和工具选择,真正的文件与命令执行落在隔离环境里。


为什么需要沙箱

只要 Agent 能写代码、改文件、跑命令,就必须认真考虑执行环境边界。

如果没有隔离层,Agent 的这些动作就会直接发生在你的宿主机上,风险包括:

沙箱的作用就是在 Agent 和宿主机之间建立边界。

这个边界的意义是:


适合使用沙箱的场景

什么时候应该认真考虑沙箱,最简单的判断标准其实不是“我想不想用”,而是“这个 Agent 会不会真的动手执行”。只要答案是会,沙箱通常就值得优先进入方案评估。

编码型 Agent

如果你的 Agent 更像一个真正会动工程环境的编码助手,那么沙箱通常会特别合适。例如:

数据分析型 Agent

同样的逻辑也适用于数据分析场景。只要任务会落文件、装依赖、跑计算,沙箱就能把副作用控制在隔离环境里。例如:

一条经验规则

只要你的 Agent 需要“真实执行”而不是“只做文本回答”,就应该优先考虑沙箱。


基本使用方式

接入沙箱通常分为 4 步:

  1. 使用对应 provider 的 SDK 创建 sandbox / devbox
  2. 把 provider 对象封装成 Deep Agents backend
  3. 把这个 backend 传给 create_deep_agent(...)
  4. 任务完成后显式销毁或关闭沙箱
import modal
from deepagents import create_deep_agent
from langchain_anthropic import ChatAnthropic
from langchain_modal import ModalSandbox

app = modal.App.lookup("your-app")
modal_sandbox = modal.Sandbox.create(app=app)
backend = ModalSandbox(sandbox=modal_sandbox)

agent = create_deep_agent(
    model=ChatAnthropic(model="claude-sonnet-4-6"),
    system_prompt="You are a Python coding assistant with sandbox access.",
    backend=backend,
)

try:
    result = agent.invoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": "Create a small Python package and run pytest",
                }
            ]
        }
    )
finally:
    modal_sandbox.terminate()

这段流程的关键点

这个模式在其他 provider 上也是一样的,只是创建与销毁沙箱的 SDK 不同。


常见 provider

可接入的沙箱 provider 包括:

选择时优先考虑:


生命周期设计

真正把沙箱接进系统之后,你很快会发现,难点并不只是“怎么创建一个环境”,而是“这个环境应该活多久、绑定谁、什么时候销毁”。沙箱会持续消耗资源和费用,因此生命周期设计几乎总会成为落地时必须正面回答的问题。

这个问题本质上是在决定:

常见有两种生命周期设计。


线程级生命周期

线程级生命周期适合“每个对话一个沙箱”。

特点:

适合什么场景

示例理解

一个数据分析机器人中:

这是最推荐的默认模式。


Assistant 级生命周期

Assistant 级生命周期适合“同一个 assistant 下所有线程共享一个沙箱”。

特点:

适合什么场景

风险

长期共享环境会积累:

如果不清理,磁盘和内存会不断膨胀。

推荐措施


基本生命周期操作

不管是哪种 provider,核心生命周期动作都差不多:

  1. 创建沙箱
  2. 执行任务
  3. 关闭或删除沙箱

Daytona 示例

from daytona import Daytona
from langchain_daytona import DaytonaSandbox

sandbox = Daytona().create()
backend = DaytonaSandbox(sandbox=sandbox)

result = backend.execute("echo hello")
# ... use sandbox
sandbox.stop()

Runloop 示例

from runloop_api_client import RunloopSDK
from langchain_runloop import RunloopSandbox

client = RunloopSDK(bearer_token="...")
devbox = client.devbox.create()
backend = RunloopSandbox(devbox=devbox)

result = backend.execute("echo hello")
# ... use sandbox
devbox.shutdown()

一个重要习惯

所有沙箱对象都应该有明确的销毁路径。
不要依赖“进程退出后自然释放”。


对话型应用中的 get-or-create 模式

在聊天应用里,通常会把一个会话映射成一个 thread_id

最佳实践通常是:

Daytona 示例

from langchain_core.utils.uuid import uuid7

from daytona import CreateSandboxFromSnapshotParams, Daytona
from deepagents import create_deep_agent
from langchain_daytona import DaytonaSandbox

client = Daytona()
thread_id = str(uuid7())

try:
    sandbox = client.find_one(labels={"thread_id": thread_id})
except Exception:
    params = CreateSandboxFromSnapshotParams(
        labels={"thread_id": thread_id},
        auto_delete_interval=3600,
    )
    sandbox = client.create(params)

backend = DaytonaSandbox(sandbox=sandbox)
agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=backend,
    system_prompt="You are a coding assistant with sandbox access. You can create and run code in the sandbox.",
)

try:
    result = agent.invoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": "Create a hello world Python script and run it",
                }
            ]
        },
        config={
            "configurable": {
                "thread_id": thread_id,
            }
        },
    )
    print(result["messages"][-1].content)
except Exception:
    client.delete(sandbox)
    raise

这里的核心思路

为什么 TTL 很重要

聊天应用里,用户可能:

如果没有 TTL:


两种集成架构模式

Agent 和沙箱结合起来之后,接下来要做的其实是一个很典型的架构选择:究竟是把 Agent 整体搬进沙箱里运行,还是把沙箱当成一个被调用的执行后端。两种方式都能工作,但适合的工程边界并不一样。


模式一:Agent 运行在沙箱里

这种模式更像是在沙箱里“托管整个 Agent 运行时”。也就是说,沙箱不只是提供执行能力,而是直接承载 Agent 本身。

这种模式下:

工作方式

通常做法是:

好处

代价

示例镜像

FROM python:3.11
RUN pip install deepagents-cli

什么时候适合这种模式

适合:


模式二:沙箱作为工具

另一种思路则更贴近大多数应用的现实做法:Agent 仍然跑在你自己的服务端或本地环境里,只有在需要执行文件操作和命令时,才把动作交给沙箱 backend。

这种模式下:

好处

代价

示例

from daytona import Daytona
from deepagents import create_deep_agent
from dotenv import load_dotenv
from langchain_daytona import DaytonaSandbox

load_dotenv()

sandbox = Daytona().create()
backend = DaytonaSandbox(sandbox=sandbox)

agent = create_deep_agent(
    model="google_genai:gemini-3.1-pro-preview",
    backend=backend,
    system_prompt="You are a coding assistant with sandbox access. You can create and run code in the sandbox.",
)

try:
    result = agent.invoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": "Create a hello world Python script and run it",
                }
            ]
        }
    )
    print(result["messages"][-1].content)
except Exception:
    sandbox.stop()
    raise

什么时候适合这种模式

适合:

实操中的默认建议

大多数场景优先使用“沙箱作为工具”模式。


沙箱是如何工作的

核心机制:execute()

沙箱 backend 的核心实现点非常集中:

其他文件能力,例如:

实际上都可以由基础类通过拼接脚本并调用 execute() 来实现。

这意味着什么

一个重要细节

只有当 backend 实现了 SandboxBackendProtocol,Agent 才会看到 execute 工具。

如果 backend 只是普通文件系统 backend:

沙箱作为 backend 的执行模型

如果前面那种“provider 只要实现 execute() 就能衍生出整套文件能力”的说法还比较抽象,可以先看下面这张图,再去理解它们之间的调用关系。

graph TB
    subgraph AgentTools[Agent tools]
        Tools[ls, read_file, ...]
        ExecuteTool[execute]
    end

    BaseSandbox[BaseSandbox\n(uses execute)] --> Tools
    ExecuteMethod[execute()] --> BaseSandbox
    ExecuteMethod --> ExecuteTool
    Provider[Provider SDK] --> ExecuteMethod

这个关系很关键,因为它解释了为什么很多 sandbox provider 只需要把 execute() 做好,其他文件工具能力就能被统一派生出来。


execute() 返回什么

当 Agent 调用 execute 时,一般会传入:

然后返回:

成功示例

4
[Command succeeded with exit code 0]

失败示例

bash: foobar: command not found
[Command failed with exit code 127]

输出过大的处理方式

如果命令输出过大,系统通常不会把全部结果直接塞回上下文,而是:

这样做是为了避免上下文窗口被一次性撑爆。


两种文件访问平面

理解沙箱时,最容易混淆的一点,是“文件到底是怎么进出沙箱的”。很多实现上的误会,都是从把这两条路径混在一起开始的。所以这里最好先停一下,把 Agent 任务内的文件访问,和应用侧跨边界的文件传输明确拆开。

实际上有两套完全不同的路径。

第一种:Agent 内部文件系统工具

这是 Agent 在执行任务时使用的工具:

这些工具:

适合:

第二种:应用侧文件传输 API

这是你的应用代码使用的接口:

这些接口:

适合:

一句区分方法

两种文件访问平面示意

先看这张图,再去读下面的说明,会更容易把“Agent 内部工具调用”和“应用侧上传下载”这两条线区分开。

graph LR
    subgraph AppSide[Your application]
        App[Application code]
    end

    subgraph AgentSide[Agent]
        LLM --> Tools[read_file, write_file, ...]
        Tools --> LLM
    end

    subgraph SandboxSide[Sandbox]
        FS[Filesystem]
    end

    App -- Provider API --> FS
    Tools -- execute() --> FS

这张图对应两个完全不同的入口:


如何预置文件到沙箱

在 Agent 开始工作前,通常需要把代码、配置或数据先放进沙箱。

这时使用 upload_files()

Daytona 示例

from daytona import Daytona
from langchain_daytona import DaytonaSandbox

sandbox = Daytona().create()
backend = DaytonaSandbox(sandbox=sandbox)

backend.upload_files(
    [
        ("/src/index.py", b"print('Hello')\n"),
        ("/pyproject.toml", b"[project]\nname = 'my-app'\n"),
    ]
)

使用规则

适合预置的内容


如何取回沙箱产物

任务结束后,如果需要把生成结果从沙箱拿回宿主环境,就使用 download_files()

Daytona 示例

from daytona import Daytona
from langchain_daytona import DaytonaSandbox

sandbox = Daytona().create()
backend = DaytonaSandbox(sandbox=sandbox)

results = backend.download_files(["/src/index.py", "/output.txt"])
for result in results:
    if result.content is not None:
        print(f"{result.path}: {result.content.decode()}")
    else:
        print(f"Failed to download {result.path}: {result.error}")

适合取回的内容


安全边界到底保护了什么

说到这里,很容易自然得出一个过度乐观的结论:既然已经有沙箱,是不是很多安全问题就自动解决了?实际上,沙箱保护的是一部分边界,而且是非常重要的一部分,但它并不是万能安全层。

沙箱能保护的是:

也就是说,Agent 在沙箱里乱跑命令,主要影响的是沙箱内部,不会直接触达你本地机器。

但是要注意,沙箱并不是万能安全方案。

它不能自动防住两类重要风险。


风险一:上下文注入

如果攻击者控制了 Agent 的部分输入内容,他可能诱导 Agent 在沙箱内执行任意命令。

沙箱虽然把宿主机保护住了,但并没有阻止:

换句话说:


风险二:网络外传

如果沙箱允许联网,那么被注入的 Agent 仍可能:

因此:

如果任务不需要联网,应尽量阻断网络。


关于密钥的最重要规则

不要把 secrets 放进沙箱。

包括但不限于:

原因很直接:

即使这些密钥:

也依然存在被滥用和外传的风险。


更安全的密钥处理方式

方式一:把认证逻辑放在沙箱外部工具里

这是最推荐的方式。

思路是:

例如:

方式二:使用代理层自动注入认证

有些 sandbox provider 可能支持:

这样 Agent 看到的是普通 URL,而不是密钥本身。

这种能力并不是所有 provider 都有,因此可用性取决于具体平台。


如果你非要把 secrets 放进沙箱

这不是推荐做法,但如果业务上暂时绕不开,至少要加这些防护:

即便如此,这仍然不是安全解法,只能算风险缓解。

原因是:


通用安全实践

不管使用哪个 provider,都建议遵循这些规则:


什么时候选沙箱,而不是 LocalShellBackend

很多人真正做选型时,不是在“普通 backend 和沙箱”之间犹豫,而是在“直接本地执行”与“隔离执行”之间犹豫。这个判断标准可以尽量简单一点:只要你开始在意边界、风险和可回收性,沙箱通常就更合适。

一个简单判断标准:

LocalShellBackend

前提是同时满足:

选沙箱

只要满足以下任一条件,就优先选沙箱:


选 provider 时的关注点

不同 sandbox provider 的 SDK 和生命周期控制不同,但选型时关注点大致一致:

如果你做的是对话型应用,优先关注:


常见错误与排查

把沙箱当成“绝对安全”环境

常见误区:

纠正理解:

没有清理沙箱,成本持续增长

常见原因:

解决:

把应用文件传输和 Agent 文件工具混为一谈

常见问题:

正确理解:

共享沙箱后环境越来越脏

常见原因:

解决:

把密钥放进沙箱

这是最高风险误用之一。

解决:

命令输出太大导致结果异常

常见情况:

正确理解:


验收标准

可以用下面的标准判断沙箱接入是否正确:


推荐实操顺序

建议按下面顺序落地:

  1. 先选一个 sandbox provider
  2. 先用最简单示例跑通 execute("echo hello")
  3. 再接入 create_deep_agent(...)
  4. 测试 Agent 是否能创建文件并运行命令
  5. 加上 upload_files() 预置项目文件
  6. 加上 download_files() 取回结果
  7. 再设计 thread-scoped 或 assistant-scoped 生命周期
  8. 最后再补 TTL、网络限制、审批和审计逻辑

关键要点


写在最后

沙箱的本质,是给 Deep Agent 提供一个“可执行但隔离”的工作区。

可以把它理解成三层价值:

当你把这三层设计清楚之后,沙箱才不是一个“能跑命令的玩具环境”,而是可以支撑真实智能体系统落地的执行基础设施。