《Deep Agents 实战笔记》第 9 篇 | 基于 deepagents (Python) 官方文档 原文:https://docs.langchain.com/oss/python/deepagents/sandboxes 原文:https://docs.langchain.com/oss/python/deepagents/interpreters
上一篇我们给 Agent 装上了记忆和技能,它已经有模有样了。但离"真正能干活"还差最后一块拼图——执行。让 Agent 写代码、跑分析、执行测试,就必须先回答一个尖锐的问题:你敢让一个行为不可预测的程序,在你的机器上执行任意 shell 命令吗?
一、"能跑代码"是能力分水岭,也是风险分水岭
官方沙箱文档的开篇说得很直白:代理会生成代码、操作文件系统并运行 shell 命令。由于你无法预测代理可能做什么,它的环境必须被隔离,以免其访问凭据、文件或网络。
这不是杞人忧天。一个被网页里的恶意提示注入的编码代理,完全可能执行 cat ~/.ssh/id_rsa 或者把你的环境变量发到某个陌生地址。沙箱的作用,就是在代理的执行环境与你的宿主系统之间建立一条边界——代理在里面可以为所欲为,但出不了这个圈子。
哪些场景值得上沙箱?官方列了两类典型:一是编码代理,自主运行的代理需要 shell、git(许多提供商提供原生 git API,例如 Daytona 的 git 操作),甚至用 Docker-in-Docker 跑构建与测试流水线;二是数据分析代理,在隔离环境里加载数据文件、安装 pandas / numpy 这类分析库、跑统计计算,最后生成一份 PowerPoint 演示文稿交回。共同点是"要装依赖、要跑命令、产物留在环境里"——这正是沙箱的定义域。
Deep Agents 里最优雅的一点是:沙箱就是 Backend。还记得第 4 篇讲的 Backend 抽象吗?StateBackend、FilesystemBackend、StoreBackend 都只暴露文件操作,而沙箱 Backend 在标准文件系统工具(ls、read_file、write_file、edit_file、delete、glob、grep)之外,还额外给代理一个 execute 工具,用于在沙箱里运行任意 shell 命令。对 create_deep_agent 来说,切换到沙箱只是换一个 backend 参数,代理侧的使用方式完全不变——这就是第 4 篇说的"同一个 Agent API,随便换后端"的威力。
二、一个 execute() 撬动整个文件系统
沙箱 Backend 的架构值得单独讲一下,因为它决定了"接入一家新的沙箱提供商有多难"。
提供商唯一必须实现的方法是 execute()——运行一条 shell 命令,返回输出。其余所有文件操作(read、write、edit、delete、ls、glob、grep)都由 BaseSandbox 基类构建在 execute() 之上:基类负责把"读取文件"翻译成一个脚本,然后通过 execute() 在沙箱里跑这个脚本。
flowchart TB
PROVIDER["提供商 SDK"]
EXEC_METHOD["execute 方法<br/>唯一必须实现的方法"]
BASE["BaseSandbox 基类<br/>用 execute 构建其余文件操作"]
TOOLS["Agent 文件工具<br/>ls / read_file / write_file / glob / grep"]
EXEC_TOOL["execute 工具<br/>运行任意 shell 命令"]
PROVIDER --> EXEC_METHOD
EXEC_METHOD --> BASE
BASE --> TOOLS
EXEC_METHOD --> EXEC_TOOL
style EXEC_METHOD fill:#fdeef0,color:#701d2a,stroke:#c94a5e
style PROVIDER fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
正在渲染图表…
这个设计带来两个结果:
- 接新提供商极简:实现一个
execute(),其余全部白嫖基类。所以 Deep Agents 的沙箱生态才能铺得这么快。 execute工具有条件出现:运行框架每次模型调用前会检查 backend 是否实现了沙箱协议,没实现就把execute从工具列表里过滤掉——代理永远看不到它。你不需要手动开关。
代理调用 execute 时传入一个 command 字符串,拿回合并的 stdout/stderr、退出码。这里有个贴心的细节:如果命令输出过大,结果会自动保存成文件,并提示代理用 read_file 分块读取——防止上下文窗口被一次 cat 大文件 打爆。官方连这个坑都替你踩掉了。
三、沙箱全家桶:六家提供商怎么选
目前官方集成的提供商:LangSmith、Daytona、E2B、Modal、Runloop、Vercel(另有 AWS AgentCore、NVIDIA OpenShell 等扩展)。它们的接入方式高度一致,以 LangSmith 为例:
from deepagents import create_deep_agent
from deepagents.backends import LangSmithSandbox
from langchain_anthropic import ChatAnthropic
from langsmith.sandbox import SandboxClient
client = SandboxClient()
ls_sandbox = client.create_sandbox()
backend = LangSmithSandbox(sandbox=ls_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:
client.delete_sandbox(ls_sandbox.name)
换一家提供商,本质上只是换前两行:E2B 用 Sandbox.create() + E2BSandbox,Daytona 用 Daytona().create() + DaytonaSandbox,Modal 用 modal.Sandbox.create(app=app) + ModalSandbox,Runloop 用 RunloopSDK 创建 devbox + RunloopSandbox,Vercel 用 Sandbox.create(runtime="python3.13") + VercelSandbox。清理动作各不相同(delete_sandbox / kill / stop / terminate / shutdown),都放在 finally 里。
选型上我的看法:
- LangSmith:第一方托管沙箱,不用注册第三方账号,和 LangSmith 追踪天然打通(trace 里直接能看到沙箱里跑了哪些命令),是这个系列默认推荐——尤其你本来就要开 LangSmith 调试。
- E2B / Daytona:老牌代码沙箱服务商,生态成熟。Daytona 还提供原生 git 操作 API,编码代理克隆仓库很方便。
- Modal:适合已经很重地用 Modal 跑计算任务的团队,沙箱和你的 GPU 作业在同一个账户体系里;它还支持
blockNetwork: true阻断网络访问(后面讲安全时会用到)。 - Runloop:devbox 概念,面向编码代理场景。
- Vercel:如果您的应用本身跑在 Vercel 生态里,顺手。
具体到注册、认证和生命周期细节,各家有差异,动手前查一下官方的沙箱集成页即可。
四、生命周期:沙箱是持续烧钱的
这是文档里用加粗语气强调的事:沙箱在被关闭之前会持续消耗资源并产生费用。 所以生命周期管理不是可选项,是必修课。
主流做法是线程作用域(默认):每个对话一个沙箱,第一次运行创建,同一线程后续轮次复用,线程结束或 TTL 过期后销毁。实现方式是在 graph 工厂里按 thread_id 解析沙箱:
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
]
if existing:
ls_sandbox = existing[0]
else:
ls_sandbox = client.create_sandbox(
name=sandbox_name,
idle_ttl_seconds=3600, # TTL: clean up when idle
)
return create_deep_agent(
model="google_genai:gemini-3.6-flash",
backend=LangSmithSandbox(sandbox=ls_sandbox),
)
另一个选择是助手作用域:同一助手的所有线程共享一个沙箱,文件、已安装的软件包、克隆过的仓库跨对话持久。听着很美好,但官方给了明确警告:助手作用域的沙箱会随时间累积状态,请配置 TTL、用快照定期重置,或实现清理逻辑,以免磁盘和内存无限制增长。
我的建议:默认线程作用域 + TTL。用户可能闲置几小时后回来,TTL 让提供商自动回收环境,下次访问再冷启动——比你手动对账沙箱账单体面得多。
五、文件的两个通道:别混用
文件进出沙箱有两条完全不同的路,理解它们的分工很重要:
- 代理文件系统工具(
read_file、write_file、execute等):LLM 在执行过程中调用的,在沙箱内部通过execute()实现,是任务执行的一部分。 - 文件传输 API(
upload_files/download_files):你的应用代码调用的,走提供商的原生文件传输接口,专用于在宿主与沙箱边界上移动文件。
flowchart LR
subgraph APP["你的应用"]
CODE["应用代码"]
end
subgraph AG["Agent"]
LLM["LLM"] --> TOOLS["read_file / write_file / execute"]
TOOLS --> LLM
end
subgraph SB["沙箱"]
FS["沙箱文件系统"]
end
CODE -->|"文件传输 API<br/>upload_files / download_files"| FS
TOOLS -->|"execute"| FS
style CODE fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
style FS fill:#f4effb,color:#4a2170,stroke:#8b52c9
正在渲染图表…
典型流程是三段式:预置(代理运行前放入源码、配置、数据)→ 代理干活 → 取回(拿走生成的报告、构建产物):
backend.upload_files(
[
("/src/index.py", b"print('Hello')\n"),
("/pyproject.toml", b"[project]\nname = 'my-app'\n"),
]
)
# ... 代理运行 ...
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}")
记住这条纪律:沙箱内部,代理用自己的文件工具,不走 upload_files。那些方法是给你——应用开发者——在边界上搬文件用的。
六、两种集成模式:沙箱中的代理 vs 沙箱即工具
官方总结了两种把代理和沙箱组合的架构,本文示例全部是第二种:
flowchart TB
subgraph MODE2["沙箱即工具(推荐起点)"]
A2["代理运行在宿主 / 服务器"]
S2["远程沙箱"]
A2 -->|"execute / read_file<br/>走提供商 API"| S2
K2["API 密钥留在沙箱外"]
end
subgraph MODE1["沙箱中的代理"]
IMG["Docker / VM 镜像<br/>预装 deepagents"]
S1["沙箱内运行代理"]
IMG --> S1
U1["应用"] -. "WebSocket / HTTP 通信" .-> S1
end
style MODE2 fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
style MODE1 fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
正在渲染图表…
沙箱中的代理:把代理本身打进镜像、跑在沙箱里,从外部通过网络与它通信。优点是与本地开发高度一致、代理与环境紧密耦合;代价是 API 密钥必须放进沙箱(安全风险)、更新要重新构建镜像、还得自己搭通信层。
沙箱即工具:代理跑在你的机器上,需要执行代码时通过工具调用远程沙箱的 API。优点很实在:改代理逻辑不用重建镜像、密钥留在沙箱外、沙箱挂了不丢代理状态、还能在多个沙箱里并行跑任务,并且只为执行时间付费。唯一代价是每次执行有网络延迟。
我的判断:默认选沙箱即工具。除非你的提供商 SDK 把通信层处理得很好、且你刻意追求"环境即镜像"的强一致性,否则沙箱中的代理模式的那三个代价(密钥、重建镜像、通信基建)每一个都够你喝一壶。
七、解释器:另一种"跑代码"
讲完沙箱,必须澄清一个容易混淆的概念。Deep Agents 有两种"代码执行",定位完全不同:
- 沙箱:以代码优先的方式操作一个环境——跑 shell 命令、装依赖、跑测试、编辑文件系统。
- 解释器:以代码优先的方式组合工具、保留状态——在代理循环内部提供一个内存中的可编程工作区,代理写代码完成任务,运行时只把相关结果返回给模型,中间结果不进上下文。
解释器目前处于 beta 阶段(API 可能随版本变化),依赖 langchain-quickjs>=0.3.0 和 Python 3.11+。用法是挂一个中间件:
from deepagents import create_deep_agent
from langchain_quickjs import CodeInterpreterMiddleware
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
middleware=[CodeInterpreterMiddleware()],
)
挂上之后代理多了一个 eval 工具。当任务适合用代码表达时,代理会编写 JavaScript 并调用 eval,代码跑在 QuickJS(编译为 WebAssembly)的内存沙箱里,console.log 输出和最后一个表达式的值会返回给模型。
为什么需要它?大多数代理在"模型推理"和"工具调用"之间交替,模型一轮可以发出多个工具调用,但这批调用发出的那一刻就固定了——没法循环、没法根据结果分支、没法重试、没法把上一个调用的输出喂给下一个,除非再过一轮模型调用,而且每个结果都会灌进上下文。解释器把这种编排移进代码:模型只推理"做什么",中间的每一步操作交给代码。
它的默认权限边界极窄:无文件系统、无网络、无 shell、无真实时钟,只有计算、状态和 console 输出。要扩大能力范围只有两座显式的桥:
- PTC(程序化工具调用):把允许列表中的工具暴露为解释器里的异步函数。默认关闭,需显式开启:
agent = create_deep_agent(
model="google_genai:gemini-3.6-flash",
middleware=[CodeInterpreterMiddleware(ptc=["web_search"])],
)
开启后,代理就能在解释器代码里批量编排工具(工具名转 camelCase,web_search 变成 tools.webSearch):
const topics = ["retrieval", "memory", "evaluation"];
const results = await Promise.all(
topics.map((topic) =>
tools.webSearch({ query: `${topic} best practices 2025` }),
),
);
results.join("\n\n");
三个主题并行搜索、合并结果,模型只看到最终合并文本——这就是 PTC 省 token 的方式。
- 动态子代理:代理配置了子代理时,解释器暴露
task()全局函数,可以从代码里扇出子代理(这部分第 6 篇已经详细讲过)。
解释器状态默认跨轮次持久化(mode="thread",每轮结束做快照、下轮恢复),也可设为 "turn"(单轮内)或 "call"(每次 eval 全新 REPL)。资源上默认 64 MB 堆内存、单次 eval 5 秒超时、结果截断 4000 字符、每次 eval 最多 256 次 PTC 调用。
把两者放进一张表,边界就清楚了:
| 维度 | 沙箱 | 解释器 |
|---|---|---|
| 作用对象 | 一个环境(文件系统 + shell + 依赖) | 代理循环内部的内存工作区 |
| 代码形式 | 任意 shell 命令(execute 工具) | QuickJS 里的 JavaScript(eval 工具) |
| 能力边界 | 接近完整操作系统 | 默认无文件、无网络、无 shell、无时钟 |
| 中间结果 | 留在环境里 | 不进模型上下文,只回传过滤后的结果 |
| 状态持久 | 沙箱实例(按 TTL / 作用域) | 快照存进 graph 状态(thread / turn / call) |
| 典型场景 | 编码代理、数据分析、跑测试 | 批量编排工具、确定性数据变换、扇出子代理 |
还有一个容易忽略的细节:解释器的快照只保留可序列化的数据——函数、类这类运行时对象恢复后不可访问,访问会直接报错。而且快照恢复的是解释器内存,不是对外部世界的影响:如果代码通过 PTC 调过工具,恢复快照不会撤销那次调用的副作用。所以"回滚"这个概念在解释器里只对变量成立,对真实世界不成立。
八、选型决策树
最后把本篇收进一张图。面对"让 Agent 跑代码"的需求,按这个顺序问自己:
flowchart TD
START["Agent 需要执行代码"] --> Q1{"只是简单的一两次<br/>外部调用?"}
Q1 -->|是| TOOL["普通工具调用"]
Q1 -->|否| Q2{"纯内存中的数据变换<br/>循环 / 分支 / 重试 / 聚合?"}
Q2 -->|是| EVAL["解释器 eval"]
Q2 -->|否| Q3{"要从代码编排大量工具调用<br/>或扇出大量子代理?"}
Q3 -->|是| PTC["解释器 PTC / 动态子代理"]
Q3 -->|否| Q4{"需要 shell、装依赖、<br/>跑测试、完整文件系统?"}
Q4 -->|是| SANDBOX["沙箱 Backend"]
Q4 -->|否| TOOL2["回到普通工具调用"]
style SANDBOX fill:#fdeef0,color:#701d2a,stroke:#c94a5e
style EVAL fill:#f4effb,color:#4a2170,stroke:#8b52c9
style PTC fill:#f4effb,color:#4a2170,stroke:#8b52c9
style TOOL fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
style TOOL2 fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
正在渲染图表…
一句话版本:要环境用沙箱,要编排用解释器,两者都要就组合使用。 解释器官方文档的安全章节也提醒:把它当作作用域受限的解释器运行时,而不是完整的生产级沙箱 Backend——需要操作系统级隔离时,老老实实上沙箱。
还有一点要提前埋个伏笔:PTC 调用目前通过解释器桥接执行,不经过正常的工具调用路径,因此 interrupt_on 审批工作流不会对 PTC 调用的工具强制生效。也就是说,你给 web_search 配了人工审批,但如果模型在解释器代码里调它,审批是拦不住的。这个坑的完整展开,正是下一篇的主题。
九、本篇踩坑点
- 忘写
finally清理:沙箱按时间计费,demo 跑完不删,月底账单教你做人。所有示例的finally不是装饰。 - 助手作用域沙箱当传家宝:文件、依赖、克隆的仓库跨对话累积,不配 TTL 或快照重置,磁盘和内存迟早爆炸。
- 把
upload_files当成代理的写文件方式:代理在沙箱里只用自己的文件工具;upload_files/download_files是应用代码在宿主与沙箱之间搬运文件用的,两条通道别混。 - 高估了沙箱的防护范围:沙箱防的是代理碰你的宿主系统,防不了上下文注入(攻击者可以指示代理在沙箱内跑任意命令)和网络渗漏(除非像 Modal 那样
blockNetwork: true)。密钥仍然要最小化投放。 - 解释器当沙箱用:解释器默认无网络无文件系统,是编排工具用的;要 shell、要装依赖、要跑测试,用沙箱 Backend。别拿
eval硬凑。 - PTC 绕过审批:给工具配了
interrupt_on但开了 PTC?解释器里的调用不走审批路径。敏感工具别放进 PTC 允许列表。
能力越大越要装笼子。下一篇我们正式装笼子:声明式文件权限规则怎么写、组合 Backend 下的权限怎么落地,以及 HITL 审批的 interrupt-恢复全流程。
参考资料:
- Sandboxes(官方文档,本文主要素材):https://docs.langchain.com/oss/python/deepagents/sandboxes
- Interpreters(官方文档,本文主要素材):https://docs.langchain.com/oss/python/deepagents/interpreters
- LangSmith 沙箱(托管沙箱详情):https://docs.langchain.com/langsmith/sandboxes
- 沙箱集成页(各提供商设置与认证):https://docs.langchain.com/oss/integrations/sandboxes