LangChain DeepAgents 面向的是更复杂的 AI Agent(人工智能代理)场景:代理不只是回答问题,还要能拆任务、写代码、改文件、运行测试,甚至通过 shell 命令完成一组开发工作。
一旦允许代理执行命令,问题就不再只是“模型会不会答错”,而是“它会不会碰到不该碰的东西”。如果代理直接运行在宿主机上,它可能读到环境变量里的密钥,误删工作目录,安装一堆冲突依赖,或者把本地文件通过网络发出去。
Sandboxes 的作用就是把这类执行动作放进一个受控环境中。代理仍然可以运行代码、操作文件系统、安装依赖,但这些动作发生在隔离空间里,而不是直接作用到宿主系统。
Sandbox 解决的核心问题
Sandbox 可以理解为 DeepAgents 的执行后端。它不是一个简单的文件存储层,而是一个带有代码执行能力的运行环境。
在这个环境中,代理可以做这些事情:
- 执行 shell 命令;
- 创建、读取、修改、删除文件;
- 安装依赖;
- 编译、构建、测试和运行代码;
- 在任务结束后把需要的产物导出到宿主侧。
它要解决的不是“让代理更聪明”,而是“让代理安全地动手做事”。
| 问题 | 没有 Sandbox 时的风险 | Sandbox 的处理方式 |
|---|---|---|
| 命令执行安全 | 命令直接作用于宿主机,可能泄露凭证或破坏文件 | 命令在隔离环境中执行,限制对宿主资源的访问 |
| 依赖冲突 | 不同任务安装不同依赖,污染本地环境 | 每个执行环境独立配置依赖 |
| 结果复现 | 本地机器差异导致运行结果不一致 | 通过 provider 提供相对一致的运行环境 |
| 文件边界 | 代理可能读写宿主文件系统 | 沙箱内部文件系统与宿主隔离 |
| 任务清理 | 执行后残留临时文件、依赖和进程 | 沙箱环境可销毁或重建 |
从架构上看,DeepAgents 不直接把 shell 暴露给宿主机,而是通过 Sandbox 后端间接执行。
flowchart LR
A[DeepAgent] --> B[工具层<br/>read_file / write_file / ls / grep / run]
B --> C[Sandbox Backend]
C --> D[execute 命令执行接口]
D --> E[(沙箱文件系统)]
D --> F[沙箱运行时<br/>Shell / Python / Node / 依赖环境]
H[宿主系统] -.隔离边界.-> C
H --> G[uploadFiles / downloadFiles]
G --> C
图中的关键点是:代理的文件操作和命令执行都进入 Sandbox Backend;宿主系统只通过受控接口与沙箱交换文件,不把本地文件系统直接暴露给代理。
Sandbox 不是绝对安全区
隔离环境能降低风险,但不能把所有安全问题清零。Sandbox 的安全边界主要解决的是“代理不要直接碰宿主机”,它并不保证代理生成的所有行为都是可信的。
典型边界包括:
- 沙箱内进程不能直接读取宿主磁盘;
- 沙箱内命令不能直接访问宿主环境变量;
- 沙箱内文件系统与宿主文件系统分离;
- 沙箱访问权限由 provider 和配置决定。
但还有两类问题需要单独处理。
第一类是上下文注入攻击。Context injection(上下文注入)指外部内容诱导代理改变原本的执行意图,例如让代理忽略安全规则、读取敏感文件或执行额外命令。Sandbox 可以限制宿主资源访问,但不能自动判断一段提示词是不是恶意诱导。
第二类是网络外传。如果沙箱允许访问网络,代理仍然可能把沙箱内能读到的内容发送到外部服务。即使沙箱不能读宿主密钥,只要你把密钥放进了沙箱,它就有机会被命令打印、上传或写入日志。
安全模型可以这样理解:
flowchart TB
A[宿主系统<br/>代码仓库 / 环境变量 / 凭证] -->|受控文件传输| B[Sandbox]
B --> C[代理执行命令]
C --> D[沙箱内部文件]
C --> E[安装依赖 / 运行测试]
C --> F{是否允许网络访问}
F -->|允许| G[外部网络]
F -->|禁止| H[无法通过网络外传]
A -.不应直接暴露.-> C
Sandbox 负责把宿主和执行环境隔开,但敏感信息仍然应该留在宿主侧,由受控工具代为处理。
execute() 是 Sandbox 的底层核心
DeepAgents 的 Sandbox 设计围绕一个核心能力展开:execute()。
execute() 的职责很直接:在沙箱内部运行一条 shell 命令,并把结果返回给调用方。它通常需要返回这些信息:
stdout:标准输出;stderr:标准错误;exit code:退出码;- 输出是否因为过长被截断;
- 可能还包括超时、工作目录、环境信息等 provider 相关字段。
可以用一个简化接口表达它的形态:
type ExecuteResult = {
stdout: string
stderr: string
exitCode: number
truncated?: boolean
}
interface SandboxBackend {
execute(command: string): Promise<ExecuteResult>
}
execute() 之所以重要,是因为很多上层工具都可以基于它实现。比如读取文件可以执行 cat,列目录可以执行 ls,搜索内容可以执行 grep,写入文件可以通过 shell 重定向或临时脚本完成。
flowchart TD
A[read_file] --> E[execute]
B[write_file] --> E
C[ls] --> E
D[grep] --> E
F[run_tests] --> E
G[install_dependency] --> E
E --> H[沙箱 Shell]
H --> I[(沙箱文件系统)]
H --> J[语言运行时和依赖]
这种设计有一个好处:DeepAgents 不需要为每种 provider 重新实现完整工具集,只要 provider 能提供一致的 execute() 能力,上层工具就可以复用同一套抽象。
文件访问分成内部操作和外部传输
Sandbox 中的文件处理有两条路径,不能混在一起理解。
一条路径是代理在沙箱内部操作文件。比如代理要查看 package.json、修改某个 Python 文件、搜索关键字、运行测试,这些动作发生在沙箱内部,通常通过 execute() 间接完成。
另一条路径是宿主应用和沙箱之间传输文件。比如任务开始前,把本地项目上传进沙箱;任务完成后,把生成的报告或修改后的代码下载回来。这类动作不一定经过 shell,而是通过 provider 提供的原生传输接口完成,例如 uploadFiles()、downloadFiles()。
| 文件路径 | 发起方 | 典型接口 | 是否经过 shell | 用途 |
|---|---|---|---|---|
| 沙箱内部文件操作 | Agent 工具层 | read_file、write_file、ls、grep | 通常经过 execute() | 代理执行任务时读写沙箱文件 |
| 宿主与沙箱文件传输 | 应用层 | uploadFiles()、downloadFiles() | 通常不经过 shell | 任务开始前导入文件,任务结束后导出产物 |
一次典型执行流程如下:
sequenceDiagram
participant App as 宿主应用
participant Sandbox as Sandbox Backend
participant Agent as DeepAgent
participant Shell as 沙箱 Shell
App->>Sandbox: uploadFiles(项目文件)
App->>Agent: 发起任务
Agent->>Sandbox: 调用 read_file / ls
Sandbox->>Shell: execute("cat ... / ls ...")
Shell-->>Sandbox: stdout / stderr / exit code
Sandbox-->>Agent: 返回工具结果
Agent->>Sandbox: 调用 write_file / run_tests
Sandbox->>Shell: execute("写文件或运行测试命令")
Shell-->>Sandbox: 执行结果
Sandbox-->>Agent: 返回结果
App->>Sandbox: downloadFiles(结果文件)
Sandbox-->>App: 返回产物
这条边界很重要:代理不应该随意读取宿主文件,宿主也不应该把全部本地目录无差别塞进沙箱。传进去什么,代理才能在沙箱内处理什么。
一个简化的使用流程
不同 provider 的软件开发工具包 SDK(Software Development Kit)名称和参数会有差异,但整体使用模式通常类似:创建沙箱、上传输入、让代理执行任务、下载结果、销毁环境。
下面用伪代码表达这个流程,重点看职责划分,而不是具体库名。
// 1. 创建一个 Sandbox 后端
const sandbox = await createSandbox({
image: "python:3.11",
network: false,
timeoutSeconds: 300,
})
// 2. 把需要处理的文件上传进沙箱
await sandbox.uploadFiles([
{
localPath: "./project",
sandboxPath: "/workspace/project",
},
])
// 3. 让 DeepAgent 使用这个 sandbox 作为执行后端
const agent = createDeepAgent({
sandbox,
tools: [
"read_file",
"write_file",
"ls",
"grep",
"run_command",
],
})
// 4. 代理在沙箱中完成任务
const result = await agent.invoke({
task: "修复 /workspace/project 中的测试失败问题,并运行测试",
})
// 5. 下载需要的结果
await sandbox.downloadFiles([
{
sandboxPath: "/workspace/project",
localPath: "./output/project",
},
])
// 6. 清理沙箱
await sandbox.dispose()
network: false 表示禁用网络访问。不是所有任务都能关闭网络,比如需要安装远程依赖时就可能需要访问网络;但对于只处理已上传文件、只运行本地测试的任务,关闭网络能减少数据外传风险。
如果任务需要安装依赖,可以把依赖预装进镜像,或者在受控阶段打开网络完成安装,再进入代理执行阶段关闭网络。具体做法取决于 provider 是否支持分阶段配置。
安全实践:不要把秘密交给沙箱
Sandbox 的使用原则可以归纳为一句话:让沙箱处理可公开的输入和可丢弃的中间结果,不让它持有长期有效的敏感凭证。
1. 不把 API 密钥放进沙箱环境变量
不要把数据库密码、云服务密钥、API(Application Programming Interface,应用程序编程接口)token 放进沙箱环境变量。代理一旦可以执行命令,就可能运行类似下面的操作:
env
cat ~/.config/some-tool/credentials
grep -R "api_key" /workspace
如果这些值存在于沙箱里,隔离宿主系统也无法阻止它们在沙箱内部被读取。
2. 敏感操作放在宿主工具里执行
如果代理确实需要访问某个受保护资源,不要把凭证下发给沙箱。更安全的方式是把敏感逻辑封装成宿主侧工具,让代理只能调用工具,而不能直接拿到凭证。
flowchart LR
A[DeepAgent] --> B[请求调用工具<br/>例如 query_customer_status]
B --> C[宿主工具服务]
C --> D[(安全凭证存储)]
C --> E[外部受保护系统]
E --> C
C --> B
B --> A
A -.不能直接读取.-> D
这种模式下,代理只能表达“我要查询某个状态”,真正的认证、权限校验、请求签名都在宿主侧完成。工具还可以增加参数校验、审计日志和速率限制。
3. 不需要网络时关闭网络
如果任务只是静态分析、代码修改、格式化、运行本地测试,网络访问通常不是必需的。关闭网络后,即使代理生成了外传命令,也无法直接把文件发送到外部地址。
| 任务类型 | 是否建议开启网络 | 说明 |
|---|---|---|
| 读取和修改已上传代码 | 否 | 输入已经在沙箱内,不需要外部访问 |
| 运行本地单元测试 | 否 | 依赖已准备好时可关闭网络 |
| 安装第三方依赖 | 视情况 | 可在初始化阶段开启,执行阶段关闭 |
| 调用外部 API | 谨慎 | 优先使用宿主侧受控工具 |
| 抓取网页或访问远程仓库 | 视情况 | 需要限制域名、权限和输出内容 |
4. 给执行设置超时和输出限制
代理可能生成长时间运行的命令,比如死循环、持续监听服务或巨大日志输出。Sandbox 后端应该对执行过程设置限制:
const result = await sandbox.execute("pytest -q", {
timeoutSeconds: 120,
maxOutputBytes: 1024 * 1024,
})
超时限制避免任务无限占用资源,输出限制避免大日志拖垮调用链。返回结果里保留 truncated 字段,也能让上层知道输出是否被截断。
provider 的角色:提供一致后端,不提供相同底层
Sandboxes 可以由不同 provider 实现。Provider 负责创建隔离环境、运行命令、管理文件传输和释放资源;DeepAgents 只依赖统一接口,不需要关心底层到底是容器、云端 devbox,还是其他执行环境。
常见 provider 的侧重点不同:
| Provider | 主要特点 | 更适合的场景 |
|---|---|---|
| Modal | 云端隔离运行,支持 GPU(Graphics Processing Unit,图形处理器)资源 | 需要云端计算、模型相关任务、较重执行环境 |
| Runloop | 快速创建 devbox 开发环境 | 代码修改、测试、自动化开发任务 |
| Daytona | 多语言环境和快速启动 | 多技术栈项目、需要灵活运行时的任务 |
选择 provider 时,需要重点看这些能力:
- 是否支持网络开关;
- 是否支持自定义镜像或预装依赖;
- 文件上传下载是否方便;
- 命令执行是否有超时和输出限制;
- 是否支持并发任务;
- 沙箱销毁后数据是否彻底清理;
- 日志和审计能力是否足够。
适合使用 Sandbox 的场景
Sandbox 适合那些“代理需要动手执行,但不能直接碰宿主机”的任务。
| 场景 | 是否适合 | 原因 |
|---|---|---|
| 自动修复代码并运行测试 | 适合 | 需要读写文件和执行测试命令 |
| 生成脚本并验证结果 | 适合 | 代码可以在隔离环境中运行 |
| 批量分析项目文件 | 适合 | 文件上传后在沙箱内处理 |
| 需要安装临时依赖的任务 | 适合 | 依赖不会污染宿主环境 |
| 直接访问生产数据库 | 不适合 | 凭证和权限风险高,应改用宿主侧受控工具 |
| 处理高度敏感文件 | 谨慎 | 需要评估 provider、网络隔离和数据清理策略 |
| 只做文本问答 | 通常不需要 | 没有代码执行和文件操作需求 |
设计取舍
Sandbox 带来的能力很明显:代理可以从“只会规划和生成文本”变成“能在隔离环境中完成可执行任务”。但它也引入了新的工程成本。
| 收益 | 代价 |
|---|---|
| 代理可以真实运行代码和测试 | 需要管理沙箱生命周期 |
| 宿主环境不被直接污染 | 执行环境初始化会有额外耗时 |
| 不同任务可以使用不同依赖 | 镜像、依赖和缓存需要维护 |
| 文件边界更清晰 | 需要设计上传和下载策略 |
| 风险比直接本机执行更低 | 仍要处理提示注入、网络外传和凭证管理 |
工程上最容易踩的坑是把 Sandbox 当成“绝对安全容器”。更合理的定位是:Sandbox 是执行隔离层,不是权限治理、密钥管理和内容安全的全部答案。
落地时的检查清单
实际接入 DeepAgents Sandboxes 时,可以按这张清单检查设计是否可靠。
| 检查项 | 建议 |
|---|---|
| 输入文件范围 | 只上传任务所需文件,不上传整个主目录 |
| 凭证处理 | 不把长期密钥放进沙箱环境变量 |
| 网络访问 | 默认关闭,需要时再按任务开启 |
| 执行限制 | 设置超时、输出大小和资源限制 |
| 文件导出 | 只下载明确需要的产物 |
| 日志记录 | 记录命令、退出码、关键错误,避免记录敏感内容 |
| 沙箱销毁 | 任务结束后释放环境并清理数据 |
| 敏感能力 | 通过宿主侧工具封装,不让代理直接接触凭证 |
DeepAgents 的 Sandbox 设计核心可以压缩成三层:统一的 execute() 命令执行接口、基于它构建的代理工具层、与宿主系统隔离的文件传输通道。理解这三层之后,就能判断一个代理任务应该放进沙箱执行,还是应该通过宿主侧工具完成。