Skip to content

LangChain 集成指南

English

将调用 Python 工具的 LangChain Agent 运行在 CubeSandbox 的 MicroVM 中。由于 Cube 暴露了 与 E2B 兼容的 API,把 LangChain 应用从 E2B 迁移到 Cube 通常只需改几个环境变量,同时还能为 Agent 生成的每一行代码获得 KVM 级隔离。本文附带可运行的 examples/langchain-integration 示例工程。

两个 LangChain 大版本(0.x 与 1.x)

本指南提供两个变体,仅在 LangChain 的 Agent API 与所需 Python 版本上不同。两者共用同一个 run_python 工具(基于官方 cubesandbox SDK)和同一个沙箱模板。

变体LangChainlangchain-openaiPythonAgent API示例路径
0.x(传统)0.3.x0.3.x3.9+AgentExecutor + create_react_agentexamples/langchain-integration/0.x
1.x(现代)1.x1.x3.10+langchain.agents.create_agent + @toolexamples/langchain-integration/1.x

若你在 Python 3.9 上,或已有 LangChain 0.3.x 代码 → 用 0.x。若在 Python 3.10+ 且从零开始 (或已在 LangChain 1.x 上)→ 用 1.x

集成对象与版本

组件0.x1.x
LangChainlangchain==0.3.23langchain>=1.3.14,<2.0
langchain-openailangchain-openai==0.3.12langchain-openai>=1.0,<2.0
LangGraph(create_agent 运行时)—(未使用;AgentExecutor + create_react_agent 不依赖 LangGraph)langgraph>=0.2(已在 1.x/requirements.txt 显式声明,保证示例自包含)
cubesandbox SDK(主驱动)cubesandbox>=0.6.0cubesandbox>=0.6.0
CubeSandbox 基础镜像ghcr.io/tencentcloud/cubesandbox-base:2026.16相同
CubeSandbox 平台>= 0.3.0(核心);可选特性见下表>= 0.3.0(核心);可选特性见下表

各特性的平台最低版本(上面的 SDK 下限覆盖基础工作流;仅当你使用该特性时才提高 cubesandbox/平台版本):

特性最低平台最低 SDK
基础工作流(通过 49983 端口的 envd 进程 API 调用 commands.run / files.write、上下文管理器销毁)>= 0.3.0>= 0.6.0
CubeEgress 密钥注入(高级用法,默认拒绝出口流量)>= 0.4.0>= 0.3.0
卷挂载(volume_mounts=Volume API)>= 0.6.0>= 0.6.0

前置条件

  • 已部署 CubeSandbox,CubeAPI 可从 http://<node>:3000 访问。
  • cubemastercli 已在 $PATH 且已连通集群。
  • 构建机装有 Docker,且 registry 能被 Cube 节点拉取。
  • Python 3.10+(1.x 变体)。0.x 变体在 Python 3.9+ 上也可运行。
  • 能访问 Cube 集群节点。cubesandbox SDK 经 CUBE_API_URL 访问 CubeAPI、经 CUBE_PROXY_NODE_IP 访问数据面(使 *.cube.app 无需 DNS 即可解析);若代理未监听 80 端口,需设置 CUBE_PROXY_PORT_HTTP
  • 一个 OpenAI 兼容的 LLM 端点(示例使用 TokenHub;任何 OpenAI 兼容端点均可经 OPENAI_BASE_URL / OPENAI_API_KEY 接入)。

为什么把 LangChain Agent 放进沙箱

LangChain Agent 经常会暴露代码执行工具(数据分析、文件转换、调用 shell)。在宿主机上运行该工具, 会把 Agent 的影响范围与你的开发机混在一起。放进 CubeSandbox 后,你将获得:

关注点CubeSandbox 提供
隔离每个会话一个 KVM MicroVM,独立客户机内核——Agent 代码无法触碰宿主机
可复现每次会话都从同一个模板快照启动
快速启动冷启动低于 60ms,因此并行跑多个 Agent 成本很低
长任务sandbox.pause() 快照 VM + 根文件系统,稍后恢复
密钥卫生CubeEgress 可在网络层注入 LLM 鉴权头——VM 内永远看不到真实密钥
出口审计每一次出站请求都会记录到出口审计日志

接入步骤

1. 构建模板镜像

cubesandbox-base 之上叠加 Python 数据科学栈(envd 已在 :49983 监听)。

dockerfile
# examples/langchain-integration/Dockerfile
ARG CUBE_BASE_IMAGE=ghcr.io/tencentcloud/cubesandbox-base:2026.16
FROM ${CUBE_BASE_IMAGE}

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        python3 python3-pip ca-certificates git \
    && rm -rf /var/lib/apt/lists/*

# 为可复现构建锁定版本,按需升级
RUN python3 -m pip install --no-cache-dir --upgrade pip \
    && python3 -m pip install --no-cache-dir --break-system-packages \
        pandas==2.2.3 numpy==1.26.4 matplotlib==3.9.3 scikit-learn==1.6.1

WORKDIR /workspace

# 预置演示数据集,使示例 Agent 无需外部输入即可运行
COPY sales.csv /workspace/sales.csv

EXPOSE 49983

构建并推送:

bash
docker build --platform linux/amd64 \
  -t <your-registry>/langchain-cube:latest \
  examples/langchain-integration
docker push <your-registry>/langchain-cube:latest

2. 注册为 Cube 模板

bash
cubemastercli tpl create-from-image \
  --image <your-registry>/langchain-cube:latest \
  --writable-layer-size 2G \
  --expose-port 49983 \
  --probe       49983 \
  --probe-path  /health

create-from-image 默认会阻塞并等待任务完成;如需提交后立即退出、稍后再轮询,请加 --detach, 之后用 cubemastercli tpl watch --job-id <job_id> 查看进度。

任务进入 READY 后,记下 template_id——每次 Sandbox.create() 都要传入它。2G 可写层适合中等 分析任务;若 Agent 运行时安装大体积包,可调到 4G+

3. 安装依赖并配置环境变量

bash
cd examples/langchain-integration/1.x      # 或 0.x,取决于你的 LangChain 版本
cp ../.env.example .env
# 填写 CUBE_API_URL、CUBE_API_KEY、CUBE_TEMPLATE_ID、CUBE_PROXY_NODE_IP 以及你的 LLM 密钥
pip install -r requirements.txt
变量流向说明
CUBE_API_URLcubesandbox SDKCubeAPI 地址(http://<node>:3000
CUBE_API_KEYcubesandbox SDK(可选)X-API-Key 请求头;仅需在启用了鉴权的 CubeAPI 后端设置——鉴权关闭时 SDK 不发送任何认证头
CUBE_TEMPLATE_IDSandbox.create(template=...)来自步骤 2
CUBE_PROXY_NODE_IPcubesandbox SDKCubeProxy 节点 IP,使 *.cube.app 无需 DNS 解析
CUBE_PROXY_PORT_HTTPcubesandbox SDK代理 HTTP 端口(默认 80;若代理监听 8081 则填 8081
OPENAI_API_KEY / TOKENHUB_API_KEYLLM 客户端OpenAI 兼容密钥
OPENAI_BASE_URLLLM 客户端https://tokenhub.tencentmaas.com/v1
CHAT_MODELLLM 客户端deepseek-v3
CUBE_SSL_CERT_FILEdemo(可选)HTTPS CubeAPI 的自签名 CA 证书路径。demo 会将其导出为 SSL_CERT_FILE(进程全局)以供 gRPC 客户端信任——该 bundle 应同时包含公共根证书,否则 LLM 端点也会受影响

4. 把代码执行工具接到 Cube

Agent 逻辑在各版本间不变,只是工具体从本地 REPL 换成 Cube 沙箱。下面这段是 run_python 工具的 参考实现——基于官方 cubesandbox Python SDKfrom cubesandbox import Sandbox), 用 sandbox.files.write 上传代码、sandbox.commands.run 执行,因此 无需裸 HTTP。 (已发布示例 0.x/1.x 在 build_agent 内部内联了等价逻辑。)

python
# run_python 工具 —— 官方 cubesandbox SDK(以下 snippet 使用此实现)
import itertools
from cubesandbox import Sandbox

def make_run_python(sandbox: Sandbox):
    """返回一个绑定到已创建 `sandbox` 的 run_python 工具。"""
    script_counter = itertools.count()
    def run_python(code: str) -> str:
        """在 Cube Sandbox MicroVM 内执行 Python;返回 stdout,若存在 stderr 则以分隔符附在其后。

        镜像已预装 pandas / numpy / matplotlib / scikit-learn。每次调用把代码片段写入唯一的
        /workspace/_agent_<n>.py 并运行,避免并发的工具调用互相覆盖。图表可保存到
        /workspace(如 /workspace/revenue.png)。
        """
        script = f"/workspace/_agent_{next(script_counter)}.py"
        sandbox.files.write(script, code)
        result = sandbox.commands.run(f"python3 {script}", timeout=120, cwd="/workspace")
        out = result.stdout
        # 用分隔符把 stderr 与 stdout 区分开,避免库警告(退出码为 0)混入
        # LLM 看到的真实输出。
        if result.stderr:
            out += "\n--- stderr ---\n" + result.stderr
        if result.exit_code != 0:
            out += f"\n[非零退出码: {result.exit_code}]"
        return out
    return run_python

一个沙箱跑完整轮、自动销毁。 整个 Agent 循环只创建一个 MicroVM(Sandbox.create(template=...)), 并在 with 代码块退出时由上下文管理器调用 sandbox.kill()DELETE /sandboxes/:sandboxID), 因此不会泄漏沙箱。随附示例正是采用这一模式。

5. 运行 Agent

两个变体都构建同一个 ChatOpenAI 客户端并复用共用的 run_python 工具,区别仅在 Agent 的构建方式。为整轮运行创建一个 Sandbox,并用 make_run_python(sandbox) 把工具绑定上去:

1.x(现代 —— langchain.agents.create_agent

python
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain_core.tools import tool
from cubesandbox import Sandbox

load_dotenv()
_llm_key = os.getenv("OPENAI_API_KEY") or os.getenv("TOKENHUB_API_KEY")
if not _llm_key:
    raise SystemExit("Missing LLM API key (set OPENAI_API_KEY or TOKENHUB_API_KEY)")

llm = ChatOpenAI(
    model=os.getenv("CHAT_MODEL") or "deepseek-v3",
    api_key=_llm_key,
    base_url=os.getenv("OPENAI_BASE_URL") or "https://tokenhub.tencentmaas.com/v1",
    timeout=60,
    max_retries=2,
    temperature=0,
)

SANDBOX_CONTEXT = (
    "You are a data analyst. You can execute Python inside a Cube Sandbox "
    "MicroVM via the run_python tool. Environment facts:\n"
    "- Working directory: /workspace\n"
    "- Demo dataset: /workspace/sales.csv with columns month,product,units,price\n"
    "  (6 rows: 3 months x 2 products; revenue is defined as units * price)\n"
    "- Preinstalled: pandas, numpy, matplotlib, scikit-learn\n"
    "- Save any charts/artifacts under /workspace\n"
    "When the user mentions 'the dataset' without a path, use /workspace/sales.csv.\n"
    "Modeling conventions for this tiny demo dataset (follow them unless the "
    "user explicitly specifies otherwise):\n"
    "- Regression/forecast target: monthly TOTAL revenue. Aggregate to one row "
    "per month, then use a numeric month index (0, 1, 2, ...) as the only "
    "feature.\n"
    "- Never use the target itself or its direct components (units, price) as "
    "features when predicting revenue - that is data leakage and yields a "
    "meaningless RMSE of 0.\n"
    "- The dataset is too small for a train/test split; fit and evaluate on "
    "all rows and explicitly state that the metric is in-sample.\n"
    "- Report only numbers actually printed by the executed code; never "
    "invent or estimate metric values."
)

with Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"], timeout=600) as sandbox:
    run_python = tool(make_run_python(sandbox))
    agent = create_agent(llm, [run_python], system_prompt=SANDBOX_CONTEXT)
    result = agent.invoke({"messages": [{"role": "user", "content":
        "从 /workspace 读取 sales.csv,按月计算总营收,并在最终回答中报告各月营收数字。"}]})
    for msg in reversed(result["messages"]):
        if msg.content:
            print(msg.content)
            break
    else:
        print("(no final answer in messages)")

0.x(传统 —— AgentExecutor + create_react_agent

python
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import Tool
from langchain_core.prompts import PromptTemplate
from cubesandbox import Sandbox

load_dotenv()
_llm_key = os.getenv("OPENAI_API_KEY") or os.getenv("TOKENHUB_API_KEY")
if not _llm_key:
    raise SystemExit("Missing LLM API key (set OPENAI_API_KEY or TOKENHUB_API_KEY)")

llm = ChatOpenAI(  # 与上面相同的参数
    model=os.getenv("CHAT_MODEL") or "deepseek-v3",
    api_key=_llm_key,
    base_url=os.getenv("OPENAI_BASE_URL") or "https://tokenhub.tencentmaas.com/v1",
    timeout=60, max_retries=2, temperature=0,
)

with Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"], timeout=600) as sandbox:
    run_python = make_run_python(sandbox)
    tools = [Tool(name="run_python", func=run_python,
                  description="在 Cube Sandbox MicroVM 中执行 Python 代码。")]
    agent = create_react_agent(llm, tools, PromptTemplate.from_template(
        "You are a data analyst. Answer the question using tools.\n\n"
        "Environment facts:\n"
        "- Demo dataset: /workspace/sales.csv with columns month,product,units,price\n"
        "  (6 rows: 3 months x 2 products; revenue is defined as units * price)\n"
        "- Preinstalled: pandas, numpy, matplotlib, scikit-learn\n"
        "- Save any charts/artifacts under /workspace\n"
        "When the user mentions 'the dataset' without a path, use /workspace/sales.csv.\n"
        "Modeling conventions (unless the user specifies otherwise):\n"
        "- Regression target: monthly TOTAL revenue; aggregate per month and use "
        "a numeric month index (0, 1, 2, ...) as the only feature.\n"
        "- Never use the target or its components (units, price) as features - "
        "that is data leakage and yields a meaningless RMSE of 0.\n"
        "- Too small for a train/test split; evaluate in-sample and say so.\n"
        "- Report only numbers actually printed by the executed code.\n\n"
        "Tools: {tools}\n\n"
        "Use the following format:\n\n"
        "Question: the input question you must answer\n"
        "Thought: you should always think about what to do\n"
        "Action: the action to take, should be one of [{tool_names}]\n"
        "Action Input: the input to the action\n"
        "Observation: the result of the action\n"
        "... (this Thought/Action/Action Input/Observation can repeat N times)\n"
        "Thought: I now know the final answer\n"
        "Final Answer: the final answer to the original input question\n\n"
        "Begin!\n\n"
        "Question: {input}\n\nThought: {agent_scratchpad}"))
    executor = AgentExecutor(agent=agent, tools=tools, verbose=True, handle_parsing_errors=True)
    result = executor.invoke({"input":
        "从 /workspace 读取 sales.csv,按月计算总营收,并在最终回答中报告各月营收数字。"})
    print(result["output"])

运行(在所选变体目录下):

bash
python langchain_agent_demo.py
# 自定义问题:
python langchain_agent_demo.py "在数据集上训练一个线性模型并报告 RMSE。"

预期效果

使用默认提示词时,宿主机上打印的 Agent 最终回答会包含各月营收数字 (2780.0 / 3375.5 / 3872.0)。(1.x 的 create_agent 默认不显示详细过程;0.x 的 AgentExecutor(verbose=True) 还会把完整推理过程打印到控制台。)

销毁是尽力而为:若 kill() 失败(例如沙箱已过期),SDK 的上下文管理器会静默吞掉错误——不会打印 任何警告。如果你需要感知销毁失败,请显式调用 sandbox.kill() 并自行处理 CubeSandboxError

关键代码片段

把 E2B 应用迁移到 Cube

如果你的 LangChain 应用已经在驱动 E2B 沙箱,代码几乎完全一致——只要把 import 换成 cubesandbox SDK,并指向 CubeAPI:

diff
- from e2b_code_interpreter import Sandbox
- # E2B 云端(托管)
- export E2B_API_KEY="e2b_xxx"
- export E2B_API_URL="https://api.e2b.dev"   # 默认值,通常省略
+ from cubesandbox import Sandbox
+ # Cube Sandbox(自托管,MicroVM 隔离)
+ export CUBE_API_URL="http://<your-cube-host>:3000"
+ # export CUBE_API_KEY="<your-api-key>"  # 仅用于启用了鉴权的后端
+ export CUBE_TEMPLATE_ID="<your-cube-template-id>"
+ export CUBE_PROXY_NODE_IP="<your-cube-host>"

Sandbox 的 API(Sandbox.createcommands.runfiles.writekill)形态保持一致, 因此工具的其他代码无需改动。上面的示例正是使用这个 SDK。

本地基线(被替换的部分)

python
# 改造前:代码经 langchain_experimental 在宿主机运行
from langchain_experimental.tools import PythonREPLTool
tools = [PythonREPLTool()]

PythonREPLTool 换成上面的 run_python Cube 工具,即可免费获得隔离能力。

注意事项

  • envd 用户。 cubesandbox SDK 的 files.write / commands.run 默认以 root 运行, 无需额外配置。(基础镜像还预置了一个非 root 的 uid=1000 用户;若让 SDK 以该用户运行, 文件权限行为会随之变化。)
  • 清理安全。 with Sandbox.create(...) as sandbox: 上下文管理器在退出时调用 sandbox.kill()DELETE /sandboxes/:sandboxID没有 /kill 子路径),即使 Agent 抛异常也不会泄漏沙箱。
  • 一个沙箱跑完整轮、跨调用复用。 整个 Agent 循环只创建一个 MicroVM,并在每次 run_python 调用间 复用,因此只需支付一次生命周期开销。
  • 状态不跨轮次保留。 commands.run 每次都是全新的 python 进程,变量/导入不会在工具调用间存续。 把片段需要的一切内联进去,或把中间状态写到 /workspace 再读回。
  • 模板必须包含所需栈。 把 pandas/numpy/matplotlib 预装进镜像;在默认拒绝出口策略下,运行时 pip install 会失败。
  • 超时。 同时设置沙箱 timeout(平台回收)和单次命令 timeout;长 Agent 循环可能耗尽任意一个。

进阶用法

网络隔离 + 密钥注入(原生 SDK)

在共享集群上,优先使用原生 cubesandbox SDK,配合默认拒绝出口与网络层密钥注入,让 LLM 密钥永 不进入 VM:

python
import os
from cubesandbox import Sandbox, Rule, Match, Action, Inject

rules = [
    Rule(
        name="allow_llm",
        match=Match(scheme="https", sni="api.openai.com", host="api.openai.com"),
        action=Action(allow=True, audit="metadata", inject=[
            Inject(header="Authorization",
                   secret=os.environ["OPENAI_API_KEY"],
                   format="Bearer ${SECRET}"),
        ]),
    ),
]
with Sandbox.create(
    template=os.environ["CUBE_TEMPLATE_ID"],
    timeout=600,
    allow_internet_access=False,           # 默认拒绝;规则中的主机自动放行
    network={"rules": rules},
) as sandbox:
    run_agent(sandbox)                     # 退出 with 块时执行 sandbox.kill()

文件挂载

Sandbox.create() 没有 mounts 参数,请改用 Cube 特有的 API:

  • 宿主机目录挂载通过 metadata["host-mount"] 传入(一个 JSON 编码的 {hostPath, mountPath, readOnly} 描述符列表),让 Agent 直接读写宿主机共享数据, 无需经 files.write 来回搬运。注意 hostPath 必须位于允许的前缀之下 (默认仅 /data/shared/;如需其他路径,请先在 CubeMaster 配置中扩展 allowed_host_mount_prefixes):
python
import json
from cubesandbox import Sandbox

sandbox = Sandbox.create(
    template=os.environ["CUBE_TEMPLATE_ID"],
    metadata={
        "host-mount": json.dumps([
            {"hostPath": "/data/shared/datasets", "mountPath": "/workspace/datasets",
             "readOnly": True},
        ]),
    },
)
  • 卷挂载使用 volume_mounts={挂载路径: 卷ID}(参见 examples/volume):
python
from cubesandbox import Sandbox, Volume

vol = Volume.create("my-workspace")
sandbox = Sandbox.create(
    template=os.environ["CUBE_TEMPLATE_ID"],
    volume_mounts={"/workspace": vol},
)

长任务暂停 / 恢复

pause() 返回 None——沙箱 ID 不会改变。恢复时对同一 ID 调用 Sandbox.connect() 即可:

python
sandbox = Sandbox.create(template=os.environ["CUBE_TEMPLATE_ID"], timeout=1800)
try:
    turn_1(sandbox)
    sandbox.pause()
    sandbox = Sandbox.connect(sandbox.sandbox_id)   # 恢复后 /workspace 完好
    turn_2(sandbox)
finally:
    sandbox.kill()

故障排查

现象可能原因解决办法
文件操作 permission deniedenvd 用户不匹配cubesandbox SDK 的 files.write / commands.run 默认以 root 运行。若使用 E2B SDK,请设置 e2b.envd.rpc.default_username = "root"
command not found: python模板缺少 Python重新构建包含 python3 的镜像
ModuleNotFoundError: pandas镜像未含该栈在 Dockerfile 中加入 pandas/numpy/matplotlib
403 Forbidden - CubeEgress默认拒绝且无放行规则放行 LLM 主机(及其他所需主机)
连接 CubeAPI Connection refusedCUBE_API_URL 错误设为 http://<node>:3000
模板卡在 PULLING集群无法访问仓库推送到集群可访问的仓库
run_python 无返回脚本没有 stdout让 Agent 显式 print() 结果

参考资料