当前位置: 首页 > news >正文

在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 内的服务
flowchart TBsubgraph 宿主进程NA["network-agent<br/>unix:///tmp/cube/network-agent-grpc.sock"]CM[cubemaster<br/>tcp :8089]CA[cube-api<br/>tcp :3000<br/>E2B 兼容 REST]CL[cubelet<br/>tcp :9999 :9998]endsubgraph Docker_ComposeMY[(MySQL :3306)]RD[(Redis :6379)]CP[cube-proxy<br/>:443 :80]CD[cube-proxy-coredns<br/>cube.app 域名解析]endSDK[E2B Python SDK] -->|HTTP| CACA -->|gRPC| CMCM -->|gRPC :9999| CLCL -->|启动时初始化网络| NACM --> MYCM --> RDCA -.->|sandbox URL<br/>*.cube.app| CPCP --> CDstyle NA fill:#fff3e0style CL fill:#fff3e0style CA fill:#e8f5e9

环境初始化

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

flowchart LRA[物理 CPU<br/>Intel VT-x / AMD-V] -->|裸金属直接跑| B["/dev/kvm 可用"]A -->|经 Hypervisor 一层<br/>Hypervisor 暴露 vmx 给 Guest| C[嵌套虚拟化]C -->|Guest 内仍可见 vmx| BC -.->|Hypervisor 不支持嵌套| D["/dev/kvm 不可用"]B --> E[Cubesandbox 可跑]D --> F[Cubesandbox 跑不起来]style B fill:#c8e6c9style D fill:#ffcdd2style E fill:#c8e6c9style F fill:#ffcdd2

安装 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 = Unimplementeddesc = 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=trueendpoint=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 等关键服务全部加载失败,时序图如下:

sequenceDiagramparticipant U as up.shparticipant NA as network-agent 进程participant SK as "/tmp/cube/network-agent-grpc.sock"participant CL as cubelet 进程U->>NA: nohup network-agent &Note over NA: 进程启动但还在做<br/>初始化 ~60sU->>CL: nohup cubelet & (立即)CL->>SK: connect (尝试 1)SK-->>CL: ENOENTCL->>CL: sleep 4s, retry (×8)Note over CL: 累计 30s 超时CL->>CL: FATAL: network plugin init failedCL->>CL: 后续依赖网络的插件全部失败NA->>SK: 终于 listen 上 (晚了)Note over CL: 但 cubelet 不会重新 init 插件

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

sudo pkill -9 -f "/Cubelet/bin/cubelet"
sleep 2
sudo ls -la /tmp/cube/network-agent-grpc.socksudo 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 Sandboxprint(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.12Hostname: tpl-XXXXKernel: 6.6.1199-0009-03_2.0.1CPU count: 2IP: 127.0.0.1']=== Test 3: Math ===
stdout: ['499999500000\n']

使用sandbox分析数据

接下来我们使用PydanticAI集成sandbox的能力,实现自然语言提问 ,AI 写代码 ,Cubesandbox 运行代码 ,产出图表的全过程。整体架构如下:

flowchart LRUSER[用户]CLI[analyst.py CLI]AGENT["PydanticAI Agent<br/>claude-sonnet-4-5"]SBSDK["E2B SDK<br/>e2b-code-interpreter"]CB[Cubesandbox 集群]LOCAL[(本地 ./output/<br/>图表落盘)]USER -->|输入问题/上传 CSV| CLICLI --> AGENTAGENT -->|LLM 决策<br/>写 Python 代码| AGENTAGENT -->|工具调用<br/>run_python / save_chart| SBSDKSBSDK -->|HTTP :3000<br/>E2B 兼容协议| CBCB -->|执行结果<br/>stdout/stderr/files| SBSDKSBSDK --> AGENTAGENT -->|文字答案| CLIAGENT -.->|读取图表| SBSDKSBSDK -.->|写入本地| LOCALCLI --> USERstyle CB fill:#fff3e0style AGENT fill:#e8f5e9

首先我们确认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.pemls -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 Annotatedfrom 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: Sandboxuploaded_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.stdoutreturn ''.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).namelocal.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:breakp = Path(f).expanduser()if not p.exists():print(f"  ! 文件不存在")continuesb_path = f"/work/{p.name}"sandbox.files.write(sb_path, p.read_bytes())deps.uploaded_files[str(p)] = sb_pathprint(f"  ✓ 上传到 {sb_path}")# 对话循环(message_history 保留多轮上下文)history = []print("输入 'exit' 退出")while True:q = input("你: ").strip()if q in ('exit', 'quit', ''):breaktry: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,底层执行路径一致

flowchart LRAPP[应用代码<br/>e2b-code-interpreter SDK]CM[cubemaster<br/>:8089 内部 gRPC<br/>:3000 E2B 兼容 REST]CLI[cubemastercli<br/>命令行管理]CL[cubelet<br/>:9999]APP -->|REST| CMCLI -->|内部 API| CMCM -->|gRPC| CLstyle APP fill:#e8f5e9style CLI fill:#e1f5festyle CM fill:#fff3e0

我们运行一个定时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
http://www.rkmt.cn/news/1421629.html

相关文章:

  • HS2-HF Patch:解锁Honey Select 2的终极游戏体验指南
  • AI绘画工具横评:模型能力与实际表现
  • 上海卖钻戒别乱找!2026年5月亲测3家平台,靠谱渠道整理好了 - 合扬奢侈品交易中心
  • OBS LocalVocal:如何实现完全本地的实时字幕和翻译解决方案
  • 广州黄金回收避坑5大套路|2026最新防骗手册(全市免费上门) - 行行星
  • 2026年,AI驱动的求职工具如何助你光速斩获Offer?5大平台实测对比
  • 沉香木哪个牌子好?实地体验助力消费选择 - 速递信息
  • Seedance 2.0 开启 2K 输出后,我实测了一轮:画质确实更细,但时间成本也上来了
  • 第23篇|深浅色适配:颜色资源不是装饰,而是可维护系统
  • 2026沃尔玛购物卡回收实测测评!4大正规平台对比,按需选不踩坑 - 博客万
  • 从AD/ADS转战Cadence OrCAD 17.4:一个电磁场硕士的软件迁移实战笔记(附新建工程踩坑点)
  • WTG系统用着用着蓝屏了?别慌,这可能是你热插拔U盘惹的祸(附系统重置与文件抢救指南)
  • 基于ESP32与SA818模块构建可编程2米波段无线电实验平台
  • 基于MOSFET的防反接保护电路:原理、设计与实战
  • 石榴花开映槐荫、和融同心润民生
  • 2026 年九华山好吃徽菜馆口碑推荐榜:九华山必吃美食、九华山农家土菜、九华山实惠餐饮、九华山必打卡的土菜馆选择指南,食材、口味、服务三维度权威解析 - 海棠依旧大
  • 微信怎么发起投票功能【新手实测简单教程】 - 微信投票小程序
  • Fluent仿真翻车实录:用了NIST真实气体模型,结果却不收敛?这7个坑我帮你踩过了
  • 基于ESP32与MAX7219的复古LED点阵机架显示器DIY全攻略
  • 质量流量计:无需温压补偿,质量流量计直接测量流体质量 - 仪表人叶工
  • 吉安黄金回收门店实测|2026 实时金价 + 上门 / 到店服务对比 + 真实体验案例 - 润富黄金珠宝行
  • 树莓派+热敏打印机打造自动日历打印终端:物联网与Python自动化实践
  • 第四封信:谈人工智能时代的教育
  • DroidCam OBS插件终极指南:5步将手机摄像头变为专业直播设备
  • 付费投流硬控互联网
  • Kubernetes节点亲和性与调度策略优化
  • 双行星搅拌机工作原理与匀浆机制:解密高效混合的核心密码 - 上海奎特机电
  • ESP32-S3边缘AI实践:Teachable Machine模型部署与离线图像识别
  • 体验Taotoken旗舰模型Qwen3.7在代码生成任务中的响应速度与稳定性
  • 2026 年吉安黄金回收行业解析|实时金价 + 正规门店名录 + 市民变现案例合集 - 润富黄金珠宝行