智能体设计思考(四):Agent 与 Sandbox 的集成架构

Agent 与 Sandbox:一个被低估的架构决策
核心问题:Agent 该运行在沙箱”里面”,还是把沙箱当作一个”工具”来调用?
这不是简单的技术选型,而是影响安全边界、迭代效率、运维成本的根本性架构决策。
为什么这个问题很重要
当 AI Agent 开始执行代码、操作文件系统、调用外部 API 时,架构师必须回答一个根本问题。
LangChain 创始人 Harrison Chase 将其清晰地归纳为两种模式,引发了社区的广泛讨论。本文将剖析这两种模式的本质差异、适用边界,以及背后的安全原理。
模式一:Agent IN Sandbox(Agent 运行在沙箱内部)
核心架构
整个 Agent 框架——包括代码、Prompt、依赖库——被打包进容器镜像,在沙箱内部启动运行。外部应用通过 HTTP 或 WebSocket 与沙箱内的 Agent 通信。
flowchart TB
subgraph APPS["外部应用层"]
App1["代码助手"]
App2["数据分析"]
App3["自动化任务"]
end
subgraph GATEWAY["接入层"]
Auth["鉴权 / 限流 / 路由"]
end
subgraph POOL["沙箱资源池"]
Scheduler["调度器"]
subgraph SB_ACTIVE["运行中的沙箱"]
direction LR
subgraph SB1["实例 #1"]
A1["Agent Runtime"]
E1["执行环境"]
end
subgraph SB2["实例 #2"]
A2["Agent Runtime"]
E2["执行环境"]
end
end
subgraph SB_IDLE["预热待命"]
subgraph SB3["实例 #3(空闲)"]
A3["Agent Runtime"]
E3["执行环境"]
end
end
Scheduler -->|"分配"| SB_ACTIVE
Scheduler -.->|"预热"| SB_IDLE
end
subgraph OPS["运维层"]
Monitor["监控 / 告警"]
Lifecycle["生命周期管理"]
end
APPS --> GATEWAY --> Scheduler
SB_ACTIVE -.->|"指标上报"| Monitor
Lifecycle -.->|"超时清理"| SB_ACTIVE
style APPS fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
style GATEWAY fill:#FFF3E0,stroke:#EF6C00,color:#E65100
style POOL fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
style OPS fill:#FCE4EC,stroke:#C62828,color:#B71C1C
style SB_ACTIVE fill:#F1F8E9,stroke:#558B2F
style SB_IDLE fill:#F5F5F5,stroke:#9E9E9E,color:#616161
优势一:开发与生产环境完全一致
“在我机器上能跑”——这句话在 Agent IN Sandbox 模式下彻底消失。本地调试好的完整环境(依赖链、库版本、系统配置)直接打包成镜像部署到生产。
FROM python:3.11
# Agent 框架与业务代码一体打包
RUN pip install langchain langchain-anthropic pandas numpy
COPY agent/ /app/agent/
EXPOSE 8080
CMD ["python", "/app/agent/server.py"]
优势二:适合”环境耦合型” Agent
有些 Agent 天然需要与执行环境深度绑定:
- 数据分析 Agent:需要在同一进程内维护大型数据集的中间状态
- 科学计算 Agent:需要直接访问 GPU 硬件加速
- IDE 类 Agent:需要持久化文件系统和长生命周期服务进程
对这类场景,Agent 与执行环境的边界本就模糊,放在一起反而是更自然的设计。
代价一:通信基础设施成本
沙箱内的 Agent 需要对外暴露 API,这意味着你必须自行构建并维护:
flowchart TB
User["外部调用方"]
subgraph SB["沙箱环境(自建成本边界)"]
subgraph COMM["⚠️ 通信基础设施层(需自行实现)"]
LB["负载均衡"]
API["HTTP/WebSocket Server<br>路由 · 鉴权 · 限流"]
Session["会话管理<br>状态存储 · 超时清理"]
Retry["错误处理 / 熔断降级"]
end
subgraph AGENT["Agent 运行层"]
AgentCore["Agent Runtime + 工具"]
end
LB --> API --> Session --> AgentCore
Retry --> AgentCore
end
User <-->|"HTTP / WebSocket"| LB
Note["💡 E2B 等托管平台已封装上述大部分基础设施"]
style COMM fill:#FFF3E0,stroke:#EF6C00,color:#E65100
style AGENT fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
style Note fill:#F1F8E9,stroke:#558B2F,color:#33691E
E2B 等托管平台的核心价值正在于此——它们替你吸收了这层复杂度。如果自建,这些基础设施投入是真实且持续的。
代价二:API 密钥的安全困境(最棘手的问题)
这是模式一最本质的安全风险:Agent 在沙箱内进行 LLM 推理,意味着 API 密钥必须注入沙箱。
一旦沙箱被攻破,API 密钥随之暴露。
# ❌ 风险场景:密钥注入沙箱,一旦逃逸即全盘暴露
ANTHROPIC_API_KEY = "sk-ant-xxx" # 沙箱内可见
# ✅ 缓解方案:Agent 调用沙箱外部的推理代理服务
class LLMProxyClient:
def call(self, prompt: str) -> str:
# 推理服务运行在沙箱外的受控环境
# 沙箱内只持有低权限的访问 Token,而非原始 API Key
return requests.post(
"https://internal-llm-proxy.company.com/v1/infer",
json={"prompt": prompt},
headers={"Authorization": f"Bearer {SHORT_LIVED_TOKEN}"}
).json()["completion"]
缓解手段:使用密钥库服务(E2B / Runloop 均在开发中)、短期临时凭证、或让推理调用经由沙箱外部的代理服务完成。
代价三:迭代周期变长
每次修改 Agent 逻辑,必须走完完整的镜像构建流程:
修改代码 → 重建镜像 → 推送仓库 → 重新部署 → 验证结果
在 Agent Prompt 工程高频调试阶段,这个流程会显著拖慢节奏。
模式二:Sandbox as Tool(沙箱作为工具)
核心架构
Agent 运行在本地或受控服务器。沙箱只是 Agent 工具箱中的一个工具——当需要执行不可信代码时,通过 API 调用远程沙箱,拿回结果,继续推理。
flowchart LR
subgraph External["受控环境(Agent 侧)"]
User["用户应用"]
Agent["Agent 框架<br>推理 · 规划 · 状态管理"]
LLM["LLM 推理客户端"]
Keys["🔑 API 密钥<br>(安全隔离)"]
User --> Agent
Agent <--> LLM
Agent --- Keys
end
subgraph Remote["远程沙箱(隔离执行)"]
SB1["沙箱 #1<br>代码执行"]
SB2["沙箱 #2<br>代码执行"]
SBN["沙箱 #N<br>代码执行"]
end
Agent -->|"发送代码"| SB1
Agent -->|"发送代码"| SB2
Agent -->|"发送代码"| SBN
SB1 -->|"返回结果"| Agent
SB2 -->|"返回结果"| Agent
SBN -->|"返回结果"| Agent
style External fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
style Remote fill:#FFF3E0,stroke:#EF6C00,color:#E65100
优势一:快速迭代,改完即生效
修改 Agent 逻辑无需任何镜像构建步骤:
from langchain_anthropic import ChatAnthropic
from langchain_e2b import E2BSandbox
from langchain.agents import create_tool_calling_agent
# 沙箱只是工具列表中的一项
sandbox = E2BSandbox()
model = ChatAnthropic(model="claude-sonnet-4-20250514")
agent = create_tool_calling_agent(model, [sandbox], prompt)
# Agent 的 Prompt、逻辑、工具配置随时可改,直接生效
# 无需重建任何镜像
优势二:API 密钥天然隔离
这是模式二最核心的安全优势。密钥永远留在 Agent 侧(受控环境),远程沙箱中只有代码和执行结果,零敏感凭证。
class Agent:
def __init__(self):
# API 密钥在受控环境中,从未离开这里
self.client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
self.sandbox = RemoteSandbox()
def process(self, user_input: str):
response = self.client.messages.create(...) # LLM 推理在本地完成
if self.needs_code_execution(response):
code = self.extract_code(response)
# 发给沙箱的只有"代码",没有任何密钥
result = self.sandbox.execute(code)
return self.synthesize(result)
优势三:清晰的关注点分离
| 组件 | 职责 | 存储位置 |
|---|---|---|
| 对话历史 | 用户交互记录 | Agent 侧 |
| 推理链 | Agent 思考过程 | Agent 侧 |
| 执行结果 | 代码运行输出 | 沙箱侧(临时) |
| 临时文件 | 中间数据 | 沙箱侧(临时) |
这种分离的实际价值:沙箱崩溃不会丢失 Agent 对话上下文;可以随时更换沙箱后端(E2B → Modal → 自建)而不影响核心逻辑。
优势四:原生支持多沙箱并行
async def parallel_analysis(tasks: list[str]):
# 为每个子任务独立分配沙箱,真正并行执行
sandboxes = [E2BSandbox() for _ in tasks]
results = await asyncio.gather(*[
agent.analyze(task, sb) for task, sb in zip(tasks, sandboxes)
])
for sb in sandboxes:
sb.kill()
return results
代价:网络延迟与有状态会话的复杂性
每次代码执行都需要跨越网络边界。这在高频、小粒度执行场景下会累积成显著延迟。
flowchart TB
subgraph PROBLEM["问题:高频小调用导致延迟累积"]
C1["Agent"] -->|"execute(item 1) →"| S1["沙箱"]
S1 -->|"← result"| C1
C1 -->|"execute(item 2) →"| S1
S1 -->|"← result"| C1
C1 -->|"execute(item 3) →"| S1
S1 -->|"← result"| C1
N1["RTT × N 次 = 不可忽视的累积延迟"]
end
subgraph SOLUTION["解法:批量执行减少往返"]
C2["Agent"] -->|"execute(batch code) →"| S2["沙箱"]
S2 -->|"← batch result"| C2
N2["1 次 RTT,搞定所有计算"]
end
style PROBLEM fill:#FFEBEE,stroke:#C62828,color:#B71C1C
style SOLUTION fill:#E8F5E9,stroke:#2E7D32,color:#1B5E20
# ❌ 低效:N 次网络往返
for i in range(100):
result = sandbox.execute(f"process_item({i})")
# ✅ 高效:1 次网络往返
result = sandbox.execute("""
for i in range(100):
process_item(i)
""")
对于有状态会话(Session),E2B、Modal 等平台支持在同一会话中共享变量、文件和已安装的依赖——但这同时引入了会话生命周期管理、超时策略、并发竞争等新的复杂性,需要仔细权衡。
安全边界:两种模式的本质差异
Witan Labs 的 Nuno Campos 提出了一个关键洞察:
在模式一下,Agent 的任何部分都不应拥有比 Bash 工具更多的权限。
这句话揭示了两种模式安全设计的根本差异:
flowchart TB
subgraph M1["模式一:安全边界围绕整个Agent"]
M1_BOUNDARY["沙箱安全边界"]
subgraph M1_CONTENT["沙箱内容(全部暴露面)"]
M1A["Agent 推理逻辑"]
M1B["对话历史"]
M1C["🔑 API 密钥"]
M1D["工具调用记录"]
end
M1_RISK["⚠️ 沙箱被攻破 = Agent 全部暴露"]
end
subgraph M2["模式二:安全边界围绕单个工具"]
M2_BOUNDARY["沙箱安全边界"]
subgraph M2_CONTENT["沙箱内容(最小暴露面)"]
M2A["代码执行结果"]
M2B["临时文件"]
end
M2_SAFE["✅ 沙箱被攻破 = 仅丢失执行结果<br>API 密钥依然安全"]
end
style M1 fill:#FFEBEE,stroke:#C62828
style M1_CONTENT fill:#FFCDD2,stroke:#C62828
style M1_RISK fill:#FFCDD2,stroke:#C62828,color:#B71C1C
style M2 fill:#E8F5E9,stroke:#2E7D32
style M2_CONTENT fill:#C8E6C9,stroke:#2E7D32
style M2_SAFE fill:#C8E6C9,stroke:#2E7D32,color:#1B5E20
模式二还能实现工具级的细粒度权限控制,这是模式一做不到的:
# 模式二:每个工具有独立的权限边界
class BashTool:
def execute(self, command: str):
# Bash 工具:禁止网络访问
return self.sandbox.run(command, network_enabled=False)
class WebFetchTool:
ALLOWED_DOMAINS = ["api.trusted.com"]
def fetch(self, url: str):
# Web 工具:仅允许白名单域名
if not any(url.startswith(d) for d in self.ALLOWED_DOMAINS):
raise PermissionError(f"域名未授权:{url}")
return requests.get(url)
# Agent 同时持有两个工具,但权限完全隔离
tools = [BashTool(sandbox), WebFetchTool()]
前瞻:推理与执行的基础设施分化
Zo Computer 的 Ben Guo 提出了一个重要的前瞻观点:
未来 Agent 的推理过程可能需要运行在昂贵的 GPU 机器上,而代码执行的沙箱环境则完全不同。
这种资源需求的天然差异,将推动基础设施的进一步分化:
flowchart LR
subgraph GPU["推理集群(GPU 密集型)"]
LLM["LLM 推理引擎<br>H100 / A100<br>高成本 · 弹性伸缩"]
end
subgraph SCHED["调度层"]
Q["任务队列"]
R["结果路由"]
end
subgraph CPU["执行集群(CPU 密集型)"]
Pool["沙箱资源池<br>低成本 · 大规模水平扩展"]
SB1["沙箱 #1"]
SB2["沙箱 #2"]
SBN["沙箱 #N"]
Pool --- SB1 & SB2 & SBN
end
LLM -->|"生成代码任务"| Q
Q -->|"分发执行"| Pool
SB1 & SB2 & SBN -->|"执行结果"| R
R -->|"反馈上下文"| LLM
style GPU fill:#F3E5F5,stroke:#7B1FA2,color:#4A148C
style CPU fill:#E3F2FD,stroke:#1565C0,color:#0D47A1
style SCHED fill:#FFF8E1,stroke:#F57F17,color:#E65100
模式二天然适配这种分化:GPU 集群负责推理,CPU 沙箱集群负责执行,两者通过 API 解耦,独立扩缩容。模式一则需要将推理和执行绑定在同一节点,资源配比必然产生浪费。
沙箱技术栈:隔离强度的三个层次
选对架构模式后,还需要选对底层沙箱技术。理解隔离强度的本质差异至关重要:
flowchart TB
subgraph L1["层次一:容器(Docker)"]
D["共享主机内核<br>启动 ~50ms<br>隔离强度:低"]
D_RISK["⚠️ 内核漏洞 = 容器逃逸"]
end
subgraph L2["层次二:用户空间内核(gVisor)"]
G["拦截系统调用<br>启动毫秒级<br>隔离强度:中"]
G_RISK["Syscall 级防护<br>仍依赖主机内核(路径更长)"]
end
subgraph L3["层次三:微虚拟机(Firecracker/Kata)"]
F["独立 Linux 内核<br>KVM 硬件虚拟化<br>启动 ~125ms"]
F_SAFE["✅ 硬件级隔离<br>内核漏洞不跨实例传播"]
end
L1 -->|"+Syscall 过滤"| L2
L2 -->|"+独立内核"| L3
style L1 fill:#FFEBEE,stroke:#C62828
style L2 fill:#FFF9C4,stroke:#F9A825
style L3 fill:#E8F5E9,stroke:#2E7D32
为什么 AI Agent 场景不能只用 Docker?
Docker 容器共享主机内核。LLM 生成的代码是完全不可预测的——一旦代码触发内核漏洞,整个宿主机暴露。这在 AI Agent 场景是不可接受的风险。
两种模式的技术选型差异
flowchart TB
subgraph M1_STACK["模式一:整体强隔离(必须选择强隔离)"]
M1_WHY["沙箱内含 API 密钥<br>攻破沙箱 = 全盘暴露"]
M1_WHY --> FC1["✅ Firecracker — 首选"]
M1_WHY --> KT1["✅ Kata Containers — 推荐"]
M1_WHY --> GV1["⚠️ gVisor — 可用但非最优"]
M1_WHY --> DK1["❌ Docker — 不推荐"]
end
subgraph M2_STACK["模式二:工具级分级隔离(按风险灵活选择)"]
M2_WHY["沙箱内仅有执行结果<br>可根据工具风险分级"]
M2_WHY --> HIGH["高风险工具(代码执行)<br>→ Firecracker"]
M2_WHY --> MID["中风险工具(文件解析)<br>→ gVisor"]
M2_WHY --> LOW["低风险工具(内部可信计算)<br>→ Docker"]
end
style M1_STACK fill:#FFEBEE,stroke:#C62828
style M2_STACK fill:#E3F2FD,stroke:#1565C0
style FC1 fill:#E8F5E9,stroke:#2E7D32
style KT1 fill:#E8F5E9,stroke:#2E7D32
style GV1 fill:#FFF9C4,stroke:#F9A825
style DK1 fill:#FFCDD2,stroke:#C62828
style HIGH fill:#E8F5E9,stroke:#2E7D32
style MID fill:#FFF9C4,stroke:#F9A825
style LOW fill:#E3F2FD,stroke:#1565C0
主流平台技术栈一览
| 平台 | 底层技术 | 适配模式 | 核心特点 |
|---|---|---|---|
| E2B | Firecracker | 两种均支持 | 每个 Agent 独立微虚拟机,SDK 封装完善 |
| Modal | Firecracker | 模式二为主 | 专注函数执行,冷启动极快 |
| Daytona | 可配置 | 模式二 | 启动 <90ms,适合高频工具调用 |
| Runloop | 未公开 | 模式二 | 有状态会话,减少网络往返 |
| AWS Lambda | Firecracker | 模式二(Serverless) | 生态最成熟 |
| Google Cloud Run | gVisor | 模式二(容器化) | 容器接口,中等隔离 |
主流平台清一色选择 Firecracker 或 gVisor——这是 AI Agent 对安全强度要求的直接体现。
决策框架:如何选型
flowchart TD
Start(["开始选型"]) --> Q1{"Agent 与执行环境<br>耦合程度?"}
Q1 -->|"深度耦合<br>需要共享状态/硬件"| MODE1["→ 倾向模式一"]
Q1 -->|"松耦合<br>只需传代码拿结果"| Q2{"Agent 迭代频率?"}
Q2 -->|"高频调试<br>每天多次改动"| MODE2_A["→ 倾向模式二"]
Q2 -->|"逻辑稳定<br>不常修改"| Q3{"是否有敏感凭证<br>需要保护?"}
Q3 -->|"有 API 密钥等<br>敏感信息"| MODE2_B["→ 倾向模式二"]
Q3 -->|"无敏感信息<br>或已外置"| Q4{"是否需要<br>多沙箱并行?"}
Q4 -->|"需要并行执行<br>多个子任务"| MODE2_C["→ 模式二"]
Q4 -->|"单任务<br>顺序执行"| Q5{"对网络延迟<br>容忍度?"}
Q5 -->|"延迟敏感<br>且网络条件好"| MODE2_D["→ 模式二(批量调用)"]
Q5 -->|"延迟不敏感"| BOTH["→ 两者皆可<br>按运维能力决定"]
style Start fill:#E1BEE7,stroke:#8E24AA
style MODE1 fill:#E8F5E9,stroke:#2E7D32
style MODE2_A fill:#E3F2FD,stroke:#1565C0
style MODE2_B fill:#E3F2FD,stroke:#1565C0
style MODE2_C fill:#E3F2FD,stroke:#1565C0
style MODE2_D fill:#E3F2FD,stroke:#1565C0
style BOTH fill:#FFF9C4,stroke:#F9A825
快速对照表
| 决策因素 | 倾向模式一 | 倾向模式二 |
|---|---|---|
| 环境耦合 | 需要深度耦合、复杂共享状态 | 松耦合,只需代码执行结果 |
| 迭代速度 | 逻辑稳定,少量修改 | 高频迭代,频繁调试 Prompt |
| 安全敏感度 | 无 API 密钥等敏感信息 | 有敏感凭证,必须隔离 |
| 并发需求 | 单任务顺序执行 | 需要多沙箱并行处理 |
| 基础设施偏好 | 有团队自建运维通信层 | 希望最小化运维复杂度 |
| 未来演进 | 推理与执行保持同构 | 预期推理/执行资源解耦 |
代码实战:模式二的完整示例
from langchain_anthropic import ChatAnthropic
from langchain_e2b import E2BSandbox
from langchain.agents import create_tool_calling_agent
from langchain import hub
# Step 1:沙箱作为工具实例化
sandbox = E2BSandbox()
# Step 2:Agent 在沙箱外部构建,完整保留推理控制权
model = ChatAnthropic(model="claude-sonnet-4-20250514")
prompt = hub.pull("hwchase17/openai-tools-agent")
agent = create_tool_calling_agent(model, [sandbox], prompt)
# Step 3:执行任务
result = agent.invoke({
"messages": [{
"role": "user",
"content": "读取 /data/sales.csv,分析过去一年的销售趋势"
}]
})
# 执行流程:
# 1. Agent 在本地规划任务(LLM 推理,密钥安全)
# 2. Agent 生成 Python 分析代码
# 3. 调用 E2B API,代码在远程沙箱执行
# 4. 沙箱返回执行结果
# 5. Agent 在本地基于结果继续推理,生成最终报告
# Step 4:用完即销毁
sandbox.stop()
结语:没有银弹,只有权衡
两种模式本质上是在不同维度间寻找平衡:
- 安全性 vs 开发敏捷性
- 环境一致性 vs 关注点分离
- 低延迟 vs 强隔离
- 架构简单 vs 资源灵活
对大多数团队的建议:
① 从模式二开始:更低的初始复杂度,更快速的验证周期,更安全的凭证隔离。
② 观察瓶颈再决策:如果网络延迟成为真实瓶颈,或者环境耦合需求明确出现,再考虑模式一。
③ 选择支持两种模式的平台(如 E2B),保留未来切换的灵活性。
不要过早优化,但也不要忽视架构选择的长远影响。在 AI Agent 快速演进的今天,保持架构的可调整性,往往比一开始就”选对”更重要。