构建
开发者工作区
位于 /notebooks 的 Python 笔记本,在沙箱化的服务端内核上执行——真实的 CPython,预装 pip 与各智能体框架。可以从只读的框架示例出发,也可以自己写,并通过你的账号调用模型与知识库。
框架示例
工作区自带四个只读示例笔记本,位于 从示例中学习:
- LangChain 基础 ——模型、提示词模板、输出解析器、LCEL 链、工具与智能体、记忆,以及 RAG。
- LangGraph 基础 ——状态、节点、边、条件路由、环、人在环中,以及多智能体监督者。
- LlamaIndex 基础 ——文档、节点、索引、检索器,以及查询引擎。
- 综合运用 — 智能体技术栈 ——使用你的知识库、构建工具、组合技能、施加护栏、访问 MCP 服务,并搭起一个小型多智能体 RAG 服务。
langchain, langgraph 和 llama_index 导入语句,在容器内核上执行。模型调用经由 zenstudio.chat_model(),一个真正的 LangChain BaseChatModel ,它通过平台路由,因此你的 IAM 模型规则、预算和追踪都会生效,而且 沙箱内部从不存在任何提供商密钥。四个示例在发布前都会以 CI 方式端到端执行验证。只读与复刻
示例可以 运行 逐单元格执行,但不能编辑。点击 复刻到我的笔记本 即可把示例复制成一个归你账号所有、可编辑的笔记本——随意改动,自动保存。你自己的笔记本位于 我的笔记本 ,且仅你可见。
单元格如何执行
- 每个代码单元格都是一个真实编辑器(CodeMirror)——用播放按钮或 Shift+Enter运行。stdout、最后一个表达式的值、错误和执行耗时都显示在单元格下方。
- 单元格执行在一个 沙箱化容器内核 上——非 root 运行、根文件系统只读、能力集被剥离、出网限定在允许清单内。内核在你首次运行时启动,空闲后被回收。
- 所有单元格共用一个解释器,与 Jupyter 一致:某个单元格里定义的变量在下一个里可见,且支持顶层
await。 - LangChain、LangGraph、LlamaIndex、pandas 和 numpy 已预装;其他任何包只需一条
!pip install即可(写入落在每会话独立的可写层中)。
zenstudio 辅助模块
所谓 zenstudio 模块会注入到每个笔记本中。它把调用路由到你的账号——你的提供商密钥、你的 IAM 模型规则、你的预算,并记录在 追踪记录:
reply = await zenstudio.chat("…", provider="openrouter", model="openai/gpt-4o-mini")——通过你在此处接入的任意 OpenAI 兼容提供商完成一次对话 集成 (openrouter、openai、gemini、groq、grok、qwen、ollama、vllm、nvidia)。hits = await zenstudio.kb_search("refund policy", top_k=5)——在你的 知识库上做混合检索(向量 + 关键词)。访问权限与别处遵循同一套规则:你只能检索自己拥有、被授权,或属于公开示例的知识库。kbs = await zenstudio.list_knowledge_bases()——你的账号可读取的知识库。zenstudio.format_context(hits)——把检索结果整理成带编号的上下文块,供提示词使用。llm = zenstudio.chat_model(model="openai/gpt-4o-mini")——一个真正的 LangChainBaseChatModel。凡是接受 LangChain 模型的地方都能用它:chain = prompt | llm | StrOutputParser()、LangGraph 节点等等。它支持通过llm.bind_tools([...])进行工具调用,因此 LangGraph 的create_react_agent和ToolNode同样可用。zenstudio.llama_llm(...)和zenstudio.kb_retriever(top_k=4)——一个 LlamaIndex LLM,以及一个面向你知识库的检索器(托管式混合检索,无需配置嵌入模型)。
这个工作区的定位
开发者工作区是平台其余部分的自由形态补充:先在这里用 Python、配上真实的模型调用与检索把思路草拟出来,再把它在这里重建成可长期运行、可部署的系统—— 集群画布 以及 智能体构建器.
只有一个运行时,且默认关闭
笔记本单元格执行在真实的容器内核上——完整的 CPython、可用的 pip install,以及真正的智能体框架。这里 没有浏览器内的降级方案:在管理员启用服务端运行时之前,打开笔记本看到的是一块写着“需要运行时”的面板并附上要执行的命令,而不是一个半残的编辑器。
docker compose --profile notebooks up -d --build然后在此处开启: 管理 → 开发者运行时。启用时会自动生成签名密钥。
安装库
内核镜像自带 build-essential ,并把 PIP_USER=1 指向每会话独立的可写 ~/.local,因此带 C 扩展的包也能正常编译安装:
!pip install polars duckdb scikit-learn安装只能访问出网允许清单内的主机
pip install git+https://github.com/…、从 Hugging Face 下载模型、私有索引源——都会失败,直到管理员把该主机加进 管理 → 开发者运行时 → 出网允许清单。在那里保存会写入清单并重启代理,任一步没成功界面都会告知你。| 环境变量 | 取值 | 用途 |
|---|---|---|
NOTEBOOK_RUNTIME_ENABLED | 开 / 关 | 总开关 |
NOTEBOOK_RUNTIME_BACKEND | docker | k8s | e2b | 内核在哪里启动 |
NOTEBOOK_RUNTIME_IMAGE | 镜像引用 | 内核镜像 |
NOTEBOOK_GATEWAY_URL | url | 浏览器连接的 WebSocket 网关 |
NOTEBOOK_RUNTIME_SECRET | secret | 会话令牌签名密钥;留空则自动生成 |
NOTEBOOK_CRON_TOKEN | token | 由回收空闲会话的外部 cron 出示 |
docker 后端是单机的
内核网络隔离
内核位于内部网络中, 没有直连互联网的能力。唯一出口是一个带允许清单、默认拒绝的出网代理,因此笔记本无法把数据外传到任意主机——pip 安装也只对你放通的主机有效。
调用你已部署的智能体与集群
辅助模块也能访问你已经建好的东西,于是笔记本可以编排它们,而不必重新实现一遍:
| 调用 | 返回 |
|---|---|
await zenstudio.chat(prompt, model=…) | 一次原始的模型补全 |
await zenstudio.kb_search(query) | 知识库命中结果 |
await zenstudio.list_knowledge_bases() | 你可读取的集合 |
await zenstudio.run_swarm(swarm_id, input) | 某个已保存集群的最终输出 |
await zenstudio.list_agents() | 你的智能体与集群,含 id |
await zenstudio.run_agent(agent_id, prompt) | 不可用 ——返回 501。见下文。 |
import zenstudio
catalog = await zenstudio.list_agents()
swarm_id = catalog["swarms"][0]["id"]
result = await zenstudio.run_swarm(swarm_id, "Summarise ticket 48213")
print(result)为什么这样设计
run_swarm 并不是 chat 多绕几步的版本。它严格按你的配置运行你所构建的东西,因此笔记本得到的行为与画布一致,而不是一个会逐渐跑偏的 Python 复刻。提供商密钥绝不进入沙箱——调用由平台代理,始终受 IAM 模型规则、预算和追踪约束。run_agent 返回 501,且一直如此
想从笔记本里运行某个已保存的智能体,把它包成一个单节点集群。 把智能体导入集群节点时,它的提示词、模型、工具和知识库都会复制到该节点上,因此运行结果忠实于该智能体。唯一值得知道的差别是:这份复制是一个 快照,所以之后对该智能体的修改不会传到由它搭起的集群里——重新导入该节点才能同步。
审批节点会被拒绝,而不是等待
注意
把笔记本发布为 API
笔记本可以被你自己的系统调用。打开它并点击 发布 即可生成 API 密钥;此后任何能发 HTTP 请求的东西都能运行它。
- 密钥名称 ——方便你之后辨认。建议为每个调用方各生成一个,这样撤销某个集成不会影响其他。
- 入口函数 ——以请求体为参数调用的函数,默认为
entrypoint。留空则从头到尾运行整个笔记本,返回最后一个表达式的值。
入口函数可以定义在任意单元格里。它接收你 POST 过来的 JSON,位于 inputs ,其返回值即为响应内容:
async def entrypoint(inputs):
date = inputs.get("date")
hits = await zenstudio.kb_search(f"incidents on {date}", top_k=5)
summary = await zenstudio.chat(
f"Summarise these incidents:\n{zenstudio.format_context(hits)}"
)
return {"date": date, "summary": summary}curl -X POST https://your-instance/api/notebook/run \
-H "Authorization: Bearer nbk_…" \
-H "Content-Type: application/json" \
-d '{"inputs": {"date": "2026-07-28"}}'| 字段 | 类型 | 含义 |
|---|---|---|
inputs | object | 传给入口函数。可选——不需要就省略。 |
async | boolean | 设为 true 会立即返回 202 和一个 runId,而不是等待 |
同步调用最多等待 110 秒。如果运行仍在继续,它会返回 202 以及一个 runId ,而不是一直挂着连接——轮询方式与 async 运行的轮询相同:
curl -X POST https://your-instance/api/notebook/run/status \
-H "Authorization: Bearer nbk_…" \
-H "Content-Type: application/json" \
-d '{"runId": "…"}'状态取值为 queued, running, succeeded (附带 result)或 error (附带 error).
为什么这样设计
密钥只显示一次
版本控制(Git)
点击 版本 即可把笔记本提交到你的 GitHub 或 GitLab 仓库。它以广泛使用的 percent 格式写成一个纯 Python 文件——与 Jupytext、VS Code 和 PyCharm 都能识别的 # %% 单元格标记完全一致:
# %% [markdown]
# ## Load yesterday's incidents
# %%
hits = await zenstudio.kb_search("incidents", top_k=5)- 提交 会写入该笔记本,外加一份小的
.json清单,记录它的发布方式——入口函数和密钥前缀,绝不含密钥本身。 - 历史 会列出每次提交及其链接,页头会显示 有未提交的更改 或 已是最新 ,这样你一眼就能看出仓库与正在运行的版本是否一致。
- 恢复 会读回该提交处的文件并替换笔记本的单元格。未提交的内容会丢失,所以它会先询问你。
为什么这样设计
注意