在AWS裸金属实例上安装Cubesandbox并集成PydanticAI进行数据分析的实践

Cubesandbox 是腾讯开源的代码沙箱运行时 ,对外暴露 E2B 兼容的 REST API。其核心定位:

  • API 协议层 :与 E2B 完全兼容,应用层代码(包括 e2b-code-interpreter Python SDK)无需改动 ,只换 E2B_API_URL
  • 执行隔离层 :每个 sandbox 是一个独立的 KVM MicroVM(不是 Docker 容器),有自己的 kernel、CPU 配额、网络命名空间
  • 典型用途:让 LLM Agent 安全地执行用户/模型生成的代码,避免容器逃逸或副作用

为了理解之后的步骤,我们先来梳理cudesandbox的组件依赖关系,其中包含两条关键链路:

  1. API 链:SDK → cube-api(:3000) → cubemaster(:8089) → cubelet(:9999) → 创建 MicroVM
  2. 数据链:SDK 拿到 sandbox-id 后,访问 https://<sandbox-id>-<port>.cube.app → cube-proxy → MicroVM 内的服务

#mermaid-svg-2gOFto9nS1xVAFRJ{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-2gOFto9nS1xVAFRJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2gOFto9nS1xVAFRJ .error-icon{fill:#552222;}#mermaid-svg-2gOFto9nS1xVAFRJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2gOFto9nS1xVAFRJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2gOFto9nS1xVAFRJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2gOFto9nS1xVAFRJ .marker.cross{stroke:#333333;}#mermaid-svg-2gOFto9nS1xVAFRJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2gOFto9nS1xVAFRJ p{margin:0;}#mermaid-svg-2gOFto9nS1xVAFRJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ .cluster-label text{fill:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ .cluster-label span{color:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ .cluster-label span p{background-color:transparent;}#mermaid-svg-2gOFto9nS1xVAFRJ .label text,#mermaid-svg-2gOFto9nS1xVAFRJ span{fill:#333;color:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ .node rect,#mermaid-svg-2gOFto9nS1xVAFRJ .node circle,#mermaid-svg-2gOFto9nS1xVAFRJ .node ellipse,#mermaid-svg-2gOFto9nS1xVAFRJ .node polygon,#mermaid-svg-2gOFto9nS1xVAFRJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2gOFto9nS1xVAFRJ .rough-node .label text,#mermaid-svg-2gOFto9nS1xVAFRJ .node .label text,#mermaid-svg-2gOFto9nS1xVAFRJ .image-shape .label,#mermaid-svg-2gOFto9nS1xVAFRJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-2gOFto9nS1xVAFRJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2gOFto9nS1xVAFRJ .rough-node .label,#mermaid-svg-2gOFto9nS1xVAFRJ .node .label,#mermaid-svg-2gOFto9nS1xVAFRJ .image-shape .label,#mermaid-svg-2gOFto9nS1xVAFRJ .icon-shape .label{text-align:center;}#mermaid-svg-2gOFto9nS1xVAFRJ .node.clickable{cursor:pointer;}#mermaid-svg-2gOFto9nS1xVAFRJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2gOFto9nS1xVAFRJ .arrowheadPath{fill:#333333;}#mermaid-svg-2gOFto9nS1xVAFRJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2gOFto9nS1xVAFRJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2gOFto9nS1xVAFRJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2gOFto9nS1xVAFRJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2gOFto9nS1xVAFRJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2gOFto9nS1xVAFRJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2gOFto9nS1xVAFRJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2gOFto9nS1xVAFRJ .cluster text{fill:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ .cluster span{color:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ 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-2gOFto9nS1xVAFRJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2gOFto9nS1xVAFRJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-2gOFto9nS1xVAFRJ .icon-shape,#mermaid-svg-2gOFto9nS1xVAFRJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2gOFto9nS1xVAFRJ .icon-shape p,#mermaid-svg-2gOFto9nS1xVAFRJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2gOFto9nS1xVAFRJ .icon-shape .label rect,#mermaid-svg-2gOFto9nS1xVAFRJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2gOFto9nS1xVAFRJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2gOFto9nS1xVAFRJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2gOFto9nS1xVAFRJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Docker_Compose
宿主进程
HTTP
gRPC
gRPC :9999
启动时初始化网络
sandbox URL

*.cube.app
network-agent

unix:///tmp/cube/network-agent-grpc.sock
cubemaster

tcp :8089
cube-api

tcp :3000

E2B 兼容 REST
cubelet

tcp :9999 :9998
MySQL :3306
Redis :6379
cube-proxy

:443 :80
cube-proxy-coredns

cube.app 域名解析
E2B Python SDK

环境初始化

本次测试环境为AL2023操作系统(上游为Fedora系统),我们启动m5系列的裸金属实例完成本次测试。

首先,验证 KVM 与 CPU 虚拟化能力,如果/dev/kvm 缺失则需要停机检查环境,停止后续的步骤。

bash 复制代码
$ ls -la /dev/kvm
crw-rw-rw-. 1 root kvm 10, 232 ...

$ lscpu | grep -iE "Architecture|Virtualization|CPU\(s\):"
Architecture:        x86_64
CPU(s):              96
Virtualization:      VT-x # 具备VT-x

/dev/kvm 不存在的环境跑不了 Cubesandbox bare-metal,需另寻支持嵌套,PVM或物理机的环境,具体的判断逻辑如下
#mermaid-svg-JoscSlkllugSPONh{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-JoscSlkllugSPONh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JoscSlkllugSPONh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JoscSlkllugSPONh .error-icon{fill:#552222;}#mermaid-svg-JoscSlkllugSPONh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JoscSlkllugSPONh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JoscSlkllugSPONh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JoscSlkllugSPONh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JoscSlkllugSPONh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JoscSlkllugSPONh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JoscSlkllugSPONh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JoscSlkllugSPONh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JoscSlkllugSPONh .marker.cross{stroke:#333333;}#mermaid-svg-JoscSlkllugSPONh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JoscSlkllugSPONh p{margin:0;}#mermaid-svg-JoscSlkllugSPONh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JoscSlkllugSPONh .cluster-label text{fill:#333;}#mermaid-svg-JoscSlkllugSPONh .cluster-label span{color:#333;}#mermaid-svg-JoscSlkllugSPONh .cluster-label span p{background-color:transparent;}#mermaid-svg-JoscSlkllugSPONh .label text,#mermaid-svg-JoscSlkllugSPONh span{fill:#333;color:#333;}#mermaid-svg-JoscSlkllugSPONh .node rect,#mermaid-svg-JoscSlkllugSPONh .node circle,#mermaid-svg-JoscSlkllugSPONh .node ellipse,#mermaid-svg-JoscSlkllugSPONh .node polygon,#mermaid-svg-JoscSlkllugSPONh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JoscSlkllugSPONh .rough-node .label text,#mermaid-svg-JoscSlkllugSPONh .node .label text,#mermaid-svg-JoscSlkllugSPONh .image-shape .label,#mermaid-svg-JoscSlkllugSPONh .icon-shape .label{text-anchor:middle;}#mermaid-svg-JoscSlkllugSPONh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JoscSlkllugSPONh .rough-node .label,#mermaid-svg-JoscSlkllugSPONh .node .label,#mermaid-svg-JoscSlkllugSPONh .image-shape .label,#mermaid-svg-JoscSlkllugSPONh .icon-shape .label{text-align:center;}#mermaid-svg-JoscSlkllugSPONh .node.clickable{cursor:pointer;}#mermaid-svg-JoscSlkllugSPONh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JoscSlkllugSPONh .arrowheadPath{fill:#333333;}#mermaid-svg-JoscSlkllugSPONh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JoscSlkllugSPONh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JoscSlkllugSPONh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JoscSlkllugSPONh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JoscSlkllugSPONh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JoscSlkllugSPONh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JoscSlkllugSPONh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JoscSlkllugSPONh .cluster text{fill:#333;}#mermaid-svg-JoscSlkllugSPONh .cluster span{color:#333;}#mermaid-svg-JoscSlkllugSPONh 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-JoscSlkllugSPONh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JoscSlkllugSPONh rect.text{fill:none;stroke-width:0;}#mermaid-svg-JoscSlkllugSPONh .icon-shape,#mermaid-svg-JoscSlkllugSPONh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JoscSlkllugSPONh .icon-shape p,#mermaid-svg-JoscSlkllugSPONh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JoscSlkllugSPONh .icon-shape .label rect,#mermaid-svg-JoscSlkllugSPONh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JoscSlkllugSPONh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JoscSlkllugSPONh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JoscSlkllugSPONh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 裸金属直接跑
经 Hypervisor 一层

Hypervisor 暴露 vmx 给 Guest
Guest 内仍可见 vmx
Hypervisor 不支持嵌套
物理 CPU

Intel VT-x / AMD-V
/dev/kvm 可用
嵌套虚拟化
/dev/kvm 不可用
Cubesandbox 可跑
Cubesandbox 跑不起来

安装 Docker

bash 复制代码
sudo dnf install -y docker
sudo systemctl enable --now docker
sudo docker version

安装 Cubesandbox

cubesandbox 安装脚本的 lib/common.sh 假设 yum 系发行版必有 EPEL 仓库。但是AL2023 是 Fedora 派生 ,但不预装 EPEL ,且默认仓库不带 ripgrep 包。此外,安装脚本会检查是否存在ripgrep和docker compose命令,因此需要提前安装好

shell 复制代码
curl -fSL https://github.com/BurntSushi/ripgrep/releases/download/14.1.1/ripgrep-14.1.1-x86_64-unknown-linux-musl.tar.gz \
  -o /tmp/rg.tar.gz
tar -xzf /tmp/rg.tar.gz -C /tmp/
sudo cp /tmp/ripgrep-14.1.1-x86_64-unknown-linux-musl/rg /usr/local/bin/rg
sudo chmod +x /usr/local/bin/rg

# 2) docker compose v2 plugin
sudo mkdir -p /usr/local/lib/docker/cli-plugins
sudo curl -fSL \
  https://github.com/docker/compose/releases/download/v2.30.3/docker-compose-linux-x86_64 \
  -o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
sudo ln -sf /usr/local/lib/docker/cli-plugins/docker-compose /usr/local/bin/docker-compose

在 root 用户下执行官方文档命令中的如下命令

bash 复制代码
curl -sL https://cnb.cool/CubeSandbox/CubeSandbox/-/git/raw/master/deploy/one-click/online-install.sh | MIRROR=cn bash

依赖装齐后重跑 install.sh,服务看起来都起来了

复制代码
[one-click-runtime] started network-agent pid=43031
[one-click-runtime] started cubemaster pid=43032
[one-click-runtime] started cube-api pid=43033
[one-click-runtime] started cubelet pid=43034

但是此时创建模板会立即失败

复制代码
status: FAILED
phase: DISTRIBUTING
error: artifact distribution failed on all 1 nodes:
       rpc error: code = Unimplemented
       desc = unknown service cubelet.services.images.v1.Images

检查/data/log/Cubelet/Cubelet-req.log发现如下错误日志

复制代码
[INFO]  network plugin init begin
[INFO]  tap plugin init: enable_network_agent=true
        endpoint=grpc+unix:///tmp/cube/network-agent-grpc.sock
[WARN]  network-agent not ready yet, retry_interval=1s err=context deadline exceeded
[WARN]  network-agent not ready yet, retry_interval=1s err=context deadline exceeded
... 
[FATAL] plugin network init failed: network-agent health check failed at init,
        waited 30s for network-agent readiness: context deadline exceeded
[WARN]  failed to load plugin {"type":"io.cubelet.internal.v1"}
[FATAL] plugin workflow init fail: failed to get internal plugin: network-agent health check failed
[FATAL] plugin gc-service init fail: ...
[FATAL] plugin cubebox-service init fail: ...
[WARN]  failed to load plugin {"type":"io.cubelet.images-service.v1"}
[INFO]  cubelet successfully booted in 31.374417s
[ERROR] cubelet successfully booted in 31.374456s

日志表明,cubelet 启动时等待30 秒才去 connect /tmp/cube/network-agent-grpc.sock,但是 network-agent 真正 listen 的时刻晚于 cubelet 30s 超时点,导致cubelet 等不及就 FATAL。此时cubelet 进程虽然没有完全退出,但 Images / Workflow / Cubebox-Service 等关键服务全部加载失败,时序图如下:
cubelet 进程 "/tmp/cube/network-agent-grpc.sock" network-agent 进程 up.sh cubelet 进程 "/tmp/cube/network-agent-grpc.sock" network-agent 进程 up.sh #mermaid-svg-BFIKvjfqBvmf9gQw{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-BFIKvjfqBvmf9gQw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BFIKvjfqBvmf9gQw .error-icon{fill:#552222;}#mermaid-svg-BFIKvjfqBvmf9gQw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BFIKvjfqBvmf9gQw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BFIKvjfqBvmf9gQw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BFIKvjfqBvmf9gQw .marker.cross{stroke:#333333;}#mermaid-svg-BFIKvjfqBvmf9gQw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BFIKvjfqBvmf9gQw p{margin:0;}#mermaid-svg-BFIKvjfqBvmf9gQw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BFIKvjfqBvmf9gQw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BFIKvjfqBvmf9gQw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-BFIKvjfqBvmf9gQw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-BFIKvjfqBvmf9gQw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-BFIKvjfqBvmf9gQw .sequenceNumber{fill:white;}#mermaid-svg-BFIKvjfqBvmf9gQw #sequencenumber{fill:#333;}#mermaid-svg-BFIKvjfqBvmf9gQw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-BFIKvjfqBvmf9gQw .messageText{fill:#333;stroke:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BFIKvjfqBvmf9gQw .labelText,#mermaid-svg-BFIKvjfqBvmf9gQw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .loopText,#mermaid-svg-BFIKvjfqBvmf9gQw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .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-BFIKvjfqBvmf9gQw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-BFIKvjfqBvmf9gQw .noteText,#mermaid-svg-BFIKvjfqBvmf9gQw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-BFIKvjfqBvmf9gQw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BFIKvjfqBvmf9gQw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BFIKvjfqBvmf9gQw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BFIKvjfqBvmf9gQw .actorPopupMenu{position:absolute;}#mermaid-svg-BFIKvjfqBvmf9gQw .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-BFIKvjfqBvmf9gQw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BFIKvjfqBvmf9gQw .actor-man circle,#mermaid-svg-BFIKvjfqBvmf9gQw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-BFIKvjfqBvmf9gQw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 进程启动但还在做 初始化 ~60s 累计 30s 超时 但 cubelet 不会重新 init 插件 nohup network-agent & nohup cubelet & (立即) connect (尝试 1) ENOENT sleep 4s, retry (×8) FATAL: network plugin init failed 后续依赖网络的插件全部失败 终于 listen 上 (晚了)

解决方法很简单,因为 network-agent 早已就绪,直接单独重启 cubelet即可

bash 复制代码
sudo pkill -9 -f "/Cubelet/bin/cubelet"
sleep 2
sudo ls -la /tmp/cube/network-agent-grpc.sock

sudo bash -c "nohup /usr/local/services/cubetoolbox/Cubelet/bin/cubelet \
  --config /usr/local/services/cubetoolbox/Cubelet/config/config.toml \
  --dynamic-conf-path /usr/local/services/cubetoolbox/Cubelet/dynamicconf/conf.yaml \
  > /var/log/cube-sandbox-one-click/cubelet.log 2>&1 < /dev/null &"

之后我们运行官方的 Python 测试代码如下(具体步骤和CUBE_TEMPLATE_ID见模板创建部分)

python 复制代码
with Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"]) as sandbox:
    result = sandbox.run_code("print('Hello')")  # 这里抛错

出现如下报错,检查Sandbox.create() 本身成功了(拿到了 sandbox-id)。问题出在 run_code 时 SDK 通过 https://<sandbox-id>-49999.cube.app 访问 sandbox 内服务,域名解析失败。

复制代码
httpcore.ConnectError: [Errno -2] Name or service not known

进一步检查发现install.sh脚本设置了 cube-dns0 link 给 systemd-resolved,但没有改 nsswitch.conf 让 NSS( Name Service Switch) 也走 systemd-resolved。/etc/resolv.conf 直接指向上游 DNS(不是 systemd-resolved 的 stub 127.0.0.53),导致大部分应用绕过 resolved。

我们把 resolve NSS 模块加进 hosts 解析链,强制让系统所有应用先问 systemd-resolved,再走传统 DNS

bash 复制代码
sudo sed -i.bak \
  's/^hosts:.*/hosts:      files resolve [!UNAVAIL=return] dns myhostname/' \
  /etc/nsswitch.conf

[!UNAVAIL=return] 的语义:如果 resolve(systemd-resolved)返回 NXDOMAIN,直接返回 ,不再 fallback 到 dns(上游 DNS)。这样 *.cube.app 走 systemd-resolved,其他域名走 systemd-resolved 失败再走上游 DNS。

模板创建与 SDK 验证

创建模板

cubelet 就绪后直接创建模板成功

bash 复制代码
sudo cubemastercli tpl create-from-image \
  --image cube-sandbox-cn.tencentcloudcr.com/cube-sandbox/sandbox-code:latest \
  --writable-layer-size 1G \
  --cpu 2000 --memory 4000 \
  --expose-port 49999 \
  --expose-port 49983 \
  --probe 49999

参数说明如下,更多参数

参数 含义
--image 源 OCI 镜像。
--writable-layer-size 1G sandbox 可写层大小(overlay 上层),决定 sandbox 可写多少数据
--expose-port 49999/49983 镜像内监听的端口(sandbox-code 镜像约定)
--probe 49999 健康探针端口;模板就绪条件就是这个 HTTP GET 通

执行成功后会输出模板id,总耗时约 40 秒(镜像 layer 已被预拉到 docker 内)。

复制代码
job_id: <JOB_ID>
template_id: <TEMPLATE_ID>
status: PENDING
phase: PULLING
...
status: READY
phase: READY
progress: 100%
distribution: 1/1 ready, 0 failed

如要监控较长任务可以通过如下命令完成

bash 复制代码
sudo cubemastercli tpl watch --job-id <JOB_ID>

E2B SDK 代码

安装 SDK

bash 复制代码
sudo dnf install -y python3 python3-pip
pip3 install e2b-code-interpreter

准备 mkcert CA:

bash 复制代码
sudo cp /root/.local/share/mkcert/rootCA.pem /tmp/cube-rootCA.pem
sudo chmod 644 /tmp/cube-rootCA.pem

设置环境变量,其中**SSL_CERT_FILE 必须设置。sandbox URL 是 HTTPS(https://*.cube.app),证书由 cubesandbox 自带的 mkcert 本地 CA 签发。SDK 走 Python httpx(基于 httpcore + 系统 OpenSSL),不会自动信任这个 CA,必须显式指向 rootCA.pem

bash 复制代码
export E2B_API_URL='http://127.0.0.1:3000'
export E2B_API_KEY='dummy'
export CUBE_TEMPLATE_ID='<TEMPLATE_ID>'
export SSL_CERT_FILE='/tmp/cube-rootCA.pem'   # mkcert 签的本地 CA

测试脚本:

python 复制代码
import os
from e2b_code_interpreter import Sandbox

print(f'API URL:  {os.environ.get("E2B_API_URL")}')
print(f'Template: {os.environ.get("CUBE_TEMPLATE_ID")}')

with Sandbox.create(template=os.environ['CUBE_TEMPLATE_ID']) as sandbox:
    print(f'Sandbox ID: {sandbox.sandbox_id}')

    # 1) 基础打印
    r1 = sandbox.run_code("print('Hello from Cube Sandbox, safely isolated!')")
    print('stdout:', r1.logs.stdout)

    # 2) 隔离性验证
    r2 = sandbox.run_code('''
import platform, os, sys, socket
print(f"Python: {sys.version.split()[0]}")
print(f"Hostname: {platform.node()}")
print(f"Kernel: {platform.release()}")
print(f"CPU count: {os.cpu_count()}")
print(f"IP: {socket.gethostbyname(socket.gethostname())}")
''')
    print('stdout:', r2.logs.stdout)

    # 3) 计算
    r3 = sandbox.run_code('print(sum(range(1_000_000)))')
    print('stdout:', r3.logs.stdout)

实际测试输出,宿主机内核版本为6.1.x-amzn2023,sandbox内核版本6.6.1199-0009-03_2.0.1,看到独立内核版本说明VM生效

复制代码
=== Test 1: Hello world ===
stdout: ['Hello from Cube Sandbox, safely isolated!\n']

=== Test 2: Compute (verify isolation) ===
stdout: ['Python: 3.12.12
         Hostname: tpl-XXXX
         Kernel: 6.6.1199-0009-03_2.0.1
         CPU count: 2
         IP: 127.0.0.1
        ']

=== Test 3: Math ===
stdout: ['499999500000\n']

使用sandbox分析数据

接下来我们使用PydanticAI集成sandbox的能力,实现自然语言提问 ,AI 写代码 ,Cubesandbox 运行代码 ,产出图表的全过程。整体架构如下:
#mermaid-svg-PFAVMNWqx7aGkI5l{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-PFAVMNWqx7aGkI5l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PFAVMNWqx7aGkI5l .error-icon{fill:#552222;}#mermaid-svg-PFAVMNWqx7aGkI5l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PFAVMNWqx7aGkI5l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PFAVMNWqx7aGkI5l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PFAVMNWqx7aGkI5l .marker.cross{stroke:#333333;}#mermaid-svg-PFAVMNWqx7aGkI5l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PFAVMNWqx7aGkI5l p{margin:0;}#mermaid-svg-PFAVMNWqx7aGkI5l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l .cluster-label text{fill:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l .cluster-label span{color:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l .cluster-label span p{background-color:transparent;}#mermaid-svg-PFAVMNWqx7aGkI5l .label text,#mermaid-svg-PFAVMNWqx7aGkI5l span{fill:#333;color:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l .node rect,#mermaid-svg-PFAVMNWqx7aGkI5l .node circle,#mermaid-svg-PFAVMNWqx7aGkI5l .node ellipse,#mermaid-svg-PFAVMNWqx7aGkI5l .node polygon,#mermaid-svg-PFAVMNWqx7aGkI5l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PFAVMNWqx7aGkI5l .rough-node .label text,#mermaid-svg-PFAVMNWqx7aGkI5l .node .label text,#mermaid-svg-PFAVMNWqx7aGkI5l .image-shape .label,#mermaid-svg-PFAVMNWqx7aGkI5l .icon-shape .label{text-anchor:middle;}#mermaid-svg-PFAVMNWqx7aGkI5l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PFAVMNWqx7aGkI5l .rough-node .label,#mermaid-svg-PFAVMNWqx7aGkI5l .node .label,#mermaid-svg-PFAVMNWqx7aGkI5l .image-shape .label,#mermaid-svg-PFAVMNWqx7aGkI5l .icon-shape .label{text-align:center;}#mermaid-svg-PFAVMNWqx7aGkI5l .node.clickable{cursor:pointer;}#mermaid-svg-PFAVMNWqx7aGkI5l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PFAVMNWqx7aGkI5l .arrowheadPath{fill:#333333;}#mermaid-svg-PFAVMNWqx7aGkI5l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PFAVMNWqx7aGkI5l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PFAVMNWqx7aGkI5l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PFAVMNWqx7aGkI5l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PFAVMNWqx7aGkI5l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PFAVMNWqx7aGkI5l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PFAVMNWqx7aGkI5l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PFAVMNWqx7aGkI5l .cluster text{fill:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l .cluster span{color:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l 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-PFAVMNWqx7aGkI5l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PFAVMNWqx7aGkI5l rect.text{fill:none;stroke-width:0;}#mermaid-svg-PFAVMNWqx7aGkI5l .icon-shape,#mermaid-svg-PFAVMNWqx7aGkI5l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PFAVMNWqx7aGkI5l .icon-shape p,#mermaid-svg-PFAVMNWqx7aGkI5l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PFAVMNWqx7aGkI5l .icon-shape .label rect,#mermaid-svg-PFAVMNWqx7aGkI5l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PFAVMNWqx7aGkI5l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PFAVMNWqx7aGkI5l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PFAVMNWqx7aGkI5l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输入问题/上传 CSV
LLM 决策

写 Python 代码
工具调用

run_python / save_chart
HTTP :3000

E2B 兼容协议
执行结果

stdout/stderr/files
文字答案
读取图表
写入本地
用户
analyst.py CLI
PydanticAI Agent

claude-sonnet-4-5
E2B SDK

e2b-code-interpreter
Cubesandbox 集群
本地 ./output/

图表落盘

首先我们确认cudesandbox集群就绪

shell 复制代码
# cube-api 在监听
sudo ss -tlnp | grep :3000

# 健康检查通过
curl -sS http://127.0.0.1:3000/health

# 模板已 READY
sudo cubemastercli tpl ls

# mkcert CA 可读
sudo cp /root/.local/share/mkcert/rootCA.pem /tmp/cube-rootCA.pem
sudo chmod 644 /tmp/cube-rootCA.pem

ls -la /tmp/cube-rootCA.pem

通过将把 E2B_API_URL 指向 E2B 沙盒,LLM 决策层和代码执行层通过 E2B SDK 完全解耦。

确认Cubesandbox 集群就绪后,准备好使用的模板 ID

bash 复制代码
export CUBE_TEMPLATE_ID=tpl-xxxxxxxxxxxxxxxxxxxx

PydanticAI 0.8 通过 OpenAIChatModel 支持任何 OpenAI 兼容的端点。本文实测用一个 LiteLLM proxy 当 LLM 网关,模型路由到指定模型:

复制代码
export LLM_BASE_URL='http://litellm.your.network:4000/v1'
export LLM_API_KEY='sk-xxx'
export LLM_MODEL='modelname'

模板 ID 为之前创建的模板

复制代码
export CUBE_TEMPLATE_ID=tpl-xxxxxxxxxxxxxxxxxxxx

完整的示例代码analyst.py

py 复制代码
"""
PydanticAI 0.8.1 + Cubesandbox + LiteLLM 数据分析助手
功能:CSV 上传 + 自然语言问答 + 错误自动重试 + 图表回传到本地
"""
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Annotated

from pydantic import Field
from pydantic_ai import Agent, ModelRetry, RunContext
from pydantic_ai.models.openai import OpenAIChatModel
from pydantic_ai.providers.openai import OpenAIProvider
from e2b_code_interpreter import Sandbox

# ============ Cubesandbox 配置(同机直连,是与 E2B 公有云的唯一区别)============
os.environ['E2B_API_URL'] = 'http://127.0.0.1:3000'
os.environ['E2B_API_KEY'] = 'dummy'
os.environ['SSL_CERT_FILE'] = '/tmp/cube-rootCA.pem'

TEMPLATE_ID = os.environ['CUBE_TEMPLATE_ID']

# ============ LLM 后端(任何 OpenAI 兼容端点)============
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'http://localhost:4000/v1')
LLM_API_KEY  = os.environ.get('LLM_API_KEY',  'sk-xxxxxx')
LLM_MODEL    = os.environ.get('LLM_MODEL',    'anthropic.claude-opus-4-7')

model = OpenAIChatModel(
    LLM_MODEL,
    provider=OpenAIProvider(base_url=LLM_BASE_URL, api_key=LLM_API_KEY),
)

# ============ Agent 依赖:跨工具调用共享 sandbox + 上传清单 ============
@dataclass
class Deps:
    sandbox: Sandbox
    uploaded_files: dict = field(default_factory=dict)


# ============ Agent 定义 ============
agent = Agent(
    model,
    deps_type=Deps,
    system_prompt="""你是数据分析助手。

可用工具:
- list_uploaded_files: 看用户上传了哪些文件(返回 sandbox 内绝对路径)
- run_python: 执行 Python 代码(pandas / matplotlib / numpy / seaborn)
- save_chart: 把 sandbox 里的图片保存到用户本地

规则:
1. 收到问题先 list_uploaded_files 看数据
2. 写代码用 sandbox 内路径(/work/data.csv,不是用户本地路径)
3. matplotlib 用 Agg 后端:plt.switch_backend('Agg');图片存 /tmp/xxx.png 然后 save_chart
4. 报错时根据 stderr 修,不要假装成功
5. 同一 sandbox 内变量、import 跨多次 run_python 持久化
""",
    retries=3,  # ModelRetry 最多重试 3 次
)


# ============ 列出已上传文件 ============
@agent.tool
def list_uploaded_files(ctx: RunContext[Deps]) -> dict:
    """返回 {本地原路径: sandbox 内路径}"""
    return ctx.deps.uploaded_files or {"info": "未上传任何文件"}


# ============ 执行 Python 代码(核心,带自动重试)============
@agent.tool
def run_python(
    ctx: RunContext[Deps],
    code: Annotated[str, Field(description="Python 代码")]
) -> str:
    """在 sandbox 执行 Python,返回 stdout。出错时触发 LLM 自我修正。"""
    r = ctx.deps.sandbox.run_code(code)

    # 报错ModelRetry把错误回传给 LLM,然后改代码再调
    if r.error:
        raise ModelRetry(
            f"代码报错:{r.error.name}: {r.error.value}
"
            f"stderr: {''.join(r.logs.stderr)}"
        )

    out = r.logs.stdout
    return ''.join(out) if isinstance(out, list) else (out or '(无 stdout)')

# ============ 把 sandbox 内的图表传回本地 ============
@agent.tool
def save_chart(ctx: RunContext[Deps], sandbox_path: str) -> str:
    """读取 sandbox 内的图片,保存到本地 ./output/ 目录"""
    try:
        content = ctx.deps.sandbox.files.read(sandbox_path, format='bytes')
    except Exception as e:
        raise ModelRetry(f"读取失败:{e}")

    out_dir = Path('./output')
    out_dir.mkdir(exist_ok=True)
    local = out_dir / Path(sandbox_path).name
    local.write_bytes(content)
    return f"图表已保存到本地:{local.absolute()}"

# ============ 主程序 ============
def main():
    with Sandbox.create(template=TEMPLATE_ID, timeout=1800) as sandbox:
        print(f"sandbox: {sandbox.sandbox_id}")

        deps = Deps(sandbox=sandbox)

        # 文件上传循环
        while True:
            f = input("上传文件 (留空跳过): ").strip()
            if not f:
                break
            p = Path(f).expanduser()
            if not p.exists():
                print(f"  ! 文件不存在")
                continue
            sb_path = f"/work/{p.name}"
            sandbox.files.write(sb_path, p.read_bytes())
            deps.uploaded_files[str(p)] = sb_path
            print(f"  ✓ 上传到 {sb_path}")

        # 对话循环(message_history 保留多轮上下文)
        history = []
        print("输入 'exit' 退出")
        while True:
            q = input("你: ").strip()
            if q in ('exit', 'quit', ''):
                break
            try:
                result = agent.run_sync(q, deps=deps, message_history=history)
                history = result.all_messages()
                print(f"AI: {result.output}")
            except Exception as e:
                print(f"出错: {type(e).__name__}: {e}")

if __name__ == '__main__':
    main()

以上示例中,sandbox 单例实现跨调用持久化

py 复制代码
with Sandbox.create(template=TEMPLATE_ID, timeout=1800) as sandbox:
    deps = Deps(sandbox=sandbox)
    while True:
        agent.run_sync(...)

每次新建 sandbox 有 1-3 秒开销,以上代码整个对话只用一个 sandbox。之后第一次工具调用 import pandas as pd; df = pd.read_csv('/work/sales.csv') 之后,后续工具调用都能直接用 df 。LLM 也知道这一点(看 system_prompt 最后那句),不会重复 import。

这里还有一个问题,文件是如何从sandbox中拷贝到宿主机的 ?即content = ctx.deps.sandbox.files.read(sandbox_path, format='bytes')检查如下源码

py 复制代码
# e2b/sandbox_sync/filesystem/filesystem.py
def read(self, path, format="text", ...):
    params = {"path": path, "username": username}
    r = self._envd_api.get(ENVD_API_FILES_ROUTE, params=params, ...)
    if format == "bytes":
        return bytearray(r.content)

通过 HTTP GET URL 子域名编码端口和 sandbox-id来实现文件读取

复制代码
https://49983-993e9bbbff29496a8737ce04fda56341.cube.app/files?path=/tmp/x.png
       └────┘ └────────────────────────────────────────┘
       端口   sandbox_id

请求会被转发到MicroVM 内 的envd,它是sandbox 内的 daemon,监听 :49983,提供 REST API如下

路由 功能
GET /files?path=... 读文件
POST /files?path=... 写文件
POST /commands 执行命令
GET /processes 列进程
POST /init 初始化(envvars / cwd / mount)

env收到 GET /files?path=/tmp/x.png 请求后执行文件 IO open(path,'rb').read()。因此SDK 只是 HTTP 客户端的封装

启动与文件上传

准备一个测试 CSV:

shell 复制代码
cat > /tmp/sales.csv <<'EOF'
order_id,product,amount,region,date
1,iPhone 15,8999,华东,2024-09-01
2,MacBook Air,9999,华北,2024-09-02
3,iPhone 15,8999,华东,2024-09-03
4,AirPods,1899,华南,2024-09-05
5,iPhone 15 Pro,11999,华西,2024-09-06
6,iPhone 15,8999,华北,2024-09-08
7,MacBook Pro,18999,华东,2024-09-10
8,AirPods,1899,华东,2024-09-11
9,iPhone 15 Pro,11999,华南,2024-09-12
10,iPad Air,4799,华东,2024-09-15
EOF

启动分析应用python3 analyst.py

复制代码
=== 数据分析助手 (PydanticAI 0.8.1 + Cubesandbox + LiteLLM) ===
sandbox: 9e7db5f7a959431cb92edc0c941849ff

上传文件 (留空跳过): /tmp/sales.csv
  ✓ 上传到 /work/sales.csv
上传文件 (留空跳过): 

输入 'exit' 退出

你:

真实对话示例

下面是 4 轮真实对话的完整 transcript。每轮 AI 内部都会自动调用 list_uploaded_filesrun_pythonsave_chart 等工具。

Q1:基本的数据内容查看

复制代码
你: 数据共多少行?什么列?日期范围多大?

AI: 数据基本情况如下:

**📊 sales.csv 概览**

| 项目 | 值 |
|------|-----|
| **行数** | 10 行 |
| **列数** | 5 列 |

**列结构:**
- `order_id` (int) --- 订单 ID
- `product` (str) --- 产品名(如 iPhone 15、MacBook Air)
- `amount` (int) --- 金额
- `region` (str) --- 区域(华东、华北等)
- `date` (date) --- 日期

**日期范围:** 2024-09-01 ~ 2024-09-15,跨度 14 天

数据规模比较小(只有 10 条订单)。需要我接着做哪方面分析?比如:
- 按产品/区域汇总销售额
- 每日销售趋势图
- TOP 产品排名

Q2 :同时触发 run_python + save_chart 两个工具:

复制代码
你: 按产品统计销售额排行榜,画柱状图保存到 /tmp/top_products.png,用 save_chart 传出

AI: 完成 ✅

**📊 产品销售额排行榜**

| 排名 | 产品 | 销售额 |
|------|------|--------:|
| 🥇 1 | iPhone 15 | 26,997 |
| 🥈 2 | iPhone 15 Pro | 23,998 |
| 🥉 3 | MacBook Pro | 18,999 |
| 4 | MacBook Air | 9,999 |
| 5 | iPad Air | 4,799 |
| 6 | AirPods | 3,798 |

**洞察:**
- **iPhone 15** 系列(标准版 + Pro)合计 50,995,是绝对主力,占总销售额 ~53%
- **MacBook 系列** 合计 28,998,排第二梯队
- **配件类**(iPad Air、AirPods)销售额较低,可考虑捆绑促销

柱状图已保存到本地 `/tmp/output/top_products.png`,可直接查看。

Q3:继承上下文,复用已加载的 DataFrame:

复制代码
你: 华东区每个产品的总销售额?给出表格

AI: ## 📊 华东区产品销售额明细

| 排名 | 产品 | 销售额 | 订单数 | 占比 |
|:---:|------|--------:|:---:|:---:|
| 🥇 1 | MacBook Pro | 18,999 | 1 | 43.48% |
| 🥈 2 | iPhone 15 | 17,998 | 2 | 41.19% |
| 🥉 3 | iPad Air | 4,799 | 1 | 10.98% |
| 4 | AirPods | 1,899 | 1 | 4.35% |

**汇总:**
- 华东区总销售额:**43,695**
- 华东区总订单数:**5 单**
- 客单价:**8,739**

**洞察:**
- 华东区 **MacBook Pro 单笔贡献最高**(43.48%),高客单价产品表现突出
- **iPhone 15** 走量优势明显(2 单),是华东最受欢迎产品
- 高端产品(MacBook Pro + iPhone 15)合计占华东 ~85%,消费力较强

统计图结果如下

Q4:用 seaborn 画热力图:

复制代码
你: 用 seaborn 画一个产品×区域的销售额热力图(plt.switch_backend Agg),保存到 /tmp/heatmap.png 并 save_chart 传出

AI: 完成 ✅ 热力图已传出到本地 `/tmp/output/heatmap.png`。

**📊 产品 × 区域 销售额矩阵**

| 产品 \ 区域 | East 华东 | North 华北 | South 华南 | West 华西 |
|------|--------:|--------:|--------:|--------:|
| iPhone 15 | **17,998** | 8,999 | 0 | 0 |
| iPhone 15 Pro | 0 | 0 | **11,999** | **11,999** |
| MacBook Pro | **18,999** | 0 | 0 | 0 |
| MacBook Air | 0 | **9,999** | 0 | 0 |
| iPad Air | 4,799 | 0 | 0 | 0 |
| AirPods | 1,899 | 0 | 1,899 | 0 |

**洞察:**
- 🔥 **华东是核心市场**:覆盖 iPhone 15 / MacBook Pro / iPad Air / AirPods 4 个品类
- 📱 **iPhone 15 Pro 偏南/西**:标准版主攻北方,Pro 版主攻南方/西部,定位差异明显
- 💻 **MacBook 区域分布单一**:Pro 仅华东、Air 仅华北,覆盖面偏窄
- ⚪ **数据稀疏**:矩阵里大量 0 值,反映样本量小(只有 10 单)

附注:因 sandbox 默认字体不支持中文,区域名做了英文映射(华东→East、华北→North、华南→South、华西→West),避免图上出现方块。

退出后查看本地落盘:

复制代码
$ ls -la ./output/
-rw-rw-r--. 1 ec2-user ec2-user 52649 May 29 03:41 heatmap.png
-rw-rw-r--. 1 ec2-user ec2-user 45826 May 29 03:40 top_products.png

$ file ./output/*.png
./output/heatmap.png:      PNG image data, 960 x 720, 8-bit/color RGBA, non-interlaced
./output/top_products.png: PNG image data, 1080 x 600, 8-bit/color RGBA, non-interlaced

热力图结果

错误重试

LM 写错代码是常态。传统做法是写一堆 try/except + 手动构造 follow-up prompt。PydanticAI 的优雅写法:

py 复制代码
if result.error:
    raise ModelRetry(f"代码报错: {result.error.value}\nstderr: ...")

retries 次数要合理,如果设太大可能让 LLM 在错误里打转消耗不必要的token。

ModelRetry 后框架自动:

  1. 把错误信息作为新的工具调用结果塞回对话历史
  2. 让 LLM 看到错误并产出新的工具调用
  3. 最多重试 Agent(retries=N)
  4. 用户视角:只看到最终成功的输出

在运行过程中,第 2 题和第 4 题各遇到一次 save_chart 偶发失败(cube-proxy DNS 还没完全就绪),LLM 表现:

  • ModelRetry 在 3 次重试后仍失败,PydanticAI 把异常抛给应用层

  • 但 LLM 没有崩溃,它知道数据已经在 sandbox 算好了,照常返回完整文字答案,只是诚实地说图传不出来

  • ModelRetry 设计的优势让重试链中得到的中间结果(已算好的数字)不丢失,最终回答仍然有用

    AI: 图已经成功生成(63KB),但 save_chart 一直报 DNS 错误(Name or service not known),
    看起来是上传服务暂时连不上,不是代码问题。

    [销售额排行表 + 数据洞察 ...]

    图片保存在 sandbox 的 /tmp/top_products.png。save_chart 这边是后台传输服务的问题,
    麻烦你过一会儿让我重试一次,或者刷新一下会话再试。要我现在再试一次吗?

第二次跑(连接已 warm up)就完全正常了。

cubemastercli 视角

cubemastercli 是 Cubesandbox 自带的 CLI 工具,用于管理模板、Sandbox 和节点。PydanticAI agent跟cubemastercli能看到同一件事的两个角度。可以清楚看到 sandbox 生命周期。cubemastercli 与 E2B SDK 的关系如下图所示,两者最终都通过 cubemaster 操作 cubelet,底层执行路径一致
#mermaid-svg-9bzK5sAwoMshP21f{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-9bzK5sAwoMshP21f .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9bzK5sAwoMshP21f .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9bzK5sAwoMshP21f .error-icon{fill:#552222;}#mermaid-svg-9bzK5sAwoMshP21f .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9bzK5sAwoMshP21f .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9bzK5sAwoMshP21f .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9bzK5sAwoMshP21f .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9bzK5sAwoMshP21f .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9bzK5sAwoMshP21f .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9bzK5sAwoMshP21f .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9bzK5sAwoMshP21f .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9bzK5sAwoMshP21f .marker.cross{stroke:#333333;}#mermaid-svg-9bzK5sAwoMshP21f svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9bzK5sAwoMshP21f p{margin:0;}#mermaid-svg-9bzK5sAwoMshP21f .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9bzK5sAwoMshP21f .cluster-label text{fill:#333;}#mermaid-svg-9bzK5sAwoMshP21f .cluster-label span{color:#333;}#mermaid-svg-9bzK5sAwoMshP21f .cluster-label span p{background-color:transparent;}#mermaid-svg-9bzK5sAwoMshP21f .label text,#mermaid-svg-9bzK5sAwoMshP21f span{fill:#333;color:#333;}#mermaid-svg-9bzK5sAwoMshP21f .node rect,#mermaid-svg-9bzK5sAwoMshP21f .node circle,#mermaid-svg-9bzK5sAwoMshP21f .node ellipse,#mermaid-svg-9bzK5sAwoMshP21f .node polygon,#mermaid-svg-9bzK5sAwoMshP21f .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9bzK5sAwoMshP21f .rough-node .label text,#mermaid-svg-9bzK5sAwoMshP21f .node .label text,#mermaid-svg-9bzK5sAwoMshP21f .image-shape .label,#mermaid-svg-9bzK5sAwoMshP21f .icon-shape .label{text-anchor:middle;}#mermaid-svg-9bzK5sAwoMshP21f .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9bzK5sAwoMshP21f .rough-node .label,#mermaid-svg-9bzK5sAwoMshP21f .node .label,#mermaid-svg-9bzK5sAwoMshP21f .image-shape .label,#mermaid-svg-9bzK5sAwoMshP21f .icon-shape .label{text-align:center;}#mermaid-svg-9bzK5sAwoMshP21f .node.clickable{cursor:pointer;}#mermaid-svg-9bzK5sAwoMshP21f .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9bzK5sAwoMshP21f .arrowheadPath{fill:#333333;}#mermaid-svg-9bzK5sAwoMshP21f .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9bzK5sAwoMshP21f .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9bzK5sAwoMshP21f .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9bzK5sAwoMshP21f .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9bzK5sAwoMshP21f .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9bzK5sAwoMshP21f .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9bzK5sAwoMshP21f .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9bzK5sAwoMshP21f .cluster text{fill:#333;}#mermaid-svg-9bzK5sAwoMshP21f .cluster span{color:#333;}#mermaid-svg-9bzK5sAwoMshP21f 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-9bzK5sAwoMshP21f .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9bzK5sAwoMshP21f rect.text{fill:none;stroke-width:0;}#mermaid-svg-9bzK5sAwoMshP21f .icon-shape,#mermaid-svg-9bzK5sAwoMshP21f .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9bzK5sAwoMshP21f .icon-shape p,#mermaid-svg-9bzK5sAwoMshP21f .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9bzK5sAwoMshP21f .icon-shape .label rect,#mermaid-svg-9bzK5sAwoMshP21f .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9bzK5sAwoMshP21f .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9bzK5sAwoMshP21f .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9bzK5sAwoMshP21f :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} REST
内部 API
gRPC
应用代码

e2b-code-interpreter SDK
cubemaster

:8089 内部 gRPC

:3000 E2B 兼容 REST
cubemastercli

命令行管理
cubelet

:9999

我们运行一个定时1s循环脚本查看sandbox详情

复制代码
# 列出sandbox的ID
cubemastercli ls -q

# 查看sandbox的详细info
cubemastercli info -s "$SBID" 2>/dev/null)

同时运行agent和脚本,跑完后提取关键事件时间线如下

  1. Sandbox.create()cubemastercli ls 能看到sandbox 创建过程很快
  2. 192.168.0.11 是 sandbox 内部 tap 网络的 IP。每次新建 sandbox 都从一个内网池分配
  3. status=running 全程不变 ,即使 LLM 那边在等模型响应,cubelet 视角下 sandbox 是 idle 但 running,等待下一个 run_code 调用
  4. Q1 → Q2 跨 37 秒,期间 cubemastercli 看到的就是同一个 sandbox,一次会话只有一个 sandbox,所有工具调用都打到它
shell 复制代码
[05:25:19 CLI] cubemastercli ls -> 0 sandbox                          # analyst.py 启动前
[05:25:20 APP] sandbox: 9aed036150994045bf5609c4096fc83d              # Sandbox.create() 完成
[05:25:20 APP] 上传文件 (留空跳过):   ✓ 上传到 /work/sales.csv          # files.write() 完成
[05:25:20 CLI] cubemastercli ls -> 9aed036150994045bf5609c4096fc83d  status=running sandbox_ip=192.168.0.11 # 立即观察到sandbox创建
[05:25:21 CLI] cubemastercli ls -> 9aed036150994045bf5609c4096fc83d  status=running sandbox_ip=192.168.0.11
[05:25:22 CLI] cubemastercli ls -> 9aed036150994045bf5609c4096fc83d  status=running sandbox_ip=192.168.0.11
[05:25:23 CLI] cubemastercli ls -> 9aed036150994045bf5609c4096fc83d  status=running sandbox_ip=192.168.0.11
...
[05:25:44 APP] AI: `sales.csv` 数据概况:                               # 第 1 个回答 (24s 后)
[05:26:21 APP] AI: 完成 ✅                                              # 第 2 个回答 (37s 后)
                  - sandbox: `/tmp/q1.png`                             #   图表已生成本地

其他常用的cubemastercli命令如下

shell 复制代码
# 节点
$ sudo cubemastercli node ls

# 模板
$ sudo cubemastercli tpl create-from-image --image <ref> --writable-layer-size 1G --expose-port 49999 --probe 49999
$ sudo cubemastercli tpl status --job-id <job>
$ sudo cubemastercli tpl watch --job-id <job>

$ sudo cubemastercli tpl ls

$ sudo cubemastercli tpl info --template-id <id>
$ sudo cubemastercli tpl info --template-id <id> --json
{
    "ret": { "ret_code": 200, "ret_msg": "success" },
    "template_id": "tpl-6f3b0a181586406db7620784",
    "instance_type": "cubebox",
    "version": "v2",
    "status": "READY",
    "replicas": [
        {
            "node_id": "172.31.24.141",
            "node_ip": "172.31.24.141",
            "instance_type": "cubebox",
            "spec": "cpu=2000m,mem=2000Mi",
            "snapshot_path": "/usr/local/services/cubetoolbox/cube-snapshot/cubebox/tpl-.../2C2000M",
            "status": "READY",
            "phase": "READY"
        }
    ]
}

$ sudo cubemastercli tpl redo --template-id <id> --failed-only --wait # 重试失败模板
$ sudo cubemastercli tpl delete --template-id <id> # 删除模板

# Sandbox 
$ sudo cubemastercli ls

$ sudo cubemastercli ls -w # 详细视图(含 template_id、annotations 等)

$ sudo cubemastercli ls -q # 只输出 sandbox_id

$ sudo cubemastercli ls --filter "status=running"

$ sudo cubemastercli info -s <sandbox-id> # 查 sandbox 详情
SANDBOX_ID        2c6ca28b2feb4abfbe8fe376c2f85754
STATUS            running
HOST_ID           172.31.24.141
HOST_IP           172.31.24.141
SANDBOX_IP        192.168.0.4
TEMPLATE_ID       tpl-6f3b0a181586406db7620784
NAMESPACE         default
ANNOTATIONS       {"cube.master.appsnapshot.template.id":"tpl-..."}
LABELS            {"cube.numa_node":"1","cube.master.instance.type":"cubebox",...}
CONTAINERS        1

$ sudo cubemastercli cubebox destroy <sandbox-id>
2026/05/29 03:28:01 doDestroySandbox RequestId:28a680fe-..., code:200, message:Success, cost:510
2026/05/29 03:28:01 destroyed: 2c6ca28b2feb4abfbe8fe376c2f85754
相关推荐
Aloudata1 小时前
语义层 vs 数据中台:轻量语义架构与重型中台路线的深度对比与选型建议
大数据·数据分析·agent·指标平台·数据中台
我爱cope1 小时前
【Agent智能体10 | 反思设计模式-AI数据分析的可视化实战】
人工智能·设计模式·数据分析
FIT2CLOUD飞致云1 小时前
加强安全防护,图表与仪表板功能优化,DataEase开源BI工具v2.10.23 LTS版本发布
数据分析·开源·数据可视化·dataease·bi
yyuuuzz1 小时前
aws亚马逊云上运维常见问题梳理
运维·服务器·网络·云计算·aws
imbackneverdie15 小时前
深耕医学科研智能化十年,MedPeer打造新一代AI生物医学科研操作系统
大数据·人工智能·ai·信息可视化·数据分析·aigc·科研
小白学大数据18 小时前
Playwright 爬虫:Python 爬取 JS 渲染的 JSP 网站
开发语言·javascript·爬虫·python·数据分析
AKAMAI19 小时前
针对 Akamai Cloud 上的 NVIDIA RTX Pro 6000 Blackwell 进行基准测试
云计算·gpu
亚林瓜子20 小时前
AWS S3日志桶常用过期文件生命周期策略
云计算·生命周期·aws·s3·过期·glacier
这个DBA有点耶20 小时前
数据库管理工具+开发工具的融合:AI如何重塑DBA工作流?
开发语言·数据库·人工智能·sql·云计算·dba