Agent 的 Tool Call 如何在沙箱里执行
当你在 agent 框架里看到一次 tool call,表面上像是一句:
run_code()commands.run()files.read()- 浏览器 / HTTP / CLI 工具调用
但在 CubeSandbox 里,这些调用并不是“直接跑在控制面上”,而是被拆成了控制面创建 + 数据面路由 + 沙箱内执行器处理三段。
这篇文章专门回答两个问题:
- 一个 agent 的 tool call,为什么最后能进入某个具体沙箱?
- 请求到了沙箱后,又是谁真正执行它?
1. 先看总图:一次 tool call 穿过了哪些层
这张图里最容易被忽略的一点是:
- 创建沙箱 走的是控制面链路:
CubeAPI -> CubeMaster -> Cubelet - 执行 tool call 走的是数据面链路:
SDK -> CubeProxy -> sandbox port
换句话说:
控制面负责“把沙箱准备出来”,数据面负责“把请求送进已经存在的沙箱”。
2. 第一步:agent 并不是直接找 Cubelet,而是先拿一个可访问的 Sandbox 对象
从 SDK 视角,一个 tool call 的前置条件不是“知道某台节点 IP”,而是先拿到 Sandbox 对象。
在 Go SDK 里,这个对象里最关键的字段包括:
SandboxIDDomainEnvdAccessTokenTrafficAccessToken
而这些字段的来源,根本上仍然来自控制面创建结果。
控制面创建后,SDK/客户端会拿到足够的信息,能够构造出类似下面这样的数据面地址:
49999-<sandboxID>.<domain>49983-<sandboxID>.<domain>
这类地址本身并不是“真实服务发现 DNS”,而是让 CubeProxy 识别该请求应该落到哪个沙箱。
3. 第二步:为什么 host 看起来像 49999-sandboxid.domain
在 Go SDK 里,sdk/go/sandbox.go 的 GetHost(port) 会直接生成:
<port>-<sandboxID>.<domain>例如:
49999-sb-123.cube.app这不是美观问题,而是路由协议本身。
CubeProxy 会根据 Host 里的:
- 端口号
- sandboxID
- domain
去找到目标沙箱及其宿主端口映射,再把请求转发进去。
因此,从数据面视角,sandboxID 并不只是对象标识,它还是路由键的一部分。
4. 第三步:tool call 真正打到哪个端口,取决于工具类型
在 CubeSandbox 里,不同工具并不一定走同一个端口。
4.1 RunCode:通常走 49999
Go SDK 的 Sandbox.RunCode() 会向:
POST https://49999-<sandboxID>.<domain>/execute
发送请求。
这意味着 run_code 依赖的是代码执行网关 / Jupyter 风格执行器,而不是普通 shell。
4.2 Commands.Run:也是到数据面,但协议不同
sdk/go/envd.go 里,Commands.Run() 最终会调:
POST /process.Process/Start
它用的是 connect 风格流式协议,底层由 envd 的 process API 拉起进程,并返回:
- stdout
- stderr
- exit code
所以从 agent 视角,“执行一个命令”和“执行一段代码”都叫 tool call;但从沙箱内部视角,它们是两个不同的执行通道。
4.3 Files.Read:走 envd 的文件 API
sdk/go/envd.go 里,Files.Read() 调的是:
GET /files?path=...
这说明文件读取也不是通过 SSH 或 shell,而是走沙箱内置的文件服务。
5. 第四步:CubeProxy 负责把请求送到正确节点,但不负责执行代码
这一点非常重要。
CubeProxy 的角色是:
- 解析 Host 或路径中的
sandboxID - 找到对应节点和宿主机端口
- 把 HTTP / WebSocket / connect 流量转发过去
它不负责:
- 解释 Python 代码
- 创建 shell 进程
- 读取文件
- 管理 Jupyter kernel
这些都是沙箱内部服务在做。
所以可以把它理解成:
CubeProxy:入站路由器envd / Jupyter / code-interpreter gateway:沙箱内执行器
6. 第五步:为什么创建时要把端口映射写进控制面元数据
在 CubeMaster/pkg/service/sandbox/sandbox_run.go 里,创建成功后会调用 setProxyToRedis()。
这里会把 ContainerToHostPorts 等信息写入 SandboxProxyMap。
这对 tool call 至关重要,因为 CubeProxy 需要知道:
- 某个
sandboxID现在在哪台节点上 - 它的容器端口
49999映射到了宿主哪个端口 - 它的
49983是否也可访问
如果这份映射没写进去,结果通常就是:
- 控制面“已经创建成功”
- 但数据面仍然无法访问
- SDK 上表现为连接失败、超时、404/502 等问题
所以对 agent 场景来说,创建成功只是前半场,端口路由登记才决定 tool call 能否真正打进去。
7. 第六步:到了沙箱内部,是谁执行了 tool call
从当前开源代码和 SDK 约定看,至少有两类内部执行器:
7.1 Jupyter / code-interpreter 风格执行器
RunCode() 调用的是 /execute,并以流式方式返回:
- stdout
- stderr
- result
- error
- execution_count 等事件
这说明端口 49999 上跑的不是通用反向代理,而是一个能理解代码执行语义的服务。
它更像:
- 接收一段代码
- 在对应 kernel / execution context 中运行
- 以结构化流的方式把结果吐回 SDK
这也是为什么模板里如果只暴露了 49983、没暴露 49999,run_code 会失败。
7.2 envd 进程服务
Commands.Run() 调 process.Process/Start,本质是让沙箱内的 envd process API 启动一个进程,例如:
/bin/bash -l -c <command>
它返回的是进程事件流,而不是 notebook 风格结果流。
这条链路更适合:
- shell 命令
- 文件生成
- 工具安装
- 调试命令
- agent 的外壳式操作
因此,同样是 tool call:
run_code更偏“代码解释器”commands.run更偏“系统命令执行器”
8. 第七步:为什么要有 EnvdAccessToken
在 sdk/go/envd.go 里,newEnvdRequest() 会在有 token 时设置:
X-Access-Token: <EnvdAccessToken>
这说明数据面并不是“只要知道 sandbox 域名就能打进去”,而是可以在沙箱内执行器这一层再加一层访问控制。
从架构上看,这意味着数据面至少分成两层权限:
- 你能不能被路由到这个沙箱
- 即使路由到了,沙箱内服务是否接受你的调用
这对 agent 场景很重要,因为一个 tool call 通常不只是“能连上”,还要确保执行器不会被任意未授权客户端直接调用。
9. 第八步:为什么 connect 和 resume 会影响 tool call
如果一个沙箱已经被 pause,数据面虽然还保留着逻辑身份,但 runtime task 已经不在可执行状态。
这时:
connect_sandbox()会先看状态- 如果是 paused,就先调用 resume
- 再返回新的沙箱详情
所以从 agent 编排角度,正确理解应该是:
- tool call 之前,不只是“有 sandboxID”
- 还要“这个 sandbox 当前处于可接收执行请求的运行态”
否则你可能拿到的是一个存在于控制面的对象,但数据面执行器尚未恢复就绪。
10. 一次典型的 agent tool call,可以拆成 8 个动作
以 run_code() 为例,可以把一次完整调用拆成下面 8 步:
- agent 决定需要一个代码执行工具。
- SDK 若无现成沙箱,则先走控制面创建沙箱。
- CubeMaster 创建成功后把
sandboxID -> hostIP / port mapping写入元数据。 - SDK 拿到
Sandbox对象,生成49999-<sandboxID>.<domain>。 - SDK 向
/execute发起 HTTP 请求。 CubeProxy根据 Host 把流量路由到目标节点的对应宿主端口。- 沙箱内
49999端口上的执行器接收代码并运行。 - 执行结果以结构化流返回给 SDK,再由 SDK 还原为 tool call 输出。
如果是 commands.run(),第 7 步会从“代码解释器”变成“envd 进程执行器”。
11. 最容易踩的几个误区
11.1 误区一:以为控制面直接执行了代码
不是。
控制面只负责:
- 创建
- 调度
- 状态管理
- 销毁
代码和命令的真正执行发生在数据面和沙箱内部。
11.2 误区二:以为只要有 sandboxID 就能访问
也不是。
还需要:
- 正确的 domain / Host 规则
CubeProxy路由元数据- 对应端口已暴露
- 对应执行器已启动
- 必要时带上
EnvdAccessToken
11.3 误区三:把 49983 和 49999 当成一个东西
不是。
它们通常代表不同的数据面服务:
- 一个偏 envd / 通用操作
- 一个偏 code interpreter / Jupyter 执行
agent 用什么 tool,取决于它要的是哪类能力。
12. 用一句话概括这条链路
如果只记一句话,可以记这个:
agent 的 tool call 先依赖控制面把沙箱和路由准备好,再由 SDK 按
<port>-<sandboxID>.<domain>规则经 CubeProxy 把请求送到沙箱内的执行器,最终由 envd 或 code-interpreter 服务真正完成执行。
这也是 CubeSandbox 能同时兼容:
- serverless 风格创建语义
- agent 风格工具调用语义
的根本原因。