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 内的服务

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 缺失则需要停机检查环境,停止后续的步骤。

$ 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或物理机的环境,具体的判断逻辑如下

裸金属直接跑

经 Hypervisor 一层
Hypervisor 暴露 vmx 给 Guest

Guest 内仍可见 vmx

Hypervisor 不支持嵌套

物理 CPU
Intel VT-x / AMD-V

/dev/kvm 可用

嵌套虚拟化

/dev/kvm 不可用

Cubesandbox 可跑

Cubesandbox 跑不起来

安装 Docker

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命令,因此需要提前安装好

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 用户下执行官方文档命令中的如下命令

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 进程启动但还在做 初始化 ~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即可

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见模板创建部分)

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

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 就绪后直接创建模板成功

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

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

sudo cubemastercli tpl watch --job-id <JOB_ID>

E2B SDK 代码

安装 SDK

sudo dnf install -y python3 python3-pip
pip3 install e2b-code-interpreter

准备 mkcert CA:

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

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

测试脚本:

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 运行代码 ,产出图表的全过程。整体架构如下:

输入问题/上传 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集群就绪

# 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

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

"""
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 单例实现跨调用持久化

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')检查如下源码

# 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:

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%,消费力较强

统计图结果如下

image-20260529120105613

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

热力图结果

image-20260529120033984

错误重试

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

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,底层执行路径一致

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,所有工具调用都打到它
[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命令如下

# 节点
$ 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
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐