Documentation Index
Fetch the complete documentation index at: https://nvd-54.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
编程智能体不仅需要一个聊天窗口。它们需要文件浏览器、代码查看器和差异面板——一种 IDE 体验。此模式将深度智能体连接到沙箱,使其能够在隔离环境中读取、写入和执行代码,然后通过自定义 API 服务器暴露沙箱文件系统,让前端能够在智能体工作时实时显示文件。
沙箱模式有三层:
-
带沙箱后端的深度智能体: 智能体自动从沙箱获取文件系统工具(
read_file、write_file、edit_file、execute)
-
自定义 API 服务器: 通过
langgraph.json 的 http.app 字段暴露的 Hono 应用,提供前端可调用的文件浏览端点
-
IDE 前端: 三面板布局(文件树、代码/差异查看器、聊天),在智能体进行更改时实时同步文件
沙箱生命周期
在深入代码之前,了解沙箱的作用域是很重要的。作用域策略决定了谁共享沙箱、它存活多久以及如何在运行时解析。
线程作用域沙箱(推荐)
每个 LangGraph 线程获得自己的沙箱。沙箱 ID 存储在线程的元数据中,并在运行时通过 getConfig() 解析。这是大多数应用程序的推荐方法:
- 对话是隔离的——一个线程中的文件更改不会影响另一个
- 沙箱状态跨页面刷新持久化(同一线程 = 同一沙箱)
- 清理很简单:当线程被删除时,其沙箱也可以删除
智能体作用域沙箱
同一助手下的所有线程共享单个沙箱。适用于您希望更改跨对话保留的持久化项目环境:
import { getConfig } from "@langchain/langgraph";
function getSandboxBackendForAssistant() {
const config = getConfig();
const assistantId = config.metadata?.assistant_id;
return getOrCreateSandboxForAssistant(assistantId);
}
用户作用域沙箱
每个用户在所有线程中获得自己的沙箱。需要自定义身份验证和用户识别:
import { getConfig } from "@langchain/langgraph";
function getSandboxBackendForUser() {
const config = getConfig();
const userId = config.configurable?.user_id;
return getOrCreateSandboxForUser(userId);
}
会话作用域沙箱(客户端)
对于没有 LangGraph 线程的简单应用,前端可以生成会话 ID 并直接传递。这种方法不会跨浏览器会话持久化,最适合演示或原型开发:
const sessionId = crypto.randomUUID();
fetch(`/api/sandbox/tree?sessionId=${sessionId}`);
本指南其余部分以线程作用域沙箱作为主要示例。
设置智能体
选择沙箱提供商
深度智能体支持多个沙箱提供商。任何实现了 SandboxBackendProtocol 的提供商都可以使用:
import { createDeepAgent, LangSmithSandbox } from "deepagents";
const sandbox = await LangSmithSandbox.create();
export const agent = createDeepAgent({
model: "google_genai:gemini-3.1-pro-preview",
backend: sandbox,
systemPrompt: "You are an expert developer working on a project in /app.",
});
智能体自动获得文件系统工具(read_file、write_file、edit_file、ls、glob、grep)和用于运行 shell 命令的 execute 工具。无需配置工具。
按线程解析沙箱
不要在模块级别创建沙箱(这会在所有线程间共享且可能过期),而是在运行时按线程解析沙箱。沙箱通过 getConfig() 从 LangGraph 配置中读取 thread_id:
import { createDeepAgent, LangSmithSandbox } from "deepagents";
import { getConfig } from "@langchain/langgraph";
async function getOrCreateSandboxForThread(threadId: string): Promise<LangSmithSandbox> {
// 检查线程元数据中的现有 sandbox_id
const client = new Client({ apiUrl: "http://localhost:2024" });
const thread = await client.threads.get(threadId);
const sandboxId = thread.metadata?.sandbox_id;
if (sandboxId) {
// 重新连接到现有沙箱
return new LangSmithSandbox({
sandbox: await new SandboxClient().getSandbox(sandboxId),
});
}
// 创建新沙箱并将 ID 存储在线程元数据中
const sandbox = await LangSmithSandbox.create({ templateName: "my-template" });
await seedSandbox(sandbox);
await client.threads.update(threadId, { metadata: { sandbox_id: sandbox.id } });
return sandbox;
}
// 创建按线程在运行时解析的沙箱
const sandbox = new LangSmithSandbox({
resolve: async () => {
const config = getConfig();
const threadId = config.configurable?.thread_id;
if (!threadId) throw new Error("No thread_id — agent must run on a thread");
return getOrCreateSandboxForThread(threadId);
},
});
export const agent = createDeepAgent({
model: "google_genai:gemini-3.1-pro-preview",
backend: sandbox,
systemPrompt: "You are an expert developer working on a project in /app.",
});
初始化沙箱
在智能体运行前,使用 uploadFiles 将项目文件填充到沙箱中:
对于 LangSmith 沙箱,容器镜像和资源限制来自
沙箱快照。创建沙箱时传入 templateName(参见上面的 getOrCreateSandboxForThread)。uploadFiles 在运行时基于该镜像种子或更新项目文件。
const SEED_FILES: Record<string, string> = {
"package.json": JSON.stringify({ name: "my-app", version: "1.0.0" }, null, 2),
"src/index.js": 'console.log("Hello");',
};
const encoder = new TextEncoder();
await sandbox.uploadFiles(
Object.entries(SEED_FILES).map(([path, content]) => [`/app/${path}`, encoder.encode(content)]),
);
上传 package.json 后运行 sandbox.execute("cd /app && npm install") 以在智能体启动前安装依赖。
添加文件浏览 API
智能体可以读写文件,但前端也需要直接访问来浏览沙箱文件系统。添加一个自定义 Hono API 服务器并通过 langgraph.json 的 http.app 字段暴露它。
创建 API 服务器
沙箱 API 端点使用线程 ID 作为 URL 路径参数。这确保前端始终访问当前对话的正确沙箱,使用与智能体后端相同的 getOrCreateSandboxForThread 函数:
// src/api/app.ts
import { Hono } from "hono";
import { getOrCreateSandboxForThread } from "./utils.js";
export const app = new Hono();
app.get("/api/sandbox/:threadId/tree", async (c) => {
const threadId = c.req.param("threadId");
const rootPath = c.req.query("filePath") || "/app";
const sandbox = await getOrCreateSandboxForThread(threadId);
const result = await sandbox.execute(
`find '${rootPath}' -printf '%y\\t%s\\t%p\\n' 2>/dev/null | sort -t$'\\t' -k3`,
);
const entries = result.output
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
const [typeChar, sizeStr, fullPath] = line.split("\t");
return {
name: fullPath.split("/").pop(),
type: typeChar === "d" ? "directory" : "file",
path: fullPath,
size: parseInt(sizeStr, 10) || 0,
};
});
return c.json({ path: rootPath, entries, sandboxId: sandbox.id });
});
app.get("/api/sandbox/:threadId/file", async (c) => {
const threadId = c.req.param("threadId");
const filePath = c.req.query("filePath");
if (!filePath) return c.json({ error: "filePath is required" }, 400);
const sandbox = await getOrCreateSandboxForThread(threadId);
const results = await sandbox.downloadFiles([filePath]);
const file = results[0];
if (file.error) return c.json({ error: file.error }, 404);
const content = new TextDecoder().decode(file.content!);
return c.json({ path: filePath, content });
});
智能体的后端和 API 服务器都调用相同的 getOrCreateSandboxForThread 函数。这确保它们对给定线程始终解析到相同的沙箱。线程元数据中的沙箱 ID 是唯一的事实来源——不需要内存缓存。
配置 langgraph.json
注册智能体图和 API 服务器。http.app 字段告诉 LangGraph 平台在默认路由旁提供您的自定义路由:
{
"node_version": "22",
"graphs": {
"coding_agent": "./src/agents/my-agent.ts:agent"
},
"env": ".env",
"http": {
"app": "./src/api/app.ts:app"
}
}
您的自定义路由与 LangGraph API 在同一主机上可用。对于使用 langgraph dev 的本地开发,即 http://localhost:2024。
在 http.app 中定义的自定义路由优先于默认的 LangGraph 路由。这意味着您可以在需要时遮蔽内置端点,但要注意不要意外覆盖 /threads 或 /runs 等路由。
构建前端
前端有三个面板:文件树侧边栏、代码/差异查看器和聊天面板。它使用 useStream 进行智能体对话,并使用自定义 API 端点进行文件浏览。
线程创建
页面加载时创建 LangGraph 线程,并将其 ID 持久化到 sessionStorage 中,以便页面刷新时重新连接到相同的沙箱:
const THREAD_KEY = "sandbox-thread-id";
function IDEPreview() {
const [threadId, setThreadId] = useState<string | null>(
() => sessionStorage.getItem(THREAD_KEY),
);
const updateThreadId = useCallback((id: string | null) => {
setThreadId(id);
if (id) sessionStorage.setItem(THREAD_KEY, id);
else sessionStorage.removeItem(THREAD_KEY);
}, []);
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "coding_agent",
threadId,
onThreadId: updateThreadId,
});
// 首次挂载时创建线程
useEffect(() => {
if (threadId) return;
stream.client.threads.create().then((t) => updateThreadId(t.thread_id));
}, [stream.client, threadId, updateThreadId]);
// 将 threadId 传递给沙箱文件钩子
const { tree, files } = useSandboxFiles(threadId);
// ...
}
“新建线程”按钮清除存储的 ID,以便下一次挂载创建新线程(和沙箱):
function handleNewThread() {
stream.switchThread(null);
updateThreadId(null);
}
文件状态管理
跟踪沙箱文件系统的两个快照:原始状态(智能体运行前)和当前状态(实时更新)。线程 ID 包含在 API URL 中,以便请求始终命中当前对话的正确沙箱:
const AGENT_URL = "http://localhost:2024";
async function fetchTree(threadId: string): Promise<FileEntry[]> {
const res = await fetch(
`${AGENT_URL}/api/sandbox/${encodeURIComponent(threadId)}/tree?filePath=/app`,
);
const data = await res.json();
return data.entries.filter((e: FileEntry) => !e.path.includes("node_modules"));
}
async function fetchFile(threadId: string, path: string): Promise<string | null> {
const res = await fetch(
`${AGENT_URL}/api/sandbox/${encodeURIComponent(threadId)}/file?filePath=${encodeURIComponent(path)}`,
);
const data = await res.json();
return data.content ?? null;
}
实时文件同步
IDE 体验的关键是在智能体工作时更新文件,而不是完成后。观察流消息中来自文件修改工具的 ToolMessage 实例。当 write_file 或 edit_file 工具调用完成时,刷新该特定文件。当 execute 完成时,刷新所有内容(因为 shell 命令可能修改任何文件):
import { useStream } from "@langchain/react";
import { ToolMessage, AIMessage } from "langchain";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
export function IDEPreview() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "coding_agent",
});
const processedIds = useRef(new Set<string>());
useEffect(() => {
// 从 AI 消息中构建文件修改工具调用的映射
const toolCallMap = new Map();
for (const msg of stream.messages) {
if (!AIMessage.isInstance(msg)) continue;
for (const tc of msg.tool_calls ?? []) {
if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
}
}
}
// 当出现文件修改工具的 ToolMessage 时,刷新
for (const msg of stream.messages) {
if (!ToolMessage.isInstance(msg)) continue;
const id = msg.id ?? msg.tool_call_id;
if (!id || processedIds.current.has(id)) continue;
const call = toolCallMap.get(msg.tool_call_id);
if (!call) continue;
processedIds.current.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
refreshSingleFile(call.args.path);
} else if (call.name === "execute") {
refreshAllFiles();
}
}
}, [stream.messages]);
}
检测变更文件
在每次智能体运行前,快照当前文件内容。文件刷新后,与快照比较以识别哪些文件发生了变更:
function detectChanges(current: FileSnapshot, original: FileSnapshot): Set<string> {
const changed = new Set<string>();
for (const [path, content] of Object.entries(current)) {
if (original[path] !== content) changed.add(path);
}
for (const path of Object.keys(original)) {
if (!(path in current)) changed.add(path);
}
return changed;
}
当用户选择变更的文件时,默认切换到差异视图,以便他们立即看到智能体修改了什么。
显示差异
使用适合框架的差异库来渲染统一差异:
| 框架 | 库 | 组件 |
|---|
| React | @pierre/diffs | <FileDiff> 配合 parseDiffFromFile |
| Vue | @git-diff-view/vue | <DiffView> 配合 @git-diff-view/file 的 generateDiffFile |
| Svelte | @git-diff-view/svelte | <DiffView> 配合 @git-diff-view/file 的 generateDiffFile |
| Angular | ngx-diff | <ngx-unified-diff> 配合 [before] 和 [after] |
使用 @pierre/diffs(React)的示例:
import { FileDiff } from "@pierre/diffs/react";
import { parseDiffFromFile } from "@pierre/diffs";
function DiffPanel({ original, current, fileName }) {
const diff = parseDiffFromFile(
{ name: fileName, contents: original },
{ name: fileName, contents: current },
);
return (
<FileDiff
fileDiff={diff}
options={{ theme: "github-dark", diffStyle: "unified", diffIndicators: "bars" }}
/>
);
}
变更文件摘要
显示所有修改文件的摘要,包含行级添加/删除计数。这为用户提供了智能体影响的快速概览——类似于 git status:
function ChangedFilesSummary({ changedFiles, files, originalFiles, onSelect }) {
const stats = [...changedFiles].map((path) => {
const oldLines = (originalFiles[path] ?? "").split("\n");
const newLines = (files[path] ?? "").split("\n");
// 通过比较行来计算添加/删除
return { path, additions, deletions };
});
return (
<div>
<h3>{stats.length} 个文件已变更</h3>
{stats.map((file) => (
<button key={file.path} onClick={() => onSelect(file.path)}>
{file.path}
<span className="text-green-400">+{file.additions}</span>
<span className="text-red-400">-{file.deletions}</span>
</button>
))}
</div>
);
}
三面板布局
IDE 布局将三个面板并排排列:
| 面板 | 宽度 | 用途 |
|---|
| 文件树 | 固定(208px) | 浏览沙箱文件,查看变更指示器 |
| 代码 / 差异 | 弹性 | 查看文件内容或统一差异 |
| 聊天 | 固定(320px) | 与智能体交互 |
<div className="flex h-screen">
<div className="w-52 shrink-0">
<FileTree />
<ChangedFilesSummary />
</div>
<CodePanel /* flex-1 */ />
<div className="w-80 shrink-0">
<ChatPanel />
</div>
</div>
文件树显示 VS Code 风格的图标(使用 @iconify-json/vscode-icons)并在修改过的文件上显示琥珀色圆点。选择修改过的文件会自动切换到差异标签页。
使用场景
沙箱是正确的选择,当:
- 编程智能体创建、修改和运行代码时需要超越聊天的可视化界面
- 代码审查工作流中,智能体提出更改建议,用户在接受前审查差异
- 教程或学习应用中,AI 助手帮助用户逐步构建项目,在上下文中展示更改
- 原型工具中,用户用自然语言描述功能并实时观看智能体实现
最佳实践
- 使用线程作用域沙箱用于生产应用。将沙箱 ID 存储在线程元数据中,并在运行时通过
getConfig() 解析。这避免了模块级状态,并保持沙箱按对话隔离。
- 在智能体后端和 API 服务器之间共享
getOrCreateSandboxForThread。两者都应该以相同方式解析沙箱——通过线程元数据——这样只有单一的事实来源,无需内存缓存。
- 在
sessionStorage 中持久化 threadId,以便页面刷新时重新连接到相同的线程和沙箱,而不是创建新的。
- 在每次相关工具调用时同步文件,而不仅仅是运行完成时。这使 IDE 感觉是实时的。监视
write_file、edit_file 和 execute 工具消息并立即刷新。
- 对变更文件默认使用差异视图。当用户点击被智能体修改的文件时,首先显示差异——这是他们关心的。
- 对只读操作显示紧凑的工具结果。不要在聊天中倾倒
read_file 的完整输出,而是显示一行如 Read router.js L1-42。将完整输出显示留给修改工具。
- 用真实项目初始化沙箱。从空沙箱开始会令人困惑。上传一个可工作的启动项目,以便用户(和智能体)立即有上下文。
- 从文件树中过滤
node_modules。没人想浏览成千上万的依赖文件。获取文件树时将其过滤掉。