LangChain 集成指南
将调用 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)和同一个沙箱模板。
| 变体 | LangChain | langchain-openai | Python | Agent API | 示例路径 |
|---|---|---|---|---|---|
| 0.x(传统) | 0.3.x | 0.3.x | 3.9+ | AgentExecutor + create_react_agent | examples/langchain-integration/0.x |
| 1.x(现代) | 1.x | 1.x | 3.10+ | langchain.agents.create_agent + @tool | examples/langchain-integration/1.x |
若你在 Python 3.9 上,或已有 LangChain 0.3.x 代码 → 用 0.x。若在 Python 3.10+ 且从零开始 (或已在 LangChain 1.x 上)→ 用 1.x。
集成对象与版本
| 组件 | 0.x | 1.x |
|---|---|---|
| LangChain | langchain==0.3.23 | langchain>=1.3.14,<2.0 |
| langchain-openai | langchain-openai==0.3.12 | langchain-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.0 | cubesandbox>=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 集群节点。
cubesandboxSDK 经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 监听)。
# 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构建并推送:
docker build --platform linux/amd64 \
-t <your-registry>/langchain-cube:latest \
examples/langchain-integration
docker push <your-registry>/langchain-cube:latest2. 注册为 Cube 模板
cubemastercli tpl create-from-image \
--image <your-registry>/langchain-cube:latest \
--writable-layer-size 2G \
--expose-port 49983 \
--probe 49983 \
--probe-path /healthcreate-from-image 默认会阻塞并等待任务完成;如需提交后立即退出、稍后再轮询,请加 --detach, 之后用 cubemastercli tpl watch --job-id <job_id> 查看进度。
任务进入 READY 后,记下 template_id——每次 Sandbox.create() 都要传入它。2G 可写层适合中等 分析任务;若 Agent 运行时安装大体积包,可调到 4G+。
3. 安装依赖并配置环境变量
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_URL | cubesandbox SDK | CubeAPI 地址(http://<node>:3000) |
CUBE_API_KEY | cubesandbox SDK | (可选)X-API-Key 请求头;仅需在启用了鉴权的 CubeAPI 后端设置——鉴权关闭时 SDK 不发送任何认证头 |
CUBE_TEMPLATE_ID | Sandbox.create(template=...) | 来自步骤 2 |
CUBE_PROXY_NODE_IP | cubesandbox SDK | CubeProxy 节点 IP,使 *.cube.app 无需 DNS 解析 |
CUBE_PROXY_PORT_HTTP | cubesandbox SDK | 代理 HTTP 端口(默认 80;若代理监听 8081 则填 8081) |
OPENAI_API_KEY / TOKENHUB_API_KEY | LLM 客户端 | OpenAI 兼容密钥 |
OPENAI_BASE_URL | LLM 客户端 | 如 https://tokenhub.tencentmaas.com/v1 |
CHAT_MODEL | LLM 客户端 | 如 deepseek-v3 |
CUBE_SSL_CERT_FILE | demo | (可选)HTTPS CubeAPI 的自签名 CA 证书路径。demo 会将其导出为 SSL_CERT_FILE(进程全局)以供 gRPC 客户端信任——该 bundle 应同时包含公共根证书,否则 LLM 端点也会受影响 |
4. 把代码执行工具接到 Cube
Agent 逻辑在各版本间不变,只是工具体从本地 REPL 换成 Cube 沙箱。下面这段是 run_python 工具的 参考实现——基于官方 cubesandbox Python SDK(from cubesandbox import Sandbox), 用 sandbox.files.write 上传代码、sandbox.commands.run 执行,因此 无需裸 HTTP。 (已发布示例 0.x/1.x 在 build_agent 内部内联了等价逻辑。)
# 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)
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)
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"])运行(在所选变体目录下):
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:
- 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.create、commands.run、files.write、kill)形态保持一致, 因此工具的其他代码无需改动。上面的示例正是使用这个 SDK。
本地基线(被替换的部分)
# 改造前:代码经 langchain_experimental 在宿主机运行
from langchain_experimental.tools import PythonREPLTool
tools = [PythonREPLTool()]把 PythonREPLTool 换成上面的 run_python Cube 工具,即可免费获得隔离能力。
注意事项
- envd 用户。
cubesandboxSDK 的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:
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):
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):
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() 即可:
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 denied | envd 用户不匹配 | 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 refused | CUBE_API_URL 错误 | 设为 http://<node>:3000 |
模板卡在 PULLING | 集群无法访问仓库 | 推送到集群可访问的仓库 |
run_python 无返回 | 脚本没有 stdout | 让 Agent 显式 print() 结果 |
参考资料
- 可运行示例 ——
0.x(LangChain 0.3.x):examples/langchain-integration/0.x·1.x(LangChain 1.x):examples/langchain-integration/1.x - 自带镜像:
docs/guide/tutorials/bring-your-own-image.md - 从镜像创建模板:
docs/guide/tutorials/template-from-image.md - 快照 / 克隆 / 回滚:
docs/guide/snapshot-rollback-clone.md - 凭证保管 + 出口控制:
docs/guide/security-proxy.md - LangChain:https://github.com/langchain-ai/langchain
- E2B SDK:https://github.com/e2b-dev/e2b
langchain_e2b(E2B 官方 LangChain 集成包):https://github.com/e2b-dev/langchain_e2b