第一部分:Agent 的工具系统
1.1 先搞清楚:工具是用来干嘛的
大模型(LLM)有一个先天缺陷------它只会输出文字。
它能写诗、能解释代码,但它做不了三件事:
| LLM 的短板 | 需要的工具 |
|---|---|
| 碰不到外界信息 | 读取类工具 |
| 动不了真实世界 | 执行/写入类工具 |
| 记不住跨轮的事 | 记忆/状态类工具 |
所有 agent 工具,本质都是在补 LLM 的这三个洞。
1.2 工具分类全景
| 类别 | 典型工具 | 干什么 | 类比 |
|---|---|---|---|
| 文件读写 | Read / Write / Edit / Glob / Grep | 看文件、写文件、改文件、按名找、按内容搜 | 眼睛 + 笔 + 字典 |
| 命令执行 | Bash / Shell | 跑 shell、编译、装包、起服务 | 一双手 |
| 网络访问 | WebFetch / WebSearch | 抓网页、搜互联网 | 望远镜 + 问路人 |
| 信息检索 | 语义搜索 / 知识库 / 代码检索 | 在自己的资料里找 | 翻自家档案柜 |
| 外部集成 | MCP 类工具 | 调第三方系统 API | 各种专用接头 |
| 执行/计算 | 代码解释器 / LSP / Notebook | 真跑代码、要类型信息 | 计算器 + 编译器 |
| 人机交互 | 提问 / 确认 / 通知 / 剪贴板 | 问你、弹确认、发通知 | 开口说话 |
| 多媒体生成 | 图像生成 / 视频生成 | 生成图片视频 | 画笔 + 摄像机 |
1.3 一个反直觉的设计:为什么文件操作要单独封装
既然有 Bash,cat、sed、find 都能干,为什么还要拆出 Read/Write/Edit/Glob/Grep 五个专用工具?
| 专用工具 | 为什么不直接用 shell |
|---|---|
| Read | cat 把大文件一次糊你一脸;Read 支持分页、行号、按段读 |
| Edit | sed 改文件极易改错且不可逆;Edit 做精确串替换 + 校验 |
| Glob | find 语法繁琐;Glob 一眼 **/*.ts 就懂 |
| Grep | grep 要拼参数;Grep 封装成结构化结果 |
规律:能用 shell 干的事,如果高频且容易出错,就会被封装成专用工具。
这就是工具设计的第一原则:不是"能不能做",而是"值不值得专门做一层"。
第二部分:内置工具 vs MCP 工具
2.1 两者的本质区别
| 维度 | 内置工具 | MCP 工具 |
|---|---|---|
| 谁提供 | Agent 框架自产自销 | 第三方服务 |
| 部署位置 | 同进程内 | 外部进程/远程 |
| 要配置吗 | 通常不用 | 通常要 |
| 调用方式 | 本地函数直接调用 | 网络 RPC |
| 速度 | 纳秒~毫秒 | 毫秒~百毫秒 |
| 用户感知度 | 低(像空气) | 高(像家具) |
一个关键认知:
内置工具 = agent 天生的器官(手、眼、嘴)
MCP 工具 = agent 外接的假肢/外设(要插、要装驱动)
2.2 感知度公式
为什么大家天天聊 MCP,却从不聊"内置 Read 工具"?
感知度 = 用户付出的"认知与操作成本" × 能力增量的"可见度"
- 内置工具:零配置(成本=0)→ 无感
- MCP 工具:要装要配(成本高)且接进来的是你原本没有的能力(增量显眼)→ 双倍存在感
注意因果方向 :不是"因为要配所以是外接的",而是"因为它是外接的,所以才要配"。配置只是"外接"这个事实的影子。
2.3 MCP 统一了什么,没统一什么
这是最容易被误解的点,必须钉死:
| 层面 | 有标准吗 | 说明 |
|---|---|---|
| 工具怎么实现 | ❌ 没有 | 各家自己写函数 |
| 工具怎么描述给模型 | ✅ 大体有 | JSON Schema(名字、参数、描述) |
| 工具怎么从外部接入 | ✅ 有 | MCP 协议 |
用餐厅打比方:
- JSON Schema = 菜单怎么写(格式统一)
- MCP = 餐厅怎么对外营业(点单流程统一)
- 工具实现 = 后厨怎么做菜(各家自便,没人管)
MCP 的价值不是"统一实现",而是"统一了外部工具被 agent 发现和调用的门槛"。
第三部分:调度链路------一次工具调用是怎么发生的
3.1 LLM 只"决定",不"执行"
这是全篇最核心的一句话。 LLM 自己没有任何执行力 。它输出的是一段结构化 JSON:
json
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "Read",
"arguments": "{\"path\": \"pom.xml\", \"limit\": 50}"
}
}]
}
注意:这里面没有"执行"二字。它只是"说了一句想调 Read"。 剩下的全交给 agent 运行时。
3.2 完整调度链路
#mermaid-svg-2D8WjiVOcHfxS2Li{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2D8WjiVOcHfxS2Li .error-icon{fill:#552222;}#mermaid-svg-2D8WjiVOcHfxS2Li .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2D8WjiVOcHfxS2Li .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2D8WjiVOcHfxS2Li .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2D8WjiVOcHfxS2Li .marker.cross{stroke:#333333;}#mermaid-svg-2D8WjiVOcHfxS2Li svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2D8WjiVOcHfxS2Li p{margin:0;}#mermaid-svg-2D8WjiVOcHfxS2Li .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li .cluster-label text{fill:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li .cluster-label span{color:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li .cluster-label span p{background-color:transparent;}#mermaid-svg-2D8WjiVOcHfxS2Li .label text,#mermaid-svg-2D8WjiVOcHfxS2Li span{fill:#333;color:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li .node rect,#mermaid-svg-2D8WjiVOcHfxS2Li .node circle,#mermaid-svg-2D8WjiVOcHfxS2Li .node ellipse,#mermaid-svg-2D8WjiVOcHfxS2Li .node polygon,#mermaid-svg-2D8WjiVOcHfxS2Li .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2D8WjiVOcHfxS2Li .rough-node .label text,#mermaid-svg-2D8WjiVOcHfxS2Li .node .label text,#mermaid-svg-2D8WjiVOcHfxS2Li .image-shape .label,#mermaid-svg-2D8WjiVOcHfxS2Li .icon-shape .label{text-anchor:middle;}#mermaid-svg-2D8WjiVOcHfxS2Li .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2D8WjiVOcHfxS2Li .rough-node .label,#mermaid-svg-2D8WjiVOcHfxS2Li .node .label,#mermaid-svg-2D8WjiVOcHfxS2Li .image-shape .label,#mermaid-svg-2D8WjiVOcHfxS2Li .icon-shape .label{text-align:center;}#mermaid-svg-2D8WjiVOcHfxS2Li .node.clickable{cursor:pointer;}#mermaid-svg-2D8WjiVOcHfxS2Li .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2D8WjiVOcHfxS2Li .arrowheadPath{fill:#333333;}#mermaid-svg-2D8WjiVOcHfxS2Li .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2D8WjiVOcHfxS2Li .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2D8WjiVOcHfxS2Li .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2D8WjiVOcHfxS2Li .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2D8WjiVOcHfxS2Li .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2D8WjiVOcHfxS2Li .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2D8WjiVOcHfxS2Li .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2D8WjiVOcHfxS2Li .cluster text{fill:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li .cluster span{color:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2D8WjiVOcHfxS2Li .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2D8WjiVOcHfxS2Li rect.text{fill:none;stroke-width:0;}#mermaid-svg-2D8WjiVOcHfxS2Li .icon-shape,#mermaid-svg-2D8WjiVOcHfxS2Li .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2D8WjiVOcHfxS2Li .icon-shape p,#mermaid-svg-2D8WjiVOcHfxS2Li .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2D8WjiVOcHfxS2Li .icon-shape .label rect,#mermaid-svg-2D8WjiVOcHfxS2Li .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2D8WjiVOcHfxS2Li .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2D8WjiVOcHfxS2Li .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2D8WjiVOcHfxS2Li :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LLM 输出 tool_calls
{name:'Read', args:{...}}
agent 运行时解析这段文本
拿到工具名 'Read'
在【本地工具注册表】按名查找
registry'Read'
命中 → 直接调用本地函数对象
ReadFn(path, limit)
函数体内: 读文件/加行号/裁剪
结果包成一条 tool 消息
追加进对话历史,重新发给 LLM
LLM 看到结果,继续下一步
3.3 关键:内置工具"怎么被调起来"
答案:就是一次普通的本地函数调用(in-process function call)。
| 内置工具 | MCP 工具 | |
|---|---|---|
| 工具是什么 | 内存里一个函数对象 | 一个远程服务 |
| 怎么找到 | 查本地注册表(一个 dict) | 走 MCP 协议问 server |
| 怎么调用 | fn(**args) 直接调 |
发 JSON-RPC 网络请求 |
| 跨进程吗 | ❌ 同进程 | ✅ 跨进程/跨机 |
3.4 靠什么实现的:工具注册表
agent 框架启动时,把内置工具登记进一张表:
python
# 伪代码
registry = {}
def register_tool(name, description, params_schema, fn):
registry[name] = {
"description": description,
"schema": params_schema, # 发给 LLM 看的(菜单)
"fn": fn, # 真正干活的(后厨)
}
register_tool("Read", "...", {...}, read_impl)
register_tool("Write", "...", {...}, write_impl)
register_tool("Bash", "...", {...}, bash_impl)
模型报个名,运行时用它去字典里捞出真函数,调它就完事。
#mermaid-svg-3PFhLkM5COUA6bRj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3PFhLkM5COUA6bRj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3PFhLkM5COUA6bRj .error-icon{fill:#552222;}#mermaid-svg-3PFhLkM5COUA6bRj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3PFhLkM5COUA6bRj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3PFhLkM5COUA6bRj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3PFhLkM5COUA6bRj .marker.cross{stroke:#333333;}#mermaid-svg-3PFhLkM5COUA6bRj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3PFhLkM5COUA6bRj p{margin:0;}#mermaid-svg-3PFhLkM5COUA6bRj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3PFhLkM5COUA6bRj .cluster-label text{fill:#333;}#mermaid-svg-3PFhLkM5COUA6bRj .cluster-label span{color:#333;}#mermaid-svg-3PFhLkM5COUA6bRj .cluster-label span p{background-color:transparent;}#mermaid-svg-3PFhLkM5COUA6bRj .label text,#mermaid-svg-3PFhLkM5COUA6bRj span{fill:#333;color:#333;}#mermaid-svg-3PFhLkM5COUA6bRj .node rect,#mermaid-svg-3PFhLkM5COUA6bRj .node circle,#mermaid-svg-3PFhLkM5COUA6bRj .node ellipse,#mermaid-svg-3PFhLkM5COUA6bRj .node polygon,#mermaid-svg-3PFhLkM5COUA6bRj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3PFhLkM5COUA6bRj .rough-node .label text,#mermaid-svg-3PFhLkM5COUA6bRj .node .label text,#mermaid-svg-3PFhLkM5COUA6bRj .image-shape .label,#mermaid-svg-3PFhLkM5COUA6bRj .icon-shape .label{text-anchor:middle;}#mermaid-svg-3PFhLkM5COUA6bRj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3PFhLkM5COUA6bRj .rough-node .label,#mermaid-svg-3PFhLkM5COUA6bRj .node .label,#mermaid-svg-3PFhLkM5COUA6bRj .image-shape .label,#mermaid-svg-3PFhLkM5COUA6bRj .icon-shape .label{text-align:center;}#mermaid-svg-3PFhLkM5COUA6bRj .node.clickable{cursor:pointer;}#mermaid-svg-3PFhLkM5COUA6bRj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3PFhLkM5COUA6bRj .arrowheadPath{fill:#333333;}#mermaid-svg-3PFhLkM5COUA6bRj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3PFhLkM5COUA6bRj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3PFhLkM5COUA6bRj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3PFhLkM5COUA6bRj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3PFhLkM5COUA6bRj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3PFhLkM5COUA6bRj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3PFhLkM5COUA6bRj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3PFhLkM5COUA6bRj .cluster text{fill:#333;}#mermaid-svg-3PFhLkM5COUA6bRj .cluster span{color:#333;}#mermaid-svg-3PFhLkM5COUA6bRj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3PFhLkM5COUA6bRj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3PFhLkM5COUA6bRj rect.text{fill:none;stroke-width:0;}#mermaid-svg-3PFhLkM5COUA6bRj .icon-shape,#mermaid-svg-3PFhLkM5COUA6bRj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3PFhLkM5COUA6bRj .icon-shape p,#mermaid-svg-3PFhLkM5COUA6bRj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3PFhLkM5COUA6bRj .icon-shape .label rect,#mermaid-svg-3PFhLkM5COUA6bRj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3PFhLkM5COUA6bRj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3PFhLkM5COUA6bRj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3PFhLkM5COUA6bRj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} agent运行时
模型侧
tool_calls (纯文本JSON)
读文件/Bash
JSON-RPC
tool 消息回灌
LLM
只看 schema
只会决定
调度器
registryname.fn
内置 fn
本地函数直调
MCP 代理 fn
发网络请求
结果
3.5 两种工具怎么在同一套调度里共存
对调度器来说,MCP 工具和内置工具结构一样,只是 fn 的实现不同:
registry = {
"Read": { schema, fn: <本地函数> }, # 内置
"github_create_pr": { schema, fn: <MCP 代理函数> } # 外部
}
统一在调度层(都从注册表捞、都按名调);分叉在执行层(本地直调 vs 网络 RPC)。
第四部分:沙箱------Agent 的"房子"
4.1 什么是沙箱
沙箱 = 一块隔离出来的、可以随便折腾坏了也不影响别人的执行环境。
4.2 沙箱解决什么问题
| 场景 | 不隔离的后果 |
|---|---|
用户让 agent rm -rf / |
把宿主服务器删了 |
| agent 跑了个挖矿脚本 | 整台机器被拖垮 |
| 用户 A 的文件 | 用户 B 能直接读到 |
| agent 装了 100 个包 | 污染全局环境 |
对应沙箱四个职责:
| 职责 | 隔离谁和谁 |
|---|---|
| 安全隔离 | agent 与宿主系统 |
| 资源隔离 | 限制 CPU/内存/磁盘 |
| 租户隔离 | 用户 A 与用户 B |
| 环境隔离 | 项目 A 与项目 B |
4.3 隔离技术分级
| 手段 | 隔离粒度 | 通俗解释 | 例子 |
|---|---|---|---|
| 进程级 | 弱 | 换个用户/目录跑 | chroot |
| 容器级 | 中 | 共享内核,独立文件系统/网络 | Docker |
| 虚机级 | 强 | 独立内核,完全另一台机器 | KVM、Firecracker |
| 函数级 | 特化 | 每次执行现开微虚机 | AWS Lambda |
第五部分:★ 两种沙箱------最容易混的核心概念
"沙箱"这个词被用在两个层级,同名不同物。这是全篇最容易混淆、也最需要厘清的一点。
5.1 对照表
| A. Agent 运行沙箱 | B. 代码执行沙箱 | |
|---|---|---|
| 装的是谁 | 整个 agent 程序 | 一段临时脚本 |
| 目的 | 隔离 agent 与宿主系统 | 隔离不可信代码与 agent 自己 |
| 生命周期 | 跟随整个会话(长) | 跟随单次执行(短,跑完就扔) |
| 谁需要它 | 云端 agent 平台 | 任何"要跑外部代码"的场景 |
| 典型技术 | Docker/VM(整个程序装进去) | 受限解释器、子进程、微VM |
| 方向 | agent 住在沙箱里(租客) | agent 把代码扔进沙箱(房东) |
看出区别了吗?方向刚好反过来!
- A:agent 是租客,住在沙箱里
- B:agent 是房东,把不可信的代码关进沙箱
5.2 二者的嵌套关系
#mermaid-svg-dafVainfS9eoqjzn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dafVainfS9eoqjzn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dafVainfS9eoqjzn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dafVainfS9eoqjzn .error-icon{fill:#552222;}#mermaid-svg-dafVainfS9eoqjzn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dafVainfS9eoqjzn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dafVainfS9eoqjzn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dafVainfS9eoqjzn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dafVainfS9eoqjzn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dafVainfS9eoqjzn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dafVainfS9eoqjzn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dafVainfS9eoqjzn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dafVainfS9eoqjzn .marker.cross{stroke:#333333;}#mermaid-svg-dafVainfS9eoqjzn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dafVainfS9eoqjzn p{margin:0;}#mermaid-svg-dafVainfS9eoqjzn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dafVainfS9eoqjzn .cluster-label text{fill:#333;}#mermaid-svg-dafVainfS9eoqjzn .cluster-label span{color:#333;}#mermaid-svg-dafVainfS9eoqjzn .cluster-label span p{background-color:transparent;}#mermaid-svg-dafVainfS9eoqjzn .label text,#mermaid-svg-dafVainfS9eoqjzn span{fill:#333;color:#333;}#mermaid-svg-dafVainfS9eoqjzn .node rect,#mermaid-svg-dafVainfS9eoqjzn .node circle,#mermaid-svg-dafVainfS9eoqjzn .node ellipse,#mermaid-svg-dafVainfS9eoqjzn .node polygon,#mermaid-svg-dafVainfS9eoqjzn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dafVainfS9eoqjzn .rough-node .label text,#mermaid-svg-dafVainfS9eoqjzn .node .label text,#mermaid-svg-dafVainfS9eoqjzn .image-shape .label,#mermaid-svg-dafVainfS9eoqjzn .icon-shape .label{text-anchor:middle;}#mermaid-svg-dafVainfS9eoqjzn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dafVainfS9eoqjzn .rough-node .label,#mermaid-svg-dafVainfS9eoqjzn .node .label,#mermaid-svg-dafVainfS9eoqjzn .image-shape .label,#mermaid-svg-dafVainfS9eoqjzn .icon-shape .label{text-align:center;}#mermaid-svg-dafVainfS9eoqjzn .node.clickable{cursor:pointer;}#mermaid-svg-dafVainfS9eoqjzn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dafVainfS9eoqjzn .arrowheadPath{fill:#333333;}#mermaid-svg-dafVainfS9eoqjzn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dafVainfS9eoqjzn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dafVainfS9eoqjzn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dafVainfS9eoqjzn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dafVainfS9eoqjzn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dafVainfS9eoqjzn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dafVainfS9eoqjzn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dafVainfS9eoqjzn .cluster text{fill:#333;}#mermaid-svg-dafVainfS9eoqjzn .cluster span{color:#333;}#mermaid-svg-dafVainfS9eoqjzn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dafVainfS9eoqjzn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dafVainfS9eoqjzn rect.text{fill:none;stroke-width:0;}#mermaid-svg-dafVainfS9eoqjzn .icon-shape,#mermaid-svg-dafVainfS9eoqjzn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dafVainfS9eoqjzn .icon-shape p,#mermaid-svg-dafVainfS9eoqjzn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dafVainfS9eoqjzn .icon-shape .label rect,#mermaid-svg-dafVainfS9eoqjzn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dafVainfS9eoqjzn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dafVainfS9eoqjzn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dafVainfS9eoqjzn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} A. Agent 运行沙箱(大盒子,装整个agent)
需要执行代码
返回结果
B. 代码执行沙箱(小盒子,装临时脚本)
跑那段 Python/JS 脚本
agent 程序
大盒子套小盒子:agent 住在 A 里,A 里再开 B 去跑不可信脚本。
5.3 两种沙箱,你要写代码实现吗
| 沙箱 | 你要写代码实现吗 | 说明 |
|---|---|---|
| A. Agent 运行沙箱 | ❌ 不写 | 部署层(Docker/K8s/云)提供 |
| B. 代码执行沙箱 | ⚠️ 可能要 | 若你要支持"跑外部代码",你得自己动手隔离 |
B 是唯一一个你可能真需要在代码里"动手"的沙箱。
第六部分:文件进出的真相
6.1 一个常见直觉 vs 真相
直觉:文件不在沙箱里,是不是"复制一份进去 → 改好 → 自动写回本地"?
真相 :要分两种 agent 看,而且两种都不是这个模型。
| 模式 | 文件在哪 | 是"复制→改→回写"吗 |
|---|---|---|
| 云端 agent | 上传的文件复制进沙箱 | ✅ 进是复制;但改写后不自动写回,要你手动下载 |
| 本地 agent | 文件本就在本地盘,直接原地读写 | ❌ 根本没有复制这回事 |
6.2 云端 agent 的文件流转
#mermaid-svg-QhZuYc5NY0iGGArl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QhZuYc5NY0iGGArl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QhZuYc5NY0iGGArl .error-icon{fill:#552222;}#mermaid-svg-QhZuYc5NY0iGGArl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QhZuYc5NY0iGGArl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QhZuYc5NY0iGGArl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QhZuYc5NY0iGGArl .marker.cross{stroke:#333333;}#mermaid-svg-QhZuYc5NY0iGGArl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QhZuYc5NY0iGGArl p{margin:0;}#mermaid-svg-QhZuYc5NY0iGGArl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QhZuYc5NY0iGGArl .cluster-label text{fill:#333;}#mermaid-svg-QhZuYc5NY0iGGArl .cluster-label span{color:#333;}#mermaid-svg-QhZuYc5NY0iGGArl .cluster-label span p{background-color:transparent;}#mermaid-svg-QhZuYc5NY0iGGArl .label text,#mermaid-svg-QhZuYc5NY0iGGArl span{fill:#333;color:#333;}#mermaid-svg-QhZuYc5NY0iGGArl .node rect,#mermaid-svg-QhZuYc5NY0iGGArl .node circle,#mermaid-svg-QhZuYc5NY0iGGArl .node ellipse,#mermaid-svg-QhZuYc5NY0iGGArl .node polygon,#mermaid-svg-QhZuYc5NY0iGGArl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QhZuYc5NY0iGGArl .rough-node .label text,#mermaid-svg-QhZuYc5NY0iGGArl .node .label text,#mermaid-svg-QhZuYc5NY0iGGArl .image-shape .label,#mermaid-svg-QhZuYc5NY0iGGArl .icon-shape .label{text-anchor:middle;}#mermaid-svg-QhZuYc5NY0iGGArl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QhZuYc5NY0iGGArl .rough-node .label,#mermaid-svg-QhZuYc5NY0iGGArl .node .label,#mermaid-svg-QhZuYc5NY0iGGArl .image-shape .label,#mermaid-svg-QhZuYc5NY0iGGArl .icon-shape .label{text-align:center;}#mermaid-svg-QhZuYc5NY0iGGArl .node.clickable{cursor:pointer;}#mermaid-svg-QhZuYc5NY0iGGArl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QhZuYc5NY0iGGArl .arrowheadPath{fill:#333333;}#mermaid-svg-QhZuYc5NY0iGGArl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QhZuYc5NY0iGGArl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QhZuYc5NY0iGGArl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QhZuYc5NY0iGGArl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QhZuYc5NY0iGGArl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QhZuYc5NY0iGGArl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QhZuYc5NY0iGGArl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QhZuYc5NY0iGGArl .cluster text{fill:#333;}#mermaid-svg-QhZuYc5NY0iGGArl .cluster span{color:#333;}#mermaid-svg-QhZuYc5NY0iGGArl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QhZuYc5NY0iGGArl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QhZuYc5NY0iGGArl rect.text{fill:none;stroke-width:0;}#mermaid-svg-QhZuYc5NY0iGGArl .icon-shape,#mermaid-svg-QhZuYc5NY0iGGArl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QhZuYc5NY0iGGArl .icon-shape p,#mermaid-svg-QhZuYc5NY0iGGArl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QhZuYc5NY0iGGArl .icon-shape .label rect,#mermaid-svg-QhZuYc5NY0iGGArl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QhZuYc5NY0iGGArl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QhZuYc5NY0iGGArl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QhZuYc5NY0iGGArl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 上传(复制)
② Read/Write 原地改
③ 交付/下载(手动)
你设备里的 report.docx
/root/uploads/report.docx
(沙箱内)
沙箱内新文件
回到你设备
- 进:上传 = 复制进沙箱
- 改:全程只动沙箱内那一份
- 出 :没有自动回写! 必须靠"交付/下载"动作
6.3 本地 agent:根本不复制
你本地: E:\project\pom.xml
↓
agent 运行时(跑在你电脑上) → 直接 open("E:\\project\\pom.xml")
↓
原地读、原地改、原地写 ← 没有副本
6.4 沙箱隔的到底是什么
沙箱隔的从来不是"文件",而是"进程能访问的范围"。
- 云端:沙箱是独立 VM,你的文件不在里面 ,所以必须先搬进去(上传)
- 本地:agent 进程和你的盘在同一系统,没有墙,直接访问
"复制进去"不是沙箱的要求,是"你的文件恰好在沙箱外"导致的必然结果。
6.5 实战坑
| 坑 | 说明 |
|---|---|
| 云端"改写不落盘" | 改了没交付 → 改动还在沙箱里,你那边纹丝不动 |
| 本地"没有后悔药" | 直接改真文件,误操作难恢复 |
第七部分:程序、环境、沙箱三者关系
7.1 纠正一个直觉错位
错误心智模型:
agent程序(A环境) ──跳──► 沙箱(B环境) ──执行工具──► 回来
正确心智模型:
┌──── 同一个环境(沙箱=宿主)────┐
│ │
│ agent程序 ── 直接调 ──► 工具 │
│ │
└────────────────────────────────┘
不存在"跨环境跳"这一步。agent 程序和它的工具都住在同一个环境里。
7.2 agent 程序 vs 沙箱:是不是一回事
答案:不是,但绑定共存。
| 概念 | 是"谁" | 是"在哪" |
|---|---|---|
| agent 程序 | ✅ 进程 | --- |
| 沙箱 | --- | ✅ 环境 |
比方:agent 程序 = 你这个人;沙箱 = 你上班的办公室。
你在办公室办公,但你 ≠ 办公室。
运行视角:绝大多数部署里它俩"绑定共存"------沙箱销毁,agent 也停;agent 停,沙箱回收。
7.3 宿主 ≠ 沙箱(关键区分)
| 概念 | 是什么 |
|---|---|
| 宿主环境 | 程序跑在哪台机/哪个系统(人人都有) |
| 沙箱 | 这个环境是否被隔离(要看有没有隔离手段) |
| 部署方式 | 宿主是? | 是沙箱吗 |
|---|---|---|
| 直接跑自己笔记本 | 笔记本 | ❌ 不是(无隔离) |
| 跑在 Docker 容器 | 容器 | ✅ 是 |
| 跑在云 VM | 云 VM | ✅ 是 |
| 跑在服务器裸机 | 服务器 | ❌ 不是 |
"部署在哪" → 得到宿主环境;"有没有隔离" → 才决定是不是沙箱。
7.4 沙箱需要 agent 代码去实现吗
主线结论:
沙箱不需要 agent 程序去代码实现。它由部署/平台层提供,程序只是"住进去"。
python
# 你的 agent 代码里,大概就这些
String result = exec("ls -la"); # 执行命令
String content = readFile("a.txt"); # 读文件
你只管"调工具",压根不管"在哪执行"。 因为环境在你程序启动前就已存在。
用比方钉死:
你写了个程序(像个打工人),你会去写"给我的办公室装防弹门"的代码吗?不会。办公室是房东(平台/运维)提供的。
| 隔离级别 | 谁提供 | agent 代码写啥 |
|---|---|---|
| 进程级 | 部署命令 | 啥都不用写 |
| 容器级 | 平台/运维写 Dockerfile | 啥都不用写 |
| 虚机级 | 云平台 | 啥都不用写 |
| 函数级 | 云厂商 | 啥都不用写 |
第八部分:代码执行沙箱(B)的实现
8.1 实现方案全景
| 方案 | 隔离强度 | 启动速度 | 难度 | 适用场景 |
|---|---|---|---|---|
| 子进程 + 资源限制 | 弱 | 极快 | 低 | 可信代码、内部脚本 |
| 受限解释器 | 弱 | 快 | 中 | Python 表达式求值 |
| 容器(Docker) | 强 | 中 | 中 | 通用,主流选择 |
| 微虚机(Firecracker/gVisor) | 很强 | 较快 | 高 | 多租户 SaaS |
| 云函数(Lambda) | 强 | 慢 | 低 | 不想自运维 |
| WASM | 强 | 极快 | 中 | JS/WASM 生态 |
8.2 子进程方案(最轻量)
python
import subprocess, resource
def run_sandboxed(code):
def preexec():
resource.setrlimit(resource.RLIMIT_CPU, (5, 5)) # 最多5秒CPU
resource.setrlimit(resource.RLIMIT_AS, (256<<20,)) # 最多256MB内存
r = subprocess.run(["python3", "-c", code],
capture_output=True, timeout=10, preexec_fn=preexec)
return r.stdout, r.stderr
- 能防:跑飞、死循环、吃内存
- 不能防:读写文件、联网(同用户同命名空间)
- 结论 :只防"跑飞",不防"干坏事"
8.3 容器方案(主流)
python
import docker
client = docker.from_env()
def run_in_container(code):
return client.containers.run(
"my-sandbox:latest",
command=["python", "-c", code],
network_disabled=True, # ★ 断网
mem_limit="256m", # ★ 限内存
cpu_quota=50000, # ★ 限CPU
read_only=True, # ★ 只读根文件系统
volumes={"/tmp/sbx": {"bind": "/work", "mode": "rw"}},
remove=True, # ★ 跑完自动删
)
8.4 沙箱一般在做哪些事
| 维度 | 沙箱要干的活 | 手段 |
|---|---|---|
| 文件系统 | 只碰指定目录,其余只读/不可见 | 挂载隔离、只读层 |
| 网络 | 默认断网,或白名单 | network_disabled、iptables |
| CPU | 限时间,防死循环 | cgroups、rlimit、超时 |
| 内存 | 限大小,防吃爆 | cgroups、rlimit |
| 磁盘 | 限写入量 | quota、临时目录 |
| 进程 | 限数量、限权限 | 降权、seccomp |
| 系统调用 | 只放行必要 syscall | seccomp-bpf |
| 生命周期 | 超时杀、跑完销毁 | remove、容器池 |
| 输入输出 | 只传代码进、只取结果出 | 标准输入输出管道 |
| 审计 | 记录跑了什么 | 日志 |
沙箱 = 一整套"限制"的集合体,不是单一功能。
8.5 实战坑
- 别用
eval()/exec()直接跑------零隔离 timeout必须设------否则死循环挂死服务- 网络默认关------否则脚本能外传数据
- 跑完要销毁------别复用同一沙箱跑不同用户代码
- 输出要限量 ------
print100 万行撑爆内存 - 别信"只读"------要真只读(挂载只读层)
第九部分:容器的创建与环境准备
9.1 容器自带 Python 吗
❌ 不是。容器是个空壳,里面有什么全看镜像。
| 镜像 | 里面有啥 |
|---|---|
python:3.11-slim |
✅ Python 3.11 |
node:20 |
✅ Node.js |
ubuntu:22.04 |
❌ 啥都没有 |
alpine |
⚠️ 极简 |
"容器自带环境"是错觉------是你选了带环境的镜像。
Docker 隔离模型 :容器 = 镜像(文件系统快照)+ 一层可写层。
9.2 镜像要提前准备
dockerfile
# Dockerfile.sandbox
FROM python:3.11-slim
RUN pip install numpy pandas requests # ★ 预装依赖
WORKDIR /work
bash
docker build -t my-agent-sandbox:latest -f Dockerfile.sandbox .
9.3 ★ 每次新建 vs 复用
默认做法:每次调用新建一个容器,跑完销毁(安全优先)。
| 每次新建的好处 | 复用同一个的坏处 |
|---|---|
| 用户 A 的痕迹不留给 B | A 的文件/内存/环境变量留在里面 |
| 跑挂的污染只影响这一个 | 一个脚本改环境,影响后续所有 |
| 状态干净可预测 | 状态漂移,结果不可复现 |
但"每次新建"有冷启动代价(几百毫秒~几秒),优化手段:
| 优化方案 | 做法 | 牺牲 |
|---|---|---|
| 容器池(Pool) | 预起 N 个待命,借出归还 | 归还前必须清理 |
| 快照/预热 | 微虚机快照,秒起 | 复杂度高 |
| 热进程复用 | 同任务内复用 | 仅限可信同源 |
| WASM | 微秒级启动 | 生态限制 |
关键原则:复用的边界 = 信任的边界。
第十部分:Agent 在沙箱里拉起沙箱容器
10.1 问题本质:DinD
#mermaid-svg-vGeZpU8wCupJmKxl{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vGeZpU8wCupJmKxl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vGeZpU8wCupJmKxl .error-icon{fill:#552222;}#mermaid-svg-vGeZpU8wCupJmKxl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vGeZpU8wCupJmKxl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vGeZpU8wCupJmKxl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vGeZpU8wCupJmKxl .marker.cross{stroke:#333333;}#mermaid-svg-vGeZpU8wCupJmKxl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vGeZpU8wCupJmKxl p{margin:0;}#mermaid-svg-vGeZpU8wCupJmKxl .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vGeZpU8wCupJmKxl .cluster-label text{fill:#333;}#mermaid-svg-vGeZpU8wCupJmKxl .cluster-label span{color:#333;}#mermaid-svg-vGeZpU8wCupJmKxl .cluster-label span p{background-color:transparent;}#mermaid-svg-vGeZpU8wCupJmKxl .label text,#mermaid-svg-vGeZpU8wCupJmKxl span{fill:#333;color:#333;}#mermaid-svg-vGeZpU8wCupJmKxl .node rect,#mermaid-svg-vGeZpU8wCupJmKxl .node circle,#mermaid-svg-vGeZpU8wCupJmKxl .node ellipse,#mermaid-svg-vGeZpU8wCupJmKxl .node polygon,#mermaid-svg-vGeZpU8wCupJmKxl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vGeZpU8wCupJmKxl .rough-node .label text,#mermaid-svg-vGeZpU8wCupJmKxl .node .label text,#mermaid-svg-vGeZpU8wCupJmKxl .image-shape .label,#mermaid-svg-vGeZpU8wCupJmKxl .icon-shape .label{text-anchor:middle;}#mermaid-svg-vGeZpU8wCupJmKxl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vGeZpU8wCupJmKxl .rough-node .label,#mermaid-svg-vGeZpU8wCupJmKxl .node .label,#mermaid-svg-vGeZpU8wCupJmKxl .image-shape .label,#mermaid-svg-vGeZpU8wCupJmKxl .icon-shape .label{text-align:center;}#mermaid-svg-vGeZpU8wCupJmKxl .node.clickable{cursor:pointer;}#mermaid-svg-vGeZpU8wCupJmKxl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vGeZpU8wCupJmKxl .arrowheadPath{fill:#333333;}#mermaid-svg-vGeZpU8wCupJmKxl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vGeZpU8wCupJmKxl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vGeZpU8wCupJmKxl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vGeZpU8wCupJmKxl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vGeZpU8wCupJmKxl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vGeZpU8wCupJmKxl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vGeZpU8wCupJmKxl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vGeZpU8wCupJmKxl .cluster text{fill:#333;}#mermaid-svg-vGeZpU8wCupJmKxl .cluster span{color:#333;}#mermaid-svg-vGeZpU8wCupJmKxl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vGeZpU8wCupJmKxl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vGeZpU8wCupJmKxl rect.text{fill:none;stroke-width:0;}#mermaid-svg-vGeZpU8wCupJmKxl .icon-shape,#mermaid-svg-vGeZpU8wCupJmKxl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vGeZpU8wCupJmKxl .icon-shape p,#mermaid-svg-vGeZpU8wCupJmKxl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vGeZpU8wCupJmKxl .icon-shape .label rect,#mermaid-svg-vGeZpU8wCupJmKxl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vGeZpU8wCupJmKxl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vGeZpU8wCupJmKxl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vGeZpU8wCupJmKxl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 宿主服务器
容器A:你的 agent 程序
想再拉一个容器B ❌
Docker 守护进程 dockerd
问题 :容器里的进程默认摸不到宿主的 Docker 守护进程------这正是隔离生效的表现。
10.2 方案一:挂载 Docker Socket(方便但危险)
yaml
services:
agent:
volumes:
- /var/run/docker.sock:/var/run/docker.sock # ★ 挂进来
⚠️ 致命问题:
能控制 docker.sock = 能控制宿主上的一切。
拿到 socket 可以起 --privileged 容器、挂载宿主根目录、读所有数据。
结论:只适合本地开发。生产别用。
10.3 方案二:DinD 真套娃(不推荐)
在容器 A 里再跑一个独立 dockerd。
- 优点:和宿主隔离
- 缺点:需要
--privileged、性能差、缓存难共享
10.4 ★ 方案三:K8s Job(云原生正统)
核心思路:不拉 Docker 容器,而是创建 K8s 资源(Job/Pod)。
python
from kubernetes import client, config
config.load_incluster_config()
batch_v1 = client.BatchV1Api()
job = client.V1Job(
metadata=client.V1ObjectMeta(name=f"sandbox-{uuid4()}"),
spec=client.V1JobSpec(
template=client.V1PodTemplateSpec(
spec=client.V1PodSpec(
restartPolicy="Never",
containers=[client.V1Container(
name="runner",
image="my-agent-sandbox:latest",
command=["python", code],
)],
)
),
backoffLimit=0,
)
)
batch_v1.create_namespaced_job(namespace="sandbox-ns", body=job)
| 优点 | 说明 |
|---|---|
| 权限可控 | 只需"创建 Job"的 RBAC 权限,不是宿主 root |
| 天然隔离 | 沙箱 Pod 与 agent Pod 是平级,不碰宿主 |
| 资源管控 | resources.limits 限 CPU/内存 |
| 网络策略 | NetworkPolicy 断网 |
| 可伸缩 | 集群资源多就能并发 |
注意:沙箱 Pod 是跑在 agent Pod "旁边",不是"里面"------平级,不是套娃。
10.5 方案四:云沙箱 API(零运维)
用 E2B、Modal、Daytona 这类服务:
python
from e2b import Sandbox
sbx = Sandbox()
result = sbx.run_code(code)
sbx.close()
10.6 选型决策表
| 方案 | 安全性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 挂 docker.sock | 💀 极低 | 低 | 本地开发 |
| DinD 套娃 | 中 | 高 | 老场景,不推荐 |
| K8s Job | ✅ 高 | 中 | 云原生首选 |
| 云沙箱 API | ✅ 高 | 极低 | 不想运维 |
| gVisor/Kata | ✅✅ 极高 | 高 | 多租户 SaaS |
10.7 ★ 一个必须摆正的因果关系
很多人会说"Docker/K8s 支持通过网络传脚本到远端执行 "------这句话结论对,因果要摆正:
不是"Docker/K8s 提供了远程执行脚本的能力",而是"它们提供了执行能力,而这份执行能力本来就可以通过网络调用"。
它们本身确实自带远程能力:
| 技术 | 自带的远程能力 |
|---|---|
| Docker | Daemon 支持 TCP 远程连接 (-H tcp://host:2375,不用本地 socket) |
| K8s | API Server 天生就是网络服务,跨机器操作是本职 |
但三层职责要分清:
#mermaid-svg-josL9bwGeFG9GlaL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-josL9bwGeFG9GlaL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-josL9bwGeFG9GlaL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-josL9bwGeFG9GlaL .error-icon{fill:#552222;}#mermaid-svg-josL9bwGeFG9GlaL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-josL9bwGeFG9GlaL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-josL9bwGeFG9GlaL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-josL9bwGeFG9GlaL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-josL9bwGeFG9GlaL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-josL9bwGeFG9GlaL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-josL9bwGeFG9GlaL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-josL9bwGeFG9GlaL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-josL9bwGeFG9GlaL .marker.cross{stroke:#333333;}#mermaid-svg-josL9bwGeFG9GlaL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-josL9bwGeFG9GlaL p{margin:0;}#mermaid-svg-josL9bwGeFG9GlaL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-josL9bwGeFG9GlaL .cluster-label text{fill:#333;}#mermaid-svg-josL9bwGeFG9GlaL .cluster-label span{color:#333;}#mermaid-svg-josL9bwGeFG9GlaL .cluster-label span p{background-color:transparent;}#mermaid-svg-josL9bwGeFG9GlaL .label text,#mermaid-svg-josL9bwGeFG9GlaL span{fill:#333;color:#333;}#mermaid-svg-josL9bwGeFG9GlaL .node rect,#mermaid-svg-josL9bwGeFG9GlaL .node circle,#mermaid-svg-josL9bwGeFG9GlaL .node ellipse,#mermaid-svg-josL9bwGeFG9GlaL .node polygon,#mermaid-svg-josL9bwGeFG9GlaL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-josL9bwGeFG9GlaL .rough-node .label text,#mermaid-svg-josL9bwGeFG9GlaL .node .label text,#mermaid-svg-josL9bwGeFG9GlaL .image-shape .label,#mermaid-svg-josL9bwGeFG9GlaL .icon-shape .label{text-anchor:middle;}#mermaid-svg-josL9bwGeFG9GlaL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-josL9bwGeFG9GlaL .rough-node .label,#mermaid-svg-josL9bwGeFG9GlaL .node .label,#mermaid-svg-josL9bwGeFG9GlaL .image-shape .label,#mermaid-svg-josL9bwGeFG9GlaL .icon-shape .label{text-align:center;}#mermaid-svg-josL9bwGeFG9GlaL .node.clickable{cursor:pointer;}#mermaid-svg-josL9bwGeFG9GlaL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-josL9bwGeFG9GlaL .arrowheadPath{fill:#333333;}#mermaid-svg-josL9bwGeFG9GlaL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-josL9bwGeFG9GlaL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-josL9bwGeFG9GlaL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-josL9bwGeFG9GlaL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-josL9bwGeFG9GlaL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-josL9bwGeFG9GlaL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-josL9bwGeFG9GlaL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-josL9bwGeFG9GlaL .cluster text{fill:#333;}#mermaid-svg-josL9bwGeFG9GlaL .cluster span{color:#333;}#mermaid-svg-josL9bwGeFG9GlaL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-josL9bwGeFG9GlaL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-josL9bwGeFG9GlaL rect.text{fill:none;stroke-width:0;}#mermaid-svg-josL9bwGeFG9GlaL .icon-shape,#mermaid-svg-josL9bwGeFG9GlaL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-josL9bwGeFG9GlaL .icon-shape p,#mermaid-svg-josL9bwGeFG9GlaL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-josL9bwGeFG9GlaL .icon-shape .label rect,#mermaid-svg-josL9bwGeFG9GlaL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-josL9bwGeFG9GlaL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-josL9bwGeFG9GlaL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-josL9bwGeFG9GlaL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ 你的应用层(业务语义)
借用
操作
① 执行环境层(本地能力)
容器运行时
真正跑代码
② 远程接口层(网络能力)
Docker Daemon TCP
K8s API Server
沙箱服务 /execute
'传脚本→跑→返回'
| 层 | 职责 | 是"网络"的吗 |
|---|---|---|
| 容器运行时 | 提供执行环境 | 本地能力 |
| 远程接口(Daemon TCP / K8s API) | 让环境可远程调用 | 网络能力 |
| 你的应用(沙箱服务) | 把"传脚本→执行→返回"封装成业务接口 | 业务能力 |
Docker/K8s 覆盖 ① 和 ②;"传脚本执行"这个语义在 ③,是你加的。
⚠️ 而且它们的原生接口是"控制面接口",极度危险:
| 直接暴露 | 后果 |
|---|---|
| Docker Daemon 2375 端口 | 等于把宿主 root 权限给了全网 |
| K8s API 无鉴权 | 谁都能删你的集群 |
所以现实里没人裸暴露它们,而是在上面包一层受控的"沙箱服务",对外只暴露
/execute。 这正是第十二部分要讲的东西。
精确措辞建议(写博客时用):
- ❌ 不准确:"Docker/K8s 支持网络传输脚本去执行"
- ✅ 准确:"Docker/K8s 提供可远程调用的执行环境 ;在其之上可以构建网络代码执行服务"
第十一部分:沙箱的依赖管理
11.1 Pod 环境也是提前定义的
Pod = 容器组合,容器 = 从镜像实例化。环境全靠镜像定义。
yaml
spec:
containers:
- name: runner
image: my-sandbox:latest # ★ 环境全在这个镜像里
11.2 依赖不全怎么办------四层解法
| 方案 | 做法 | 优劣 |
|---|---|---|
| 1. 预装大全套 | 常用依赖全塞镜像 | 开箱即用;镜像巨大、总有漏 |
| 2. 多镜像分层 | 按场景准备多个镜像按需选 | 主流做法 |
| 3. 动态构建镜像 | 按请求依赖临时 build | 最灵活;首次慢 |
| 4. 错误喂回 LLM | 让 LLM 自己改代码补依赖 | ★ agent 特有的自愈 |
11.3 动态构建镜像:不是每次真 build
核心洞察:用"依赖清单的哈希"当镜像 tag,命中就复用,没命中才 build。
python
import hashlib
def get_or_build_image(requirements):
# 1. 依赖清单排序后算哈希 → 当 tag
key = hashlib.md5("|".join(sorted(requirements)).encode()).hexdigest()[:12]
tag = f"sandbox-dep-{key}"
# 2. 先查有没有
if image_exists(tag):
return tag # ★ 命中!零 build 成本
# 3. 没有才真 build
build_image(requirements, tag)
return tag
效果:
| 请求 | 依赖清单 | 结果 |
|---|---|---|
| 第 1 次 | numpy, pandas | build → sandbox-dep-a1b2c3 |
| 第 2 次 | numpy, pandas | 哈希相同 → 秒复用 |
| 第 3 次 | pandas, numpy | 排序后哈希相同 → 还是复用! |
| 第 4 次 | numpy, pandas, scipy | 哈希不同 → 新 build |
缓存的 key 不是"请求",而是"依赖集合"------相同依赖组合只 build 一次。
11.4 和 Maven 依赖缓存是同一个套路
| Maven | 动态 build 镜像 |
|---|---|
pom.xml 没变 → 用 ~/.m2 缓存 |
依赖清单没变 → 用已有镜像 tag |
pom.xml 变了 → 重新下载 |
依赖清单变了 → 重新 build |
| 依赖坐标算 key | 依赖列表算哈希当 tag |
11.5 Docker 分层缓存
build 镜像 = 分层叠加
├─ 基础层 FROM python:3.11 ← 已存在,直接复用(不重建)
├─ 依赖层 RUN pip install xxx ← ★ 只有这层是新的
└─ 结果:一个新镜像 tag
只要 pip install 那行没变,Docker 直接复用上次的层,秒过。
11.6 动态 build 完整流程
#mermaid-svg-gdmUCkHE6d3zS3bc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gdmUCkHE6d3zS3bc .error-icon{fill:#552222;}#mermaid-svg-gdmUCkHE6d3zS3bc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gdmUCkHE6d3zS3bc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gdmUCkHE6d3zS3bc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gdmUCkHE6d3zS3bc .marker.cross{stroke:#333333;}#mermaid-svg-gdmUCkHE6d3zS3bc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gdmUCkHE6d3zS3bc p{margin:0;}#mermaid-svg-gdmUCkHE6d3zS3bc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc .cluster-label text{fill:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc .cluster-label span{color:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc .cluster-label span p{background-color:transparent;}#mermaid-svg-gdmUCkHE6d3zS3bc .label text,#mermaid-svg-gdmUCkHE6d3zS3bc span{fill:#333;color:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc .node rect,#mermaid-svg-gdmUCkHE6d3zS3bc .node circle,#mermaid-svg-gdmUCkHE6d3zS3bc .node ellipse,#mermaid-svg-gdmUCkHE6d3zS3bc .node polygon,#mermaid-svg-gdmUCkHE6d3zS3bc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gdmUCkHE6d3zS3bc .rough-node .label text,#mermaid-svg-gdmUCkHE6d3zS3bc .node .label text,#mermaid-svg-gdmUCkHE6d3zS3bc .image-shape .label,#mermaid-svg-gdmUCkHE6d3zS3bc .icon-shape .label{text-anchor:middle;}#mermaid-svg-gdmUCkHE6d3zS3bc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gdmUCkHE6d3zS3bc .rough-node .label,#mermaid-svg-gdmUCkHE6d3zS3bc .node .label,#mermaid-svg-gdmUCkHE6d3zS3bc .image-shape .label,#mermaid-svg-gdmUCkHE6d3zS3bc .icon-shape .label{text-align:center;}#mermaid-svg-gdmUCkHE6d3zS3bc .node.clickable{cursor:pointer;}#mermaid-svg-gdmUCkHE6d3zS3bc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gdmUCkHE6d3zS3bc .arrowheadPath{fill:#333333;}#mermaid-svg-gdmUCkHE6d3zS3bc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gdmUCkHE6d3zS3bc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gdmUCkHE6d3zS3bc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gdmUCkHE6d3zS3bc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gdmUCkHE6d3zS3bc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gdmUCkHE6d3zS3bc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gdmUCkHE6d3zS3bc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gdmUCkHE6d3zS3bc .cluster text{fill:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc .cluster span{color:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gdmUCkHE6d3zS3bc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gdmUCkHE6d3zS3bc rect.text{fill:none;stroke-width:0;}#mermaid-svg-gdmUCkHE6d3zS3bc .icon-shape,#mermaid-svg-gdmUCkHE6d3zS3bc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gdmUCkHE6d3zS3bc .icon-shape p,#mermaid-svg-gdmUCkHE6d3zS3bc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gdmUCkHE6d3zS3bc .icon-shape .label rect,#mermaid-svg-gdmUCkHE6d3zS3bc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gdmUCkHE6d3zS3bc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gdmUCkHE6d3zS3bc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gdmUCkHE6d3zS3bc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
没有
请求: code + requirements=numpy,pandas
① 排序算哈希 → tag=sandbox-dep-a1b2c3
② 仓库有这个 tag 吗?
③ 直接复用 ✅
④ build 新镜像
⑤ 推入仓库供下次复用
⑥ 用该镜像拉 Pod 跑代码
11.7 大全套 vs 动态 build
| 大全套镜像 | 动态 build | |
|---|---|---|
| 镜像体积 | 几 GB | 按需,几百 MB |
| 启动速度 | 慢 | 快 |
| 灵活度 | 低(总有漏) | 高(要啥装啥) |
| 存储成本 | 高 | 低 |
| 首次准备 | 一次搞定 | 首次略慢 |
主流是"预置常用 + 动态兜底 + LLM 自愈"三件套组合。
第十二部分:远程沙箱服务
12.1 需求:agent 不一定能拉起本地容器
现实里常有两种困境:
- agent 自己跑在容器里,不想挂
docker.sock(危险) - agent 所在机器没有 Docker / 没有权限
解法:把"拉起容器"这件事,从 agent 本机搬到一个专用服务上------本地另一台机器、或内网的一台"私有沙箱服务器"。
┌──────────────┐ 网络 ┌──────────────────┐
│ agent 程序 │ ─────HTTP/gRPC───► │ 沙箱服务器(你的) │
│ (哪都行) │ ◄────返回结果───── │ 跑 Docker 容器 │
└──────────────┘ └──────────────────┘
这在架构上叫"远程代码执行服务"(Remote Code Execution Service)------云端 E2B 那套逻辑的私有化版本。
12.2 为什么值得这么做
| 动机 | 说明 |
|---|---|
| 安全解耦 | agent 本身不需要 Docker 权限,宿主更干净 |
| 资源集中 | 沙箱机器统一配好、装好依赖、集中管理 |
| 规避 DinD | 直接绕开"容器里拉容器"的难题 |
特别是第三条 ------remote sandbox 是"agent 跑在容器里"这一困境的最优雅解法:agent 容器只需发 HTTPS 请求,不需要任何本地容器运行时权限。
12.3 实现方案(从简到繁)
方案 1:最简------HTTP 接口 + Docker SDK
python
# ===== 沙箱服务器 sandbox_server.py =====
from fastapi import FastAPI
from pydantic import BaseModel
import docker
app = FastAPI()
client = docker.from_env()
class ExecRequest(BaseModel):
code: str
language: str = "python"
requirements: list[str] = []
timeout: int = 30
@app.post("/execute")
def execute(req: ExecRequest):
container = client.containers.run(
"my-sandbox:latest",
command=["python", "-c", req.code],
network_disabled=True, mem_limit="256m",
cpu_quota=50000, read_only=True,
remove=True, detach=False,
)
return {"output": container.decode("utf-8")}
python
# ===== agent 侧 =====
import requests
def run_code_remote(code):
r = requests.post("http://sandbox-host:8000/execute",
json={"code": code, "language": "python"}, timeout=60)
return r.json()["output"]
本质就是把"本地 containers.run()"换成"发个 HTTP 请求,让另一台机器 containers.run()"。
方案 2:异步 + 任务队列(生产级)
同步 HTTP 有超时限制,长任务会断。生产做法用队列:
Docker 沙箱Worker 消息队列(Redis/RabbitMQ) agent程序 Docker 沙箱Worker 消息队列(Redis/RabbitMQ) agent程序 #mermaid-svg-R68Xyeg0y0MOXWT9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R68Xyeg0y0MOXWT9 .error-icon{fill:#552222;}#mermaid-svg-R68Xyeg0y0MOXWT9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R68Xyeg0y0MOXWT9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R68Xyeg0y0MOXWT9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R68Xyeg0y0MOXWT9 .marker.cross{stroke:#333333;}#mermaid-svg-R68Xyeg0y0MOXWT9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R68Xyeg0y0MOXWT9 p{margin:0;}#mermaid-svg-R68Xyeg0y0MOXWT9 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-R68Xyeg0y0MOXWT9 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-R68Xyeg0y0MOXWT9 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-R68Xyeg0y0MOXWT9 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-R68Xyeg0y0MOXWT9 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-R68Xyeg0y0MOXWT9 .sequenceNumber{fill:white;}#mermaid-svg-R68Xyeg0y0MOXWT9 #sequencenumber{fill:#333;}#mermaid-svg-R68Xyeg0y0MOXWT9 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-R68Xyeg0y0MOXWT9 .messageText{fill:#333;stroke:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-R68Xyeg0y0MOXWT9 .labelText,#mermaid-svg-R68Xyeg0y0MOXWT9 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .loopText,#mermaid-svg-R68Xyeg0y0MOXWT9 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-R68Xyeg0y0MOXWT9 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-R68Xyeg0y0MOXWT9 .noteText,#mermaid-svg-R68Xyeg0y0MOXWT9 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-R68Xyeg0y0MOXWT9 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-R68Xyeg0y0MOXWT9 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-R68Xyeg0y0MOXWT9 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-R68Xyeg0y0MOXWT9 .actorPopupMenu{position:absolute;}#mermaid-svg-R68Xyeg0y0MOXWT9 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-R68Xyeg0y0MOXWT9 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-R68Xyeg0y0MOXWT9 .actor-man circle,#mermaid-svg-R68Xyeg0y0MOXWT9 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-R68Xyeg0y0MOXWT9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 提交任务(code, deps)分发任务拉容器执行结果写回结果轮询/订阅结果返回结果
好处:解耦、可并发、可重试、支持长任务。Celery / RQ / 自建均可。
方案 3:★ 容器池------解决冷启动
每次新建容器慢(几百毫秒~秒级)。改进:在沙箱服务器上预热一批容器待命。
python
class ContainerPool:
def __init__(self, size=5, image="my-sandbox:latest"):
self.image = image
self.pool = [self._spawn() for _ in range(size)]
def _spawn(self):
return client.containers.create( # 只创建,不启动
self.image, command=["sleep", "infinity"],
network_disabled=True, mem_limit="256m",
)
def acquire(self):
return self.pool.pop() if self.pool else self._spawn()
def release(self, c):
c.remove(force=True) # ★ 用完销毁,不复用同一容器
self.pool.append(self._spawn()) # 补一个新的
关键:池化的是"启动开销",不是"运行状态"。用完的容器必须销毁重建------不同用户的代码不能共享同一容器。
方案 4:远程 K8s------最彻底的解耦
沙箱服务器本身是 K8s 集群时,agent 只需调 K8s API 起沙箱 Pod,连 Docker 都不用碰。
12.4 ★ 生产级设计要点
远程沙箱不只是"发个 HTTP",真功夫在这些:
| 要点 | 做法 |
|---|---|
| 认证 | agent 与沙箱服务器之间必须有 token/mTLS,否则谁都能调你的沙箱跑挖矿 |
| 隔离 | 沙箱服务器部署在内网 ,不对外暴露 |
| 依赖 | 服务器上预置好镜像(见第十一部分) |
| 超时 | 服务端强制超时杀容器 |
| 输出限制 | 限制返回大小,防撑爆 agent |
| 并发控制 | 限制同时运行的容器数,防打满机器 |
| 清理 | 定时清理残留容器 |
| 文件传输 | 设计"传文件进/取文件出"接口(见第十三部分) |
⚠️ 别把沙箱服务暴露到公网。它是"能跑任意代码"的服务,裸奔 = 给别人一台肉鸡。
12.5 完整架构图
#mermaid-svg-CQCrpKlVkTzCkan3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CQCrpKlVkTzCkan3 .error-icon{fill:#552222;}#mermaid-svg-CQCrpKlVkTzCkan3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CQCrpKlVkTzCkan3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CQCrpKlVkTzCkan3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CQCrpKlVkTzCkan3 .marker.cross{stroke:#333333;}#mermaid-svg-CQCrpKlVkTzCkan3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CQCrpKlVkTzCkan3 p{margin:0;}#mermaid-svg-CQCrpKlVkTzCkan3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 .cluster-label text{fill:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 .cluster-label span{color:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 .cluster-label span p{background-color:transparent;}#mermaid-svg-CQCrpKlVkTzCkan3 .label text,#mermaid-svg-CQCrpKlVkTzCkan3 span{fill:#333;color:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 .node rect,#mermaid-svg-CQCrpKlVkTzCkan3 .node circle,#mermaid-svg-CQCrpKlVkTzCkan3 .node ellipse,#mermaid-svg-CQCrpKlVkTzCkan3 .node polygon,#mermaid-svg-CQCrpKlVkTzCkan3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CQCrpKlVkTzCkan3 .rough-node .label text,#mermaid-svg-CQCrpKlVkTzCkan3 .node .label text,#mermaid-svg-CQCrpKlVkTzCkan3 .image-shape .label,#mermaid-svg-CQCrpKlVkTzCkan3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-CQCrpKlVkTzCkan3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CQCrpKlVkTzCkan3 .rough-node .label,#mermaid-svg-CQCrpKlVkTzCkan3 .node .label,#mermaid-svg-CQCrpKlVkTzCkan3 .image-shape .label,#mermaid-svg-CQCrpKlVkTzCkan3 .icon-shape .label{text-align:center;}#mermaid-svg-CQCrpKlVkTzCkan3 .node.clickable{cursor:pointer;}#mermaid-svg-CQCrpKlVkTzCkan3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CQCrpKlVkTzCkan3 .arrowheadPath{fill:#333333;}#mermaid-svg-CQCrpKlVkTzCkan3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CQCrpKlVkTzCkan3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CQCrpKlVkTzCkan3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CQCrpKlVkTzCkan3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CQCrpKlVkTzCkan3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CQCrpKlVkTzCkan3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CQCrpKlVkTzCkan3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CQCrpKlVkTzCkan3 .cluster text{fill:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 .cluster span{color:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CQCrpKlVkTzCkan3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CQCrpKlVkTzCkan3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-CQCrpKlVkTzCkan3 .icon-shape,#mermaid-svg-CQCrpKlVkTzCkan3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CQCrpKlVkTzCkan3 .icon-shape p,#mermaid-svg-CQCrpKlVkTzCkan3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CQCrpKlVkTzCkan3 .icon-shape .label rect,#mermaid-svg-CQCrpKlVkTzCkan3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CQCrpKlVkTzCkan3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CQCrpKlVkTzCkan3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CQCrpKlVkTzCkan3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 沙箱服务器(内网)
Agent 所在机器
① 提交代码
④ 返回结果
② 借出容器执行
③ 结果
Docker 运行时
沙箱容器1
断网/限资源
沙箱容器2
沙箱容器3
agent 程序
沙箱客户端
(HTTP/gRPC SDK)
沙箱 API 服务
(FastAPI/认证)
容器池
(预热待命)
本质上,你等于把"云端 agent 平台"的沙箱服务,自己私有化部署了一台。
第十三部分:产物回传------沙箱产生的文件怎么带回
13.1 先给产物分类(别一上来就想"传文件")
| 产物类型 | 例子 | 大小 | 传输方式 |
|---|---|---|---|
| ① 标准输出 | print、日志 | 小 | stdout 直接带回 |
| ② 返回值 | 函数 return | 小 | 约定输出通道 |
| ③ 生成的文件 | CSV、图片、Excel | 中~大 | ★ 需要文件传输机制 |
| ④ 目录结构 | 一堆文件、多级目录 | 大 | 打包/挂载 |
| ⑤ 副作用 | 改了数据库、发请求 | --- | 不传,直接生效 |
①② 是"文本结果",⑤ 是"世界已变",只有 ③④ 才真正需要"传回"。
13.2 ★ 两种"传回"范式(记住它俩,后面都是变体)
范式 A:拉取式(Pull)------产物落在共享卷,agent 去取
沙箱容器 → 写到挂载目录 → agent 读该目录
范式 B:推送式(Push)------沙箱主动把产物发出去
沙箱容器 → 调上传接口 → 传到对象存储/回调地址
| 拉取式 (Pull) | 推送式 (Push) | |
|---|---|---|
| 方向 | agent 主动去取 | 沙箱主动送出 |
| 依赖 | 共享存储 | 网络回调 |
| 适合 | agent 和沙箱能共享文件系统 | agent 和沙箱只能靠网络 |
| 典型 | K8s 挂 PVC / volume | 上传到 S3 / 回调 webhook |
13.3 方案全景
方案 1:共享卷挂载(拉取式)------ 同机/集群内首选
python
client.containers.run(
"my-sandbox:latest",
command=["python", "-c", code],
volumes={"/host/sandbox_out": {"bind": "/output", "mode": "rw"}},
# ↑ 宿主目录 ↑ 容器内目录
)
# 约定: 代码把产物写到 /output;跑完 agent 读宿主对应目录
K8s 场景用 PVC 共享卷同理。
优点 :简单、快、不传大文件。
缺点 :要求 agent 和沙箱能共享文件系统(同机 or 同集群)。
方案 2:docker cp(拉取式)------ 本地 Docker 专用
python
container = client.containers.run("my-sandbox:latest",
command=["python", "-c", code],
detach=True) # ★ 后台跑,不自动删
container.wait()
subprocess.run(["docker", "cp", f"{container.id}:/output", "./local_out"])
container.remove()
注意:必须先别删容器。只适合本地 Docker。
方案 3:对象存储上传(推送式)------ 分布式/远程首选
沙箱跑完,代码自己把产物上传到对象存储(S3/MinIO),agent 从对象存储下载。
优点 :跨网络、跨机房都能用,天然适合分布式。
缺点 :沙箱需要网络出口------破坏"默认断网"的隔离 ,必须做白名单(只放行对象存储)。
方案 4:webhook 回调(推送式)------ 长任务/异步首选
沙箱跑完主动回调 agent 的一个地址,适合异步长任务。
方案 5:base64 内联(推送式)------ 小文件专用
产物很小(几十 KB 内),直接 base64 编码塞进 stdout。
优点 :零基础设施。
缺点:只适合小文件(大文件撑爆内存)。
13.4 选型决策表
| 场景 | 推荐方案 |
|---|---|
| 只返回文本结果 | 直接 stdout |
| 本地 Docker + 有产物 | 共享卷挂载 或 docker cp |
| K8s 集群内 | PVC 共享卷 |
| 沙箱和 agent 不在同一存储域 | 对象存储 |
| 异步长任务 | webhook 回调 |
| 产物很小 | base64 内联 |
13.5 ★ 实战要点(翻车重灾区)
| 要点 | 说明 |
|---|---|
| 约定输出目录 | 统一约定"产物放 /output",别让代码随便写 |
| 大小限制 | 必须限产物大小,否则一个 10GB 文件传死你 |
| 清理 | 传完要清理沙箱内临时产物,防泄漏给下一个人 |
| 命名冲突 | 多任务并发时用 task_id 隔离目录 |
| 断网悖论 | 推送式要联网,和"默认断网"冲突------做白名单出口 |
| 格式约定 | 输出"结构化清单"(JSON 列出产物路径+类型),别让 agent 猜 |
| 异步适配 | 长任务别用同步 HTTP 等,会超时断开 |
13.6 ★ 推荐的"产物回传协议"
正规做法是约定一套产物协议,不靠猜:
json
{
"status": "success",
"stdout": "...",
"stderr": "",
"artifacts": [
{"name": "result.csv", "type": "file", "size": 2048, "path": "task_123/result.csv"},
{"name": "chart.png", "type": "image", "size": 51200, "path": "task_123/chart.png"}
],
"artifacts_uri": {
"type": "s3", "bucket": "sandbox-output", "prefix": "task_123/"
}
}
agent 拿到结构就知道:有哪些产物、多大、去哪取。
13.7 一张图总收口
#mermaid-svg-I8jjW2NdzSpcgT33{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I8jjW2NdzSpcgT33 .error-icon{fill:#552222;}#mermaid-svg-I8jjW2NdzSpcgT33 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I8jjW2NdzSpcgT33 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I8jjW2NdzSpcgT33 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I8jjW2NdzSpcgT33 .marker.cross{stroke:#333333;}#mermaid-svg-I8jjW2NdzSpcgT33 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I8jjW2NdzSpcgT33 p{margin:0;}#mermaid-svg-I8jjW2NdzSpcgT33 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 .cluster-label text{fill:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 .cluster-label span{color:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 .cluster-label span p{background-color:transparent;}#mermaid-svg-I8jjW2NdzSpcgT33 .label text,#mermaid-svg-I8jjW2NdzSpcgT33 span{fill:#333;color:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 .node rect,#mermaid-svg-I8jjW2NdzSpcgT33 .node circle,#mermaid-svg-I8jjW2NdzSpcgT33 .node ellipse,#mermaid-svg-I8jjW2NdzSpcgT33 .node polygon,#mermaid-svg-I8jjW2NdzSpcgT33 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I8jjW2NdzSpcgT33 .rough-node .label text,#mermaid-svg-I8jjW2NdzSpcgT33 .node .label text,#mermaid-svg-I8jjW2NdzSpcgT33 .image-shape .label,#mermaid-svg-I8jjW2NdzSpcgT33 .icon-shape .label{text-anchor:middle;}#mermaid-svg-I8jjW2NdzSpcgT33 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-I8jjW2NdzSpcgT33 .rough-node .label,#mermaid-svg-I8jjW2NdzSpcgT33 .node .label,#mermaid-svg-I8jjW2NdzSpcgT33 .image-shape .label,#mermaid-svg-I8jjW2NdzSpcgT33 .icon-shape .label{text-align:center;}#mermaid-svg-I8jjW2NdzSpcgT33 .node.clickable{cursor:pointer;}#mermaid-svg-I8jjW2NdzSpcgT33 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-I8jjW2NdzSpcgT33 .arrowheadPath{fill:#333333;}#mermaid-svg-I8jjW2NdzSpcgT33 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I8jjW2NdzSpcgT33 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I8jjW2NdzSpcgT33 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I8jjW2NdzSpcgT33 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-I8jjW2NdzSpcgT33 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I8jjW2NdzSpcgT33 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-I8jjW2NdzSpcgT33 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I8jjW2NdzSpcgT33 .cluster text{fill:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 .cluster span{color:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I8jjW2NdzSpcgT33 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-I8jjW2NdzSpcgT33 rect.text{fill:none;stroke-width:0;}#mermaid-svg-I8jjW2NdzSpcgT33 .icon-shape,#mermaid-svg-I8jjW2NdzSpcgT33 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I8jjW2NdzSpcgT33 .icon-shape p,#mermaid-svg-I8jjW2NdzSpcgT33 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-I8jjW2NdzSpcgT33 .icon-shape .label rect,#mermaid-svg-I8jjW2NdzSpcgT33 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I8jjW2NdzSpcgT33 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-I8jjW2NdzSpcgT33 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-I8jjW2NdzSpcgT33 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} agent
沙箱容器
共享卷挂载
S3/MinIO
HTTP 回调
跑代码
产物写到 /output
或: 上传到对象存储
读共享卷(拉取)
从对象存储下载(拉取)
接收 webhook(推送)
第十四部分:沙箱执行框架
14.1 什么是"沙箱执行框架"
沙箱执行框架 = 一层"包在用户代码外面"的固定逻辑,负责处理所有"用户代码不该关心、但必须做"的事。
它管的不只是上传,而是用户代码的整个生命周期:
| 框架负责的杂务 | 说明 |
|---|---|
| 准备环境 | 放好输入文件、切到工作目录 |
| 跑用户代码 | 真正 exec 用户脚本 |
| 捕获输出 | 收集 stdout/stderr/返回值 |
| 收集产物 | 扫描 /output 目录 |
| 上传产物 | 传到 MinIO/S3(第十三部分) |
| 限时限资源 | 超时杀、限内存 |
| 清理 | 销毁容器、清临时文件 |
| 组装结果 | 打包成结构化 JSON 返回 |
14.2 用户代码 vs 框架代码
#mermaid-svg-Po2T3vewzW515uRh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Po2T3vewzW515uRh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Po2T3vewzW515uRh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Po2T3vewzW515uRh .error-icon{fill:#552222;}#mermaid-svg-Po2T3vewzW515uRh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Po2T3vewzW515uRh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Po2T3vewzW515uRh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Po2T3vewzW515uRh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Po2T3vewzW515uRh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Po2T3vewzW515uRh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Po2T3vewzW515uRh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Po2T3vewzW515uRh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Po2T3vewzW515uRh .marker.cross{stroke:#333333;}#mermaid-svg-Po2T3vewzW515uRh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Po2T3vewzW515uRh p{margin:0;}#mermaid-svg-Po2T3vewzW515uRh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Po2T3vewzW515uRh .cluster-label text{fill:#333;}#mermaid-svg-Po2T3vewzW515uRh .cluster-label span{color:#333;}#mermaid-svg-Po2T3vewzW515uRh .cluster-label span p{background-color:transparent;}#mermaid-svg-Po2T3vewzW515uRh .label text,#mermaid-svg-Po2T3vewzW515uRh span{fill:#333;color:#333;}#mermaid-svg-Po2T3vewzW515uRh .node rect,#mermaid-svg-Po2T3vewzW515uRh .node circle,#mermaid-svg-Po2T3vewzW515uRh .node ellipse,#mermaid-svg-Po2T3vewzW515uRh .node polygon,#mermaid-svg-Po2T3vewzW515uRh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Po2T3vewzW515uRh .rough-node .label text,#mermaid-svg-Po2T3vewzW515uRh .node .label text,#mermaid-svg-Po2T3vewzW515uRh .image-shape .label,#mermaid-svg-Po2T3vewzW515uRh .icon-shape .label{text-anchor:middle;}#mermaid-svg-Po2T3vewzW515uRh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Po2T3vewzW515uRh .rough-node .label,#mermaid-svg-Po2T3vewzW515uRh .node .label,#mermaid-svg-Po2T3vewzW515uRh .image-shape .label,#mermaid-svg-Po2T3vewzW515uRh .icon-shape .label{text-align:center;}#mermaid-svg-Po2T3vewzW515uRh .node.clickable{cursor:pointer;}#mermaid-svg-Po2T3vewzW515uRh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Po2T3vewzW515uRh .arrowheadPath{fill:#333333;}#mermaid-svg-Po2T3vewzW515uRh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Po2T3vewzW515uRh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Po2T3vewzW515uRh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Po2T3vewzW515uRh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Po2T3vewzW515uRh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Po2T3vewzW515uRh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Po2T3vewzW515uRh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Po2T3vewzW515uRh .cluster text{fill:#333;}#mermaid-svg-Po2T3vewzW515uRh .cluster span{color:#333;}#mermaid-svg-Po2T3vewzW515uRh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Po2T3vewzW515uRh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Po2T3vewzW515uRh rect.text{fill:none;stroke-width:0;}#mermaid-svg-Po2T3vewzW515uRh .icon-shape,#mermaid-svg-Po2T3vewzW515uRh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Po2T3vewzW515uRh .icon-shape p,#mermaid-svg-Po2T3vewzW515uRh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Po2T3vewzW515uRh .icon-shape .label rect,#mermaid-svg-Po2T3vewzW515uRh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Po2T3vewzW515uRh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Po2T3vewzW515uRh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Po2T3vewzW515uRh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 沙箱执行框架(固定不变)
用户代码(每次不同、不可信)
import pandas...
df.to_csv('/output/x.csv')
2. 捕获 stdout/stderr
3. 扫描 /output
4. 上传 MinIO
5. 组装结构化结果
6. 清理
- 准备: 放输入、切目录
框架是不变的"壳",用户代码是每次变的"芯"。
14.3 ★ 框架写在哪儿------它其实是"跨层"的
准确答案:框架不是"一个文件",而是一套分散在多层的逻辑。
| 落点 | 具体位置 | 形态 |
|---|---|---|
| ① 镜像内的脚本 | 镜像里 /framework/runner.py |
最常见 |
| ② 镜像的 ENTRYPOINT/CMD | Dockerfile 指定 | 最"原生" |
| ③ 独立的 sidecar 容器 | K8s 里另一个容器 | 最解耦 |
| ④ 沙箱服务端(容器外) | 沙箱服务器的编排代码里 | 服务侧 |
#mermaid-svg-3mwTbsb5io97B9Qj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3mwTbsb5io97B9Qj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3mwTbsb5io97B9Qj .error-icon{fill:#552222;}#mermaid-svg-3mwTbsb5io97B9Qj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3mwTbsb5io97B9Qj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3mwTbsb5io97B9Qj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3mwTbsb5io97B9Qj .marker.cross{stroke:#333333;}#mermaid-svg-3mwTbsb5io97B9Qj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3mwTbsb5io97B9Qj p{margin:0;}#mermaid-svg-3mwTbsb5io97B9Qj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3mwTbsb5io97B9Qj .cluster-label text{fill:#333;}#mermaid-svg-3mwTbsb5io97B9Qj .cluster-label span{color:#333;}#mermaid-svg-3mwTbsb5io97B9Qj .cluster-label span p{background-color:transparent;}#mermaid-svg-3mwTbsb5io97B9Qj .label text,#mermaid-svg-3mwTbsb5io97B9Qj span{fill:#333;color:#333;}#mermaid-svg-3mwTbsb5io97B9Qj .node rect,#mermaid-svg-3mwTbsb5io97B9Qj .node circle,#mermaid-svg-3mwTbsb5io97B9Qj .node ellipse,#mermaid-svg-3mwTbsb5io97B9Qj .node polygon,#mermaid-svg-3mwTbsb5io97B9Qj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3mwTbsb5io97B9Qj .rough-node .label text,#mermaid-svg-3mwTbsb5io97B9Qj .node .label text,#mermaid-svg-3mwTbsb5io97B9Qj .image-shape .label,#mermaid-svg-3mwTbsb5io97B9Qj .icon-shape .label{text-anchor:middle;}#mermaid-svg-3mwTbsb5io97B9Qj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3mwTbsb5io97B9Qj .rough-node .label,#mermaid-svg-3mwTbsb5io97B9Qj .node .label,#mermaid-svg-3mwTbsb5io97B9Qj .image-shape .label,#mermaid-svg-3mwTbsb5io97B9Qj .icon-shape .label{text-align:center;}#mermaid-svg-3mwTbsb5io97B9Qj .node.clickable{cursor:pointer;}#mermaid-svg-3mwTbsb5io97B9Qj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3mwTbsb5io97B9Qj .arrowheadPath{fill:#333333;}#mermaid-svg-3mwTbsb5io97B9Qj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3mwTbsb5io97B9Qj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3mwTbsb5io97B9Qj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3mwTbsb5io97B9Qj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3mwTbsb5io97B9Qj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3mwTbsb5io97B9Qj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3mwTbsb5io97B9Qj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3mwTbsb5io97B9Qj .cluster text{fill:#333;}#mermaid-svg-3mwTbsb5io97B9Qj .cluster span{color:#333;}#mermaid-svg-3mwTbsb5io97B9Qj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3mwTbsb5io97B9Qj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3mwTbsb5io97B9Qj rect.text{fill:none;stroke-width:0;}#mermaid-svg-3mwTbsb5io97B9Qj .icon-shape,#mermaid-svg-3mwTbsb5io97B9Qj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3mwTbsb5io97B9Qj .icon-shape p,#mermaid-svg-3mwTbsb5io97B9Qj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3mwTbsb5io97B9Qj .icon-shape .label rect,#mermaid-svg-3mwTbsb5io97B9Qj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3mwTbsb5io97B9Qj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3mwTbsb5io97B9Qj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3mwTbsb5io97B9Qj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} sidecar(另一个容器)
上传器
让凭证远离不可信代码
沙箱镜像内
runner.py
准备/跑/捕获/收集
沙箱服务端(容器外)
起容器 / 传参 / 收结果 / 清理
14.4 形态①:镜像内的 runner 脚本(最直观)
dockerfile
# Dockerfile.sandbox
FROM python:3.11-slim
RUN pip install pandas boto3
# ★ 框架代码打进镜像
COPY framework/runner.py /framework/runner.py
# ★ 镜像的默认入口就是框架
ENTRYPOINT ["python", "/framework/runner.py"]
python
# /framework/runner.py ------ 镜像里,用户看不到也改不了
import subprocess, os, json, glob
import boto3
def main():
code_path = os.environ["USER_CODE_PATH"] # 用户代码路径,由沙箱服务传入
os.makedirs("/output", exist_ok=True) # ① 准备
proc = subprocess.run( # ② 跑用户代码
["python", code_path], capture_output=True,
text=True, timeout=30, cwd="/work",
)
artifacts = [] # ③ 收集 + ④ 上传
for f in glob.glob("/output/**/*", recursive=True):
if os.path.isfile(f):
key = f"{os.environ['TASK_ID']}/{os.path.basename(f)}"
upload_to_minio(f, key)
artifacts.append({"name": os.path.basename(f), "path": key})
print(json.dumps({ # ⑤ 组装结果
"status": "ok" if proc.returncode == 0 else "error",
"stdout": proc.stdout, "stderr": proc.stderr, "artifacts": artifacts,
}))
main()
沙箱服务这样起容器:
python
client.containers.run(
"my-sandbox:latest",
environment={
"USER_CODE_PATH": "/code/user.py",
"TASK_ID": task_id,
"S3_ENDPOINT": "http://minio:9000",
"S3_KEY": "...", "S3_SECRET": "...",
},
volumes={"/host/codes": {"bind": "/code", "mode": "ro"}}, # 用户代码挂进来
)
分工清晰:镜像里有框架(runner.py);沙箱服务负责挂代码、传环境变量、起容器、收结果。
14.5 形态③:sidecar(框架在另一个容器里)
框架逻辑不放进沙箱镜像,而是放在旁边的 sidecar:
yaml
containers:
- name: runner # 只跑用户代码,镜像可极简
image: my-sandbox:latest
volumeMounts:
- { name: output, mountPath: /output }
- name: uploader # ★ 框架的一部分在这
image: minio/mc:latest
command: ["sh", "-c", "while true; do mc mirror /output/ myminio/sandbox-output/$TASK_ID/; sleep 2; done"]
volumeMounts:
- { name: output, mountPath: /output } # ★ 共享同一个卷
volumes:
- name: output
emptyDir: {}
sidecar 模式最安全------跑用户代码的主容器根本没有任何 S3 凭证。
14.6 ★ 为什么上传逻辑绝不能写在用户脚本里
假设让用户/模型生成的脚本自己调 s3.upload():
| 问题 | 说明 |
|---|---|
| 污染业务代码 | 用户只想算数据,却要写一堆上传样板 |
| 模型容易写错 | LLM 每次生成 S3 上传代码,参数/凭证易错 |
| 凭证泄露 | 脚本里得塞 access key------等于把凭证给了不可信代码 |
| 依赖耦合 | 脚本得装 boto3,还得知道 bucket/endpoint |
| 无法统一管控 | 大小限制、路径规范、清理策略全乱 |
| 破坏断网隔离 | 每个脚本都要开网络出口 |
核心矛盾:脚本是"不可信、临时、不该知道基础设施"的,你却要它操作基础设施------角色错配。
比方:让访客自己拿钥匙去开你家保险柜存东西。 正确做法是在门口装个收纳箱。
14.7 三种模式对照表
| 脚本里写 | wrapper 层 | sidecar | |
|---|---|---|---|
| 用户代码是否干净 | ❌ 污染 | ✅ 干净 | ✅ 干净 |
| 凭证暴露给用户代码 | ❌ 暴露 | ⚠️ 环境变量可见 | ✅✅ 完全不可见 |
| 换传输方式成本 | 改所有脚本 | 改一处 | 改 sidecar |
| 实现复杂度 | 低 | 中 | 中高 |
| 适合 | ❌ 不推荐 | 单容器场景 | K8s 场景 |
14.8 ★ 怎么决定"某段逻辑放哪层"
| 逻辑 | 放哪层 | 为什么 |
|---|---|---|
| 起容器、传参、销毁 | 服务端 | 只有"容器外"才能做 |
| 跑用户代码、捕获 stdout | 镜像内 | 必须和用户代码同进程树 |
扫 /output、打包 |
镜像内 或 服务端 | 看能不能共享卷 |
| 上传(需要凭证) | sidecar 优先 | ★ 让凭证远离不可信代码 |
| 组装返回结构 | 服务端 | 面向 agent 的接口 |
核心原则:
- "必须和用户代码同环境"的 → 放镜像里
- "不该让用户代码碰到"的(尤其凭证)→ 放 sidecar 或服务端
- "面向外部接口"的 → 放服务端
14.9 为什么框架要"独立于用户代码"(设计哲学)
| 如果不用框架 | 用了框架 |
|---|---|
| 用户代码得自己管上传、清理 | 用户代码只管业务 |
| 凭证得给用户代码 | 凭证被隔离 |
| 每次生成代码都要写样板 | 业务代码干净 |
| 无法统一限时/限资源 | 框架统一管控 |
| 产物格式五花八门 | 框架统一协议 |
框架的价值 = 把"基础设施杂务"从"业务逻辑"里剥离。用户代码只该关心"算数",不该关心"怎么上传"。
这就是软件工程里最经典的关注点分离------只不过这次分离的是"不可信业务代码"和"可信基础设施逻辑"。
14.10 一句话收口
沙箱执行框架 = 包在用户代码外的一层"杂务处理逻辑"(准备/跑/捕获/上传/清理/组装)。它不写在用户脚本里,而是写在"镜像内 runner" + "sidecar 上传器" + "沙箱服务端编排"这三层中的某几层。原则:必须同环境的放镜像里,不该让用户碰的(凭证)放 sidecar/服务端。
第十五部分:Agent 实现方案的共识与分歧
15.1 一个诚实的判断
走到这里,你可能想问:实现一个 agent 要考虑这么多东西,那业界有没有一套"最佳实现方案"?
答案是:没有"唯一最佳方案"。有共识的分层骨架,但没有标准答案;主流是"骨架趋同 + 细节各自摸索"。
换句话说:"用什么架构"基本达成共识了,"每个模块怎么填"还在百花齐放。
15.2 为什么没有统一方案(四个根本原因)
| 原因 | 说明 |
|---|---|
| 场景差异太大 | 写代码的 agent、做客服的、跑数据分析的,需求南辕北辙 |
| 技术演进太快 | 2023 年的最佳实践,2025 年可能已被推翻 |
| 没有"编译期"约束 | 传统软件错了编译不过;agent 的"对错"要跑起来才知道 |
| 概率性系统 | ★ agent 不是确定性系统------没有确定性,就难有"最佳" |
第四条最根本:传统软件的正确性是可证明 的,agent 的行为是概率分布。这从根本上决定了它难以形成硬标准。
15.3 ★ 共识的部分:五层骨架已趋同
虽然没有统一实现,但大家的分层结构已经高度一致:
#mermaid-svg-8V3QBG1dh43e42jN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8V3QBG1dh43e42jN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8V3QBG1dh43e42jN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8V3QBG1dh43e42jN .error-icon{fill:#552222;}#mermaid-svg-8V3QBG1dh43e42jN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8V3QBG1dh43e42jN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8V3QBG1dh43e42jN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8V3QBG1dh43e42jN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8V3QBG1dh43e42jN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8V3QBG1dh43e42jN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8V3QBG1dh43e42jN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8V3QBG1dh43e42jN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8V3QBG1dh43e42jN .marker.cross{stroke:#333333;}#mermaid-svg-8V3QBG1dh43e42jN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8V3QBG1dh43e42jN p{margin:0;}#mermaid-svg-8V3QBG1dh43e42jN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8V3QBG1dh43e42jN .cluster-label text{fill:#333;}#mermaid-svg-8V3QBG1dh43e42jN .cluster-label span{color:#333;}#mermaid-svg-8V3QBG1dh43e42jN .cluster-label span p{background-color:transparent;}#mermaid-svg-8V3QBG1dh43e42jN .label text,#mermaid-svg-8V3QBG1dh43e42jN span{fill:#333;color:#333;}#mermaid-svg-8V3QBG1dh43e42jN .node rect,#mermaid-svg-8V3QBG1dh43e42jN .node circle,#mermaid-svg-8V3QBG1dh43e42jN .node ellipse,#mermaid-svg-8V3QBG1dh43e42jN .node polygon,#mermaid-svg-8V3QBG1dh43e42jN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8V3QBG1dh43e42jN .rough-node .label text,#mermaid-svg-8V3QBG1dh43e42jN .node .label text,#mermaid-svg-8V3QBG1dh43e42jN .image-shape .label,#mermaid-svg-8V3QBG1dh43e42jN .icon-shape .label{text-anchor:middle;}#mermaid-svg-8V3QBG1dh43e42jN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8V3QBG1dh43e42jN .rough-node .label,#mermaid-svg-8V3QBG1dh43e42jN .node .label,#mermaid-svg-8V3QBG1dh43e42jN .image-shape .label,#mermaid-svg-8V3QBG1dh43e42jN .icon-shape .label{text-align:center;}#mermaid-svg-8V3QBG1dh43e42jN .node.clickable{cursor:pointer;}#mermaid-svg-8V3QBG1dh43e42jN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8V3QBG1dh43e42jN .arrowheadPath{fill:#333333;}#mermaid-svg-8V3QBG1dh43e42jN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8V3QBG1dh43e42jN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8V3QBG1dh43e42jN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8V3QBG1dh43e42jN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8V3QBG1dh43e42jN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8V3QBG1dh43e42jN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8V3QBG1dh43e42jN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8V3QBG1dh43e42jN .cluster text{fill:#333;}#mermaid-svg-8V3QBG1dh43e42jN .cluster span{color:#333;}#mermaid-svg-8V3QBG1dh43e42jN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8V3QBG1dh43e42jN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8V3QBG1dh43e42jN rect.text{fill:none;stroke-width:0;}#mermaid-svg-8V3QBG1dh43e42jN .icon-shape,#mermaid-svg-8V3QBG1dh43e42jN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8V3QBG1dh43e42jN .icon-shape p,#mermaid-svg-8V3QBG1dh43e42jN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8V3QBG1dh43e42jN .icon-shape .label rect,#mermaid-svg-8V3QBG1dh43e42jN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8V3QBG1dh43e42jN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8V3QBG1dh43e42jN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8V3QBG1dh43e42jN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ① 模型层
LLM 推理
② 编排层
循环/规划/状态机
③ 工具层
内置 + MCP
④ 记忆层
短期/长期
⑤ 执行层
沙箱
| 层 | 共识度 | 现状 |
|---|---|---|
| 模型层 | ✅ 高 | 就是调 LLM API,没啥争议 |
| 编排层 | ⚠️ 中 | 循环模式已共识,具体框架各不同 |
| 工具层 | ✅ 高 | JSON Schema + MCP 已是事实标准 |
| 记忆层 | ❌ 低 | 最不成熟,各家玩法差异最大 |
| 执行层 | ⚠️ 中 | 沙箱思路共识,实现选型分歧大 |
"五层骨架"是共识,"每层怎么填"是分歧。
15.4 真正趋同的那部分:编排循环(ReAct)
如果你只能记一个"共识",记这个:几乎所有 agent 都是同一个循环。
#mermaid-svg-g9U6Fq2ZrSjlL9gu{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .error-icon{fill:#552222;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .marker.cross{stroke:#333333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu p{margin:0;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .cluster-label text{fill:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .cluster-label span{color:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .cluster-label span p{background-color:transparent;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .label text,#mermaid-svg-g9U6Fq2ZrSjlL9gu span{fill:#333;color:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .node rect,#mermaid-svg-g9U6Fq2ZrSjlL9gu .node circle,#mermaid-svg-g9U6Fq2ZrSjlL9gu .node ellipse,#mermaid-svg-g9U6Fq2ZrSjlL9gu .node polygon,#mermaid-svg-g9U6Fq2ZrSjlL9gu .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .rough-node .label text,#mermaid-svg-g9U6Fq2ZrSjlL9gu .node .label text,#mermaid-svg-g9U6Fq2ZrSjlL9gu .image-shape .label,#mermaid-svg-g9U6Fq2ZrSjlL9gu .icon-shape .label{text-anchor:middle;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .rough-node .label,#mermaid-svg-g9U6Fq2ZrSjlL9gu .node .label,#mermaid-svg-g9U6Fq2ZrSjlL9gu .image-shape .label,#mermaid-svg-g9U6Fq2ZrSjlL9gu .icon-shape .label{text-align:center;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .node.clickable{cursor:pointer;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .arrowheadPath{fill:#333333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-g9U6Fq2ZrSjlL9gu .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g9U6Fq2ZrSjlL9gu .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-g9U6Fq2ZrSjlL9gu .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .cluster text{fill:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .cluster span{color:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-g9U6Fq2ZrSjlL9gu rect.text{fill:none;stroke-width:0;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .icon-shape,#mermaid-svg-g9U6Fq2ZrSjlL9gu .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .icon-shape p,#mermaid-svg-g9U6Fq2ZrSjlL9gu .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .icon-shape .label rect,#mermaid-svg-g9U6Fq2ZrSjlL9gu .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-g9U6Fq2ZrSjlL9gu .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-g9U6Fq2ZrSjlL9gu .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-g9U6Fq2ZrSjlL9gu :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 要
不要
观察
Observation
思考
LLM 推理
要不要
调工具?
行动
执行工具
输出答案
这就是 ReAct 范式 (Reasoning + Acting)的精神内核。不管上层包装多花哨,底下都是这个 loop。
共识的是"循环",分歧的是:
| 分歧点 | 几种流派 |
|---|---|
| 规划方式 | 边想边做(ReAct) vs 先规划再做(Plan-and-Execute) |
| 状态管理 | 全靠对话历史 vs 显式状态机 vs 图结构 |
| 多 agent | 单 agent vs 多 agent 协作 vs 主从 |
| 记忆 | 全塞上下文 vs RAG vs 结构化存储 |
15.5 业界主流的"参考实现"(是参照,不是标准)
| 类别 | 代表 | 特点 |
|---|---|---|
| 框架 | LangChain / LlamaIndex | 生态大,但抽象重、常被吐槽过度封装 |
| 图驱动 | LangGraph | 用状态图管理流程,适合复杂 agent |
| 轻量 | 直接手写 loop | 大厂常见------框架越大越不信框架 |
| 厂商 SDK | OpenAI/Anthropic 官方 SDK | 贴近原生,不绑架 |
| 工作流 | Dify / Coze 等 | 低代码编排,面向业务人员 |
| 强平台 | 各云厂商 agent 平台 | 托管 + 沙箱 + 全栈 |
一个有趣的行业现象:
越是有深度的团队,越倾向于"手写最小 loop + 自己搭基础设施",而不是用现成重框架。
因为框架的抽象成本,常常高于自己实现核心循环的成本------agent 的核心循环其实就几十行。
15.6 "最佳实践"到底存不存在------分层回答
| 层面 | 有没有最佳实践 |
|---|---|
| 架构骨架 | ✅ 有(五层 + ReAct 循环) |
| 工具协议 | ✅ 有(MCP 正在成为事实标准) |
| 工程细节 | ⚠️ 有局部最佳实践(沙箱、流式、重试、可观测) |
| 整体方案 | ❌ 没有,取决于场景 |
| "该不该用多 agent" | ❌ 争议巨大,甚至有"多 agent 是伪需求"的观点 |
15.7 ★ 追"最佳实践"本身有个陷阱
agent 领域的"最佳实践"更新极快------今年的最佳实践,明年可能是反模式。
| 曾经的"最佳实践" | 现在被质疑 |
|---|---|
| 上多 agent 协作 | "大部分场景单 agent 更好" |
| 用重框架(LangChain) | "抽象太重,不如手写" |
| 把历史全塞上下文 | "上下文要精细管理" |
| 复杂规划器 | "简单 ReAct 往往更稳" |
正确姿势不是"追最佳实践",而是"抓住不变量,其余保持可替换"。
不变量(值得学):
- ReAct 循环
- 工具用 schema 描述
- 沙箱隔离
- 关注点分离
可变量(别死磕):
- 用什么框架
- 用什么记忆方案
- 单 agent 还是多 agent
- 用什么编排方式
15.8 落地建议
| 步骤 | 做法 |
|---|---|
| 1 | 先写最小循环------不上框架,手写 ReAct loop(那几十行) |
| 2 | 工具层按 MCP 精神设计------schema 描述,为日后接外部做准备 |
| 3 | 沙箱从简到繁------先子进程,再容器/K8s |
| 4 | 记忆先不做复杂------短期用对话历史,需要长期再上 RAG |
| 5 | 框架只当参照不当依赖------读 LangChain/LangGraph 学思路,核心自己控 |
核心:把"骨架"学扎实,"细节"保持可替换------这样无论业界怎么变,你都不会被淘汰。
15.9 一句话收口
业界没有统一的"最佳 agent 实现方案"。有的是:趋同的骨架(五层 + ReAct 循环)、渐成事实标准的协议(MCP)、以及各自摸索的细节。越成熟的团队越倾向"手写最小循环 + 自建基础设施"。正确姿势不是追最佳实践,而是抓住不变量、把可变量做成可替换。
结语:一张总图串起全部
#mermaid-svg-nD6KWOK0bkh1rTEh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nD6KWOK0bkh1rTEh .error-icon{fill:#552222;}#mermaid-svg-nD6KWOK0bkh1rTEh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nD6KWOK0bkh1rTEh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nD6KWOK0bkh1rTEh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nD6KWOK0bkh1rTEh .marker.cross{stroke:#333333;}#mermaid-svg-nD6KWOK0bkh1rTEh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nD6KWOK0bkh1rTEh p{margin:0;}#mermaid-svg-nD6KWOK0bkh1rTEh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh .cluster-label text{fill:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh .cluster-label span{color:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh .cluster-label span p{background-color:transparent;}#mermaid-svg-nD6KWOK0bkh1rTEh .label text,#mermaid-svg-nD6KWOK0bkh1rTEh span{fill:#333;color:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh .node rect,#mermaid-svg-nD6KWOK0bkh1rTEh .node circle,#mermaid-svg-nD6KWOK0bkh1rTEh .node ellipse,#mermaid-svg-nD6KWOK0bkh1rTEh .node polygon,#mermaid-svg-nD6KWOK0bkh1rTEh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nD6KWOK0bkh1rTEh .rough-node .label text,#mermaid-svg-nD6KWOK0bkh1rTEh .node .label text,#mermaid-svg-nD6KWOK0bkh1rTEh .image-shape .label,#mermaid-svg-nD6KWOK0bkh1rTEh .icon-shape .label{text-anchor:middle;}#mermaid-svg-nD6KWOK0bkh1rTEh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nD6KWOK0bkh1rTEh .rough-node .label,#mermaid-svg-nD6KWOK0bkh1rTEh .node .label,#mermaid-svg-nD6KWOK0bkh1rTEh .image-shape .label,#mermaid-svg-nD6KWOK0bkh1rTEh .icon-shape .label{text-align:center;}#mermaid-svg-nD6KWOK0bkh1rTEh .node.clickable{cursor:pointer;}#mermaid-svg-nD6KWOK0bkh1rTEh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nD6KWOK0bkh1rTEh .arrowheadPath{fill:#333333;}#mermaid-svg-nD6KWOK0bkh1rTEh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nD6KWOK0bkh1rTEh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nD6KWOK0bkh1rTEh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nD6KWOK0bkh1rTEh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nD6KWOK0bkh1rTEh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nD6KWOK0bkh1rTEh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nD6KWOK0bkh1rTEh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nD6KWOK0bkh1rTEh .cluster text{fill:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh .cluster span{color:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nD6KWOK0bkh1rTEh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nD6KWOK0bkh1rTEh rect.text{fill:none;stroke-width:0;}#mermaid-svg-nD6KWOK0bkh1rTEh .icon-shape,#mermaid-svg-nD6KWOK0bkh1rTEh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nD6KWOK0bkh1rTEh .icon-shape p,#mermaid-svg-nD6KWOK0bkh1rTEh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nD6KWOK0bkh1rTEh .icon-shape .label rect,#mermaid-svg-nD6KWOK0bkh1rTEh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nD6KWOK0bkh1rTEh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nD6KWOK0bkh1rTEh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nD6KWOK0bkh1rTEh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 平台/部署层
A. Agent 运行沙箱 (宿主/容器/云VM)
agent 程序
(调LLM / 解析 / 调度)
工具注册表
内置工具 fn
本地直调
MCP 代理 fn
网络RPC
本地执行代码
⑧ 远程沙箱服务
(HTTP/gRPC)
B. 代码执行沙箱
(容器/K8s Job)
跑不可信脚本
沙箱执行框架
准备/跑/捕获/清理
⑨ 产物回传
共享卷 / 对象存储 / 回调
隔离墙(隔宿主/别人/你的设备)
四句话收尾
- 工具系统 :内置工具是同进程函数调用,MCP 工具是网络 RPC------统一在调度,分叉在执行。
- 沙箱有两种 :A 是 agent 住的房子(部署层给),B 是关不可信代码的笼子(可能要自己写)------同名不同物,方向相反。
- 落地关键 :K8s Job 是云原生拉沙箱的正解;依赖不全靠"预置 + 动态构建(哈希缓存)+ LLM 自愈"三件套兜底------不赌装全,而是设计好缺了怎么办。
- 分布式沙箱 :agent 不必自己拉容器,可以调远程沙箱服务 ;产物回传分拉取式/推送式 两大范式;上传等基础设施杂务一律交给沙箱执行框架 ,绝不让不可信代码碰凭证------关注点分离,让业务代码只关心业务。
一以贯之的核心主线
LLM 只负责"决定",agent 运行时负责"调度",工具负责"执行",沙箱负责"隔离"。
四层各司其职,链条清晰------这就是一个 agent 从"想"到"做"的完整骨架。
全篇结构索引
| 部分 | 主题 |
|---|---|
| 一 | 工具系统:分类与设计原则 |
| 二 | 内置工具 vs MCP 工具 |
| 三 | 调度链路:一次工具调用怎么发生 |
| 四 | 沙箱基础:职责与隔离分级 |
| 五 | ★ 两种沙箱(核心对照) |
| 六 | 文件进出的真相 |
| 七 | 程序、环境、沙箱三者关系 |
| 八 | 代码执行沙箱的实现方案 |
| 九 | 容器的创建与环境准备 |
| 十 | Agent 在沙箱里拉起沙箱容器(含远程能力辨析) |
| 十一 | 沙箱的依赖管理 |
| 十二 | ★ 远程沙箱服务 |
| 十三 | ★ 产物回传 |
| 十四 | ★ 沙箱执行框架 |
| 十五 | ★ Agent 实现方案的共识与分歧(工程选型视角) |
一以贯之的核心主线
LLM 只负责"决定",agent 运行时负责"调度",工具负责"执行",沙箱负责"隔离"。
四层各司其职,链条清晰------这就是一个 agent 从"想"到"做"的完整骨架。