02 Sandbox 生命周期
Sandbox 是 Deep Agents 的执行环境后端:它让 Agent 不只读写文件,还能通过 execute 跑命令。生命周期问题解决的是“这个环境什么时候创建、什么时候复用、什么时候销毁”。生产里最常见的是 threadscoped:每个 conversation 一个 sandbox,同一个 threadi
Sandbox 是 Deep Agents 的执行环境后端:它让 Agent 不只读写文件,还能通过 execute 跑命令。生命周期问题解决的是“这个环境什么时候创建、什么时候复用、什么时候销毁”。生产里最常见的是 thread-scoped:每个 conversation 一个 sandbox,同一个 thread_id 后续 run 复用它,不同 thread 彼此隔离。
最小代码
本章代码在:
deepagent_src/advanced_teach/02_sandbox_lifecycle.py
核心逻辑只有一个 registry:
@dataclass
class ThreadSandboxRegistry:
root_dir: Path
sandboxes: dict[str, LocalShellBackend] = field(default_factory=dict)
def get_or_create(self, thread_id: str) -> LocalShellBackend:
if thread_id not in self.sandboxes:
workspace = self.root_dir / f"thread-{thread_id}"
workspace.mkdir(parents=True, exist_ok=True)
self.sandboxes[thread_id] = LocalShellBackend(root_dir=workspace)
return self.sandboxes[thread_id]
同一个 thread_id 会拿回同一个 backend;不同 thread_id 会创建不同 backend 和不同工作目录。官方生产文档里的 SandboxClient.list_sandboxes() / create_sandbox(name=..., idle_ttl_seconds=...) 做的就是同一件事,只是它创建的是远端隔离容器。
运行命令
uv run python -m deepagent_src.advanced_teach.02_sandbox_lifecycle
预期现象:
- 输出两个不同 backend id,例如
local-xxxx。 alpha两次获取的是同一个 backend,写入/note.txt后后续还能读到。beta是另一个 backend,/note.txt内容和alpha不一样。execute("pwd")的输出分别落在thread-alpha和thread-beta临时目录。
生产写法
官方最新文档推荐生产中用 graph factory 读取 config["configurable"]["thread_id"],再按名字查找或创建 sandbox:
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langchain_core.runnables import RunnableConfig
from langsmith.sandbox import SandboxClient
client = SandboxClient()
async def agent(config: RunnableConfig):
thread_id = config["configurable"]["thread_id"]
sandbox_name = f"thread-{thread_id}"
existing = [sb for sb in client.list_sandboxes() if getattr(sb, "name", None) == sandbox_name]
ls_sandbox = existing[0] if existing else client.create_sandbox(
name=sandbox_name,
idle_ttl_seconds=3600,
)
return create_deep_agent(
model="openai:gpt-5.5",
backend=LangSmithSandbox(sandbox=ls_sandbox),
)
这里不能用静态 graph,因为 sandbox 选择依赖每次 run 的 thread_id。所以生产里导出的 agent 是 async factory,LangGraph server 每次 run 调它,拿到本次 thread 对应的 backend 后再返回 agent graph。
常见误区
别把 LocalShellBackend 当成安全 sandbox。官方文档明确警告:FilesystemBackend 和 LocalShellBackend 会直接访问 host,不要在部署环境里用。它适合本地教学和开发验证;真正面向用户的 coding agent 要用 LangSmith Sandbox、Daytona、E2B、Modal 这类隔离 provider。
另一个坑是忘记 TTL。sandbox 会占资源和成本,不设置 idle_ttl_seconds 或清理策略,assistant-scoped sandbox 会越堆越脏,磁盘和依赖状态都会膨胀。
验证
本章不触发 LLM 调用。验证重点是 sandbox backend 的真实行为:文件写入、文件读取、命令执行、同 thread 复用、不同 thread 隔离。
相关资源
查看示例代码:deepagent_src/advanced_teach/02_sandbox_lifecycle.py
from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path from tempfile import TemporaryDirectory from deepagents.backends import LocalShellBackend @dataclass class ThreadSandboxRegistry: root_dir: Path sandboxes: dict[str, LocalShellBackend] = field(default_factory=dict) def get_or_create(self, thread_id: str) -> LocalShellBackend: if not thread_id: raise ValueError("thread_id is required") if thread_id not in self.sandboxes: workspace = self.root_dir / f"thread-{thread_id}" workspace.mkdir(parents=True, exist_ok=True) self.sandboxes[thread_id] = LocalShellBackend( root_dir=workspace, virtual_mode=True, env={"PATH": "/usr/bin:/bin"}, timeout=5, ) return self.sandboxes[thread_id] def read_text(backend: LocalShellBackend, path: str) -> str: result = backend.read(path) if result.error: raise AssertionError(result.error) return result.file_data["content"] def main() -> None: with TemporaryDirectory(prefix="deepagents-sandbox-lifecycle-") as tmp: registry = ThreadSandboxRegistry(Path(tmp)) alpha_first = registry.get_or_create("alpha") alpha_second = registry.get_or_create("alpha") beta = registry.get_or_create("beta") assert alpha_first is alpha_second assert alpha_first is not beta assert alpha_first.id == alpha_second.id assert alpha_first.id != beta.id alpha_first.write("/note.txt", "created by alpha") beta.write("/note.txt", "created by beta") alpha_pwd = alpha_second.execute("pwd") beta_pwd = beta.execute("pwd") assert alpha_pwd.exit_code == 0, alpha_pwd.output assert beta_pwd.exit_code == 0, beta_pwd.output assert "thread-alpha" in alpha_pwd.output assert "thread-beta" in beta_pwd.output assert read_text(alpha_second, "/note.txt") == "created by alpha" assert read_text(beta, "/note.txt") == "created by beta" print(f"alpha backend id: {alpha_first.id}") print(f"beta backend id: {beta.id}") print("thread-scoped sandbox lifecycle ok") if __name__ == "__main__": main()