主题
沙箱
为由沙箱环境支撑的编码智能体构建类 IDE 的 UI
编码智能体需要的不仅仅是一个聊天窗口。它们需要文件浏览器、代码 查看器和差异面板,即一种 IDE 体验。此模式将一个深度 智能体连接到一个沙箱,使其能够在隔离环境中读取、 写入和执行代码,然后通过一个自定义 API 服务器暴露沙箱 文件系统,以便前端在智能体工作时实时显示文件。
本页涵盖三面板 UI(文件树、代码查看器和聊天)以及 将其与沙箱文件系统对接的自定义 API 路由。有关沙箱 服务商、生命周期范围设定、预置文件、密钥、部署以及生产环境 useStream 配置,请参阅投入生产。
在线演示
此处为官方文档中的交互式演示(PatternEmbed),静态站点无法呈现。请前往官方原文查看。
架构
此设置包含三个部分:
带有沙箱后端的深度智能体: 智能体会自动从沙箱获得文件系统工具 (
read_file、write_file、edit_file、delete、execute)自定义 API 服务器——一个通过
langgraph.json的http.app字段暴露的 FastAPI 应用,提供前端可调用的文件浏览端点三面板前端: 文件树、代码/差异查看器和聊天面板, 会随着智能体做出更改而实时同步文件
%%{
init: {
"fontFamily": "monospace",
"flowchart": {
"curve": "curve"
}
}
}%%
graph LR
UI["IDE Frontend"]
API["API Server"]
AGENT["createDeepAgent()"]
SANDBOX["Sandbox"]
UI --"useStream()"--> AGENT
UI --"/sandbox/:threadId/*"--> API
AGENT --"read/write/execute"--> SANDBOX
API --"ls / read"--> SANDBOX
classDef blueHighlight fill:#E5F4FF,stroke:#006DDD,color:#030710;
classDef greenHighlight fill:#F6FFDB,stroke:#6E8900,color:#2E3900;
classDef purpleHighlight fill:#EBD0F0,stroke:#885270,color:#441E33;
classDef orangeHighlight fill:#FDF3FF,stroke:#7E65AE,color:#504B5F;
class UI blueHighlight;
class AGENT greenHighlight;
class SANDBOX purpleHighlight;
class API orangeHighlight;
沙箱生命周期
在接入前端之前,先确定沙箱的存活时长以及谁会共享它。 有关线程范围与助手范围的沙箱、异步图工厂 设置、TTL 行为以及 SDK 调用示例,请参阅沙箱生命周期。
本指南默认使用线程范围的沙箱。前端和 自定义 API 服务器都从 LangGraph 线程 ID 解析沙箱。这会保持对话相互隔离,并且 在你持久化线程 ID 后,页面重新加载时能重新连接到同一环境。
sequenceDiagram
participant FE as Frontend
participant LG as LangGraph API
participant HTTP as API Server
participant SB as Sandbox
Note over FE: Page loads
FE->>LG: POST /threads
LG-->>FE: threadId
FE->>HTTP: GET /sandbox/:threadId/tree
HTTP->>LG: threads.get(threadId) → metadata.sandbox_id
alt No sandbox yet
HTTP->>SB: LangSmithSandbox.create()
HTTP->>LG: threads.update(threadId, metadata.sandbox_id)
else Existing sandbox
HTTP->>SB: connect(sandbox_id)
end
HTTP-->>FE: file tree
Note over FE: User sends message
FE->>LG: POST /threads/:threadId/runs/stream
LG->>LG: backend reads thread_id from config
LG->>SB: connect to same sandbox
对于多租户应用, 请改为在后端工厂中按用户或助手来界定沙箱范围。对于 没有 LangGraph 线程的演示,请在 API URL 中传入一个由客户端生成的会话 ID。该会话 ID 不会 跨浏览器会话持久保留。
连接智能体与 API 服务器
按照执行环境中所述, 为深度智能体配置沙箱后端。 智能体会自动获得文件系统工具和一个 execute 工具;无需额外的 工具配置。
构建此 UI 在生产设置之上增加了一项要求:一个 自定义 API 服务器,它运行在智能体图之外,因此智能体 后端和你的文件浏览路由必须为每个线程解析出同一个沙箱。 将沙箱 ID 存储在线程元数据上,并在两者之间共享同一个 查找函数。
从线程元数据解析沙箱
python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="google_genai:gemini-3.6-flash",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="openai:gpt-5.5",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="anthropic:claude-sonnet-5",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="openrouter:z-ai/glm-5.2",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="fireworks:accounts/fireworks/models/glm-5p2",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="baseten:zai-org/GLM-5.2",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)python
from deepagents import create_deep_agent
from deepagents.backends.langsmith import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
if not thread_id:
raise ValueError("thread_id is required")
# Look up sandbox_id from thread metadata, create if missing, and seed files.
raise NotImplementedError(
"Implement sandbox lookup and creation for your deployment environment."
)
def get_thread_id_from_config() -> str:
configurable = get_config().get("configurable", {})
thread_id = configurable.get("thread_id")
if not thread_id:
raise ValueError("No thread_id, agent must run on a thread")
return thread_id
def agent():
return create_deep_agent(
model="ollama:north-mini-code-1.0",
backend=lambda _runtime: get_or_create_sandbox_for_thread(
get_thread_id_from_config()
),
)NOTE
与投入生产中的示例类似, 该智能体是一个在每次运行时被调用的异步图工厂。将沙箱 ID 存储 在线程元数据上,以便自定义 http.app 路由可以调用同一个 getOrCreateSandboxForThread 辅助函数。当 LangGraph SDK 是唯一的入口点时, 投入生产一节改用服务商标签查找。
预置项目文件
在智能体运行之前,使用 uploadFiles / upload_files 上传起始文件。有关预置模式、服务商示例,以及将 记忆或技能同步到 沙箱,请参阅文件传输。 对于 LangSmith 沙箱,在创建容器时传入来自 沙箱快照的 templateName。
TIP
在上传 package.json 后运行 sandbox.execute("cd /app && npm install"),以便在智能体第一轮之前依赖项就已就绪。
添加文件浏览 API
智能体可以读写文件,但前端还需要直接访问以 浏览沙箱文件系统。添加一个自定义的 FastAPI API 服务器, 并通过 langgraph.json 中的 http.app 字段暴露它。
创建 API 服务器
沙箱 API 端点使用线程 ID 作为 URL 路径参数。这 确保前端始终为当前会话访问正确的 沙箱,并使用与智能体后端相同的 get_or_create_sandbox_for_thread 函数:
python
# src/api/server.py
from fastapi import FastAPI, Query, Path
from utils import get_or_create_sandbox_for_thread
app = FastAPI()
@app.get("/sandbox/{thread_id}/tree")
async def list_tree(
thread_id: str = Path(...),
filePath: str = Query("/app"),
):
sandbox = await get_or_create_sandbox_for_thread(thread_id)
result = await sandbox.aexecute(
f"find {filePath} -printf '%y\\t%s\\t%p\\n' 2>/dev/null | sort"
)
entries = []
for line in result.output.strip().split("\n"):
if not line:
continue
type_char, size_str, full_path = line.split("\t")
entries.append({
"name": full_path.split("/")[-1],
"type": "directory" if type_char == "d" else "file",
"path": full_path,
"size": int(size_str),
})
return {"path": filePath, "entries": entries, "sandboxId": sandbox.id}
@app.get("/sandbox/{thread_id}/file")
async def read_file(
thread_id: str = Path(...),
filePath: str = Query(...),
):
sandbox = await get_or_create_sandbox_for_thread(thread_id)
results = await sandbox.adownload_files([filePath])
return {"path": filePath, "content": results[0].content.decode()}NOTE
智能体后端和 API 服务器都调用同一个 get_or_create_sandbox_for_thread 函数。这确保它们对于给定的线程
始终解析到同一个沙箱。线程元数据中的沙箱 ID 是唯一的真实来源——不需要内存缓存。
配置 langgraph.json
同时注册智能体图和 API 服务器。http.app 字段告诉 LangGraph 平台在提供默认路由的同时也提供你的自定义路由。 请参阅应用结构和 LangSmith 部署, 以了解全套 langgraph.json 选项。
json
{
"graphs": {
"deep_agent_ide": "./src/agents/my_agent.py:agent"
},
"env": ".env",
"http": {
"app": "./src/api/server.py:app"
}
}你的自定义路由与 LangGraph API 位于同一主机。对于 使用 langgraph dev 的本地开发,即 http://localhost:2024。
NOTE
在 http.app 中定义的自定义路由优先于默认的 LangGraph 路由。这意味着如果需要, 你可以覆盖内置端点,但要小心不要意外覆盖 /threads 或 /runs 之类的 路由。
构建前端
前端有三个面板:文件树侧边栏、代码/差异查看器和 聊天面板。它使用 useStream 进行智能体对话,并使用自定义 API 端点进行文件浏览。
对于生产部署,请将 apiUrl 指向你的 LangSmith 部署,并在每次运行时传入稳定的 thread_id。有关这些设置,请参阅 投入生产中的 前端, 以及使用 thread_id 和运行时 context调用智能体。
线程创建
在页面加载时创建一个 LangGraph 线程,并将其 ID 持久化到 sessionStorage 中,以便页面重新加载时重新连接到同一个沙箱:
tsx
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: "deep_agent_ide",
threadId,
onThreadId: updateThreadId,
});
// Create thread on first mount
useEffect(() => {
if (threadId) return;
stream.client.threads.create().then((t) => updateThreadId(t.thread_id));
}, [stream.client, threadId, updateThreadId]);
// Pass threadId to sandbox file hooks
const { tree, files } = useSandboxFiles(threadId);
// ...
}"新建线程"按钮会清除已存储的 ID,以便下次挂载时创建一个 全新的线程(以及沙箱):
tsx
function handleNewThread() {
updateThreadId(null);
}文件状态管理
跟踪沙箱文件系统的两个快照:原始状态(智能体 运行之前)和当前状态(实时更新)。线程 ID 包含在 API URL 中,因此请求始终会命中正确的沙箱:
ts
const AGENT_URL = "http://localhost:2024";
async function fetchTree(threadId: string): Promise<FileEntry[]> {
const res = await fetch(
`${AGENT_URL}/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}/sandbox/${encodeURIComponent(threadId)}/file?filePath=${encodeURIComponent(path)}`,
);
const data = await res.json();
return data.content ?? null;
}实时文件同步
IDE 体验的关键在于在智能体工作时更新文件,而不是 在它完成之后。监视流的消息中来自文件变更工具的 ToolMessage 实例。当 write_file 或 edit_file 工具调用 完成时,刷新那个特定文件。当 execute 完成时,刷新 所有内容(因为 shell 命令可能修改任何文件):
tsx
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: "deep_agent_ide",
});
const processedIds = useRef(new Set<string>());
useEffect(() => {
// Build a map of file-mutating tool calls from AI messages
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 });
}
}
}
// When a ToolMessage appears for a file-mutating tool, refresh
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 ?? call.args.file_path);
} else if (call.name === "execute") {
refreshTreeAndFiles();
}
}
}, [stream.messages]);
}vue
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { ToolMessage, AIMessage } from "langchain";
import { watch } from "vue";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
const processedIds = new Set<string>();
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
watch(
() => stream.messages.value,
(messages) => {
const toolCallMap = new Map();
for (const msg of messages) {
if (AIMessage.isInstance(msg)) {
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 });
}
}
}
}
for (const msg of messages) {
if (!ToolMessage.isInstance(msg)) continue;
const id = msg.id ?? msg.tool_call_id;
if (!id || processedIds.has(id)) continue;
const call = toolCallMap.get(msg.tool_call_id);
if (!call) continue;
processedIds.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
refreshTreeAndFiles();
}
}
},
{ deep: true },
);
</script>svelte
<script lang="ts">
import { useStream } from "@langchain/svelte";
import { ToolMessage, AIMessage } from "langchain";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
const processedIds = new Set<string>();
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
$effect(() => {
const msgs = stream.messages;
const toolCallMap = new Map();
for (const msg of msgs) {
if (AIMessage.isInstance(msg)) {
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 });
}
}
}
}
for (const msg of msgs) {
if (!ToolMessage.isInstance(msg)) continue;
const id = msg.id ?? msg.tool_call_id;
if (!id || processedIds.has(id)) continue;
const call = toolCallMap.get(msg.tool_call_id);
if (!call) continue;
processedIds.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
refreshTreeAndFiles();
}
}
});
</script>ts
import { Component, effect } from "@angular/core";
import { injectStream } from "@langchain/angular";
import { ToolMessage, AIMessage } from "langchain";
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "execute"]);
@Component({
selector: "app-ide-preview",
template: `<!-- ... -->`,
})
export class IdePreviewComponent {
stream = injectStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_ide",
});
private processedIds = new Set<string>();
constructor() {
effect(() => {
const messages = this.stream.messages();
const toolCallMap = new Map();
for (const msg of messages) {
if (AIMessage.isInstance(msg)) {
for (const tc of (msg as AIMessage).tool_calls ?? []) {
if (tc.id && FILE_MUTATING_TOOLS.has(tc.name)) {
toolCallMap.set(tc.id, { name: tc.name, args: tc.args });
}
}
}
}
for (const msg of messages) {
if (!ToolMessage.isInstance(msg)) continue;
const id = (msg as ToolMessage).id ?? (msg as ToolMessage).tool_call_id;
if (!id || this.processedIds.has(id)) continue;
const call = toolCallMap.get((msg as ToolMessage).tool_call_id);
if (!call) continue;
this.processedIds.add(id);
if (call.name === "write_file" || call.name === "edit_file") {
this.refreshSingleFile(call.args.path ?? call.args.file_path);
} else if (call.name === "execute") {
this.refreshTreeAndFiles();
}
}
});
}
}检测已更改的文件
在每次智能体运行之前,为当前文件内容创建快照。文件刷新后, 与快照进行比较,以识别哪些文件发生了更改:
ts
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):
tsx
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:
tsx
function ChangedFilesSummary({ changedFiles, files, originalFiles, onSelect }) {
const stats = [...changedFiles].map((path) => {
const oldLines = (originalFiles[path] ?? "").split("\n");
const newLines = (files[path] ?? "").split("\n");
// Compute additions/deletions by comparing lines
return { path, additions, deletions };
});
return (
<div>
<h3>{stats.length} Files Changed</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>
);
}使用场景
在以下情况下,沙箱是合适的选择:
- 编码智能体需要创建、修改并运行代码,因此需要聊天之外的 可视化界面
- 代码审查工作流:智能体提出更改建议,用户在 接受前审查 diff
- 教程或学习应用:AI 助手帮助用户逐步构建 项目,并在上下文中展示更改
- 原型设计工具:用户用自然语言描述功能, 并实时观看智能体实现它们
最佳实践
前端专用:
将
threadId持久化到sessionStorage,这样页面重新加载时会重新连接到 同一个线程和沙箱,而不是创建新的。在每次相关工具调用时同步文件,而不仅仅是在运行结束时。留意
write_file、edit_file、delete和execute工具消息并立即刷新。对已更改的文件默认使用 diff 视图。当用户点击一个 被智能体修改过的文件时,先显示 diff——这才是他们关心的。
对只读操作显示简洁的工具结果。不要在聊天中倾泻
read_file的完整输出,而是显示像Read router.js L1-42这样的一行摘要。完整输出展示留给会修改状态的工具。从文件树中过滤掉
node_modules。没人想浏览 成千上万的依赖文件。获取文件树时把它们过滤掉。
对于后端和沙箱:
- 生产应用使用线程级沙箱。请参阅 沙箱生命周期。
- 在智能体后端和 API 服务器之间共享沙箱解析,通过 线程元数据,让两者解析到同一个环境,无需内存缓存。
- 用真实项目为沙箱播种。请参阅 文件传输。
- 让机密远离沙箱。使用 沙箱认证代理 而不是环境变量或文件上传来处理 API 密钥。
- 上线前添加护栏。为自主编码智能体配置 速率限制、 错误处理 和 数据隐私 middleware。
相关
Going to production
使用持久化沙箱、认证、护栏和生产级 useStream 设置来部署智能体。
Sandboxes
沙箱服务商、安全模型和文件传输 API。
Frontend overview
其他深度智能体 UI 模式:子智能体流式传输、待办列表和自定义状态。
Application structure
完整的 langgraph.json 参考,包括自定义 http.app 路由。
相关 LangChain 指南
沙箱 IDE 构建于核心 LangChain 前端模式之上。这些指南涵盖了 本页所使用的相同 useStream 原语:
Tool calling
将文件和执行工具调用渲染为专用 UI 卡片,并带有类型安全的结果。
Headless tools
在客户端运行浏览器和设备 API——适用于在 IDE 面板中嵌入实时预览或终端输出。
Open-ended generative UI
在生成式 UI 谱系的开放端渲染沙箱化应用预览和 MCP 生成的界面。
Join & rejoin streams
页面重新加载后重新连接到正在运行的编码会话,且不丢失沙箱状态。