Skip to content

编码智能体需要的不仅仅是聊天窗口。它们还需要文件浏览器、代码 查看器和 diff 面板,也就是一种 IDE 体验。此模式将深度 智能体连接到沙箱,使其能够在隔离的环境中读取、 写入和执行代码,然后通过自定义 API 服务器暴露沙箱 文件系统,让前端能够在智能体工作时实时显示文件。

本页涵盖三面板界面(文件树、代码查看器和聊天)以及 向其暴露沙箱文件系统的自定义 API 路由。有关沙箱 提供商、生命周期范围、预置文件、机密信息、部署和生产环境 useStream 配置,请参阅进入生产环境

import { PatternEmbed } from "/snippets/pattern-embed.jsx";

架构

此设置包含三个部分:

  1. 带沙箱后端的深度智能体: 智能体会自动从沙箱获得文件系统工具 (read_filewrite_fileedit_filedeleteexecute

  2. 带沙箱后端的深度智能体: 智能体会自动从沙箱获得文件系统工具 (read_filewrite_fileedit_fileexecute

  3. 自定义 API 服务器 — 一个通过 langgraph.jsonhttp.app 字段暴露的 FastAPI 应用,提供前端可以调用的文件浏览端点

  4. 自定义 API 服务器: 一个通过 langgraph.jsonhttp.app 字段暴露的 Hono 应用,提供前端可以调用的文件浏览端点

  5. 三面板前端: 文件树、代码/diff 查看器和聊天面板, 在智能体做出更改时实时同步文件

沙箱生命周期

在接入前端之前,先选择沙箱的存活时长以及由谁共享它。 请参阅沙箱生命周期,了解线程级 与助手级沙箱、异步图工厂 设置、TTL 行为和 SDK 调用示例。

本指南默认使用线程级沙箱。前端和 自定义 API 服务器都从 LangGraph 线程 ID 解析沙箱。这样可以保持对话隔离, 并且当您持久化线程 ID时,页面刷新可以重新连接到同一个环境。

对于多租户应用, 请改为在后端工厂中按用户或助手限定沙箱范围。对于 没有 LangGraph 线程的演示,请在 API URL 中传入客户端生成的会话 ID。 会话 ID 不会在浏览器会话之间持久保留。

连接智能体和 API 服务器

按照执行环境中的描述, 为深度智能体配置沙箱后端。 智能体会自动获得文件系统工具和一个 execute 工具;无需额外 的工具配置。

构建此界面在生产环境设置之上多了一项要求:一个 自定义 API 服务器,它运行在智能体图之外,因此智能体 后端和您的文件浏览路由都必须为每个线程解析同一个沙箱。 将沙箱 ID 存储在线程元数据上,并在两者之间共享一个 查找函数。

从线程元数据解析沙箱

在一个共享模块中定义 getOrCreateSandboxForThread。智能体图 工厂和自定义 API 路由都会导入它:

ts
// src/api/utils.ts
import { Client } from "@langchain/langgraph-sdk";
import { LangSmithSandbox } from "deepagents";
import { SandboxClient } from "langsmith/sandbox";

export async function getOrCreateSandboxForThread(threadId: string) {
  const client = new Client({ apiUrl: "http://localhost:2024" });
  const thread = await client.threads.get(threadId);
  const sandboxId = thread.metadata?.sandbox_id;

  if (sandboxId) {
    const existing = await new SandboxClient().getSandbox(sandboxId);
    if (existing.status === "ready") {
      return new LangSmithSandbox({ sandbox: existing });
    }
  }

  const sandbox = await LangSmithSandbox.create({ templateName: "my-template" });
  await seedSandbox(sandbox);
  await client.threads.update(threadId, { metadata: { sandbox_id: sandbox.id } });
  return sandbox;
}

将智能体接入为一个异步图工厂,它从运行配置中读取 thread_id,并将解析出的后端传递给 createDeepAgent

ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "google-genai:gemini-3.6-flash",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "openai:gpt-5.5",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "anthropic:claude-sonnet-4-6",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "openrouter:openrouter:z-ai/glm-5.2",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "fireworks:accounts/fireworks/models/glm-5p2",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "baseten:zai-org/GLM-5.2",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
ts
import { createDeepAgent } from "deepagents";
import type { LangGraphRunnableConfig } from "@langchain/langgraph";

import { getOrCreateSandboxForThread } from "./api/utils.js";

export async function agent(config: LangGraphRunnableConfig) {
  const threadId = config.configurable?.thread_id;
  if (!threadId) throw new Error("No thread_id — agent must run on a thread");

  const backend = await getOrCreateSandboxForThread(threadId);

  return createDeepAgent({
    model: "ollama:north-mini-code-1.0",
    backend,
    systemPrompt: "You are an expert developer working on a project in /app.",
  });
}
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-4-6",
        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()
        ),
    )

INFO

进入生产环境中的示例类似,该 智能体是一个在每次运行时被调用的异步图工厂。将沙箱 ID 存储在 线程元数据上,以便自定义 http.app 路由可以调用同一个 getOrCreateSandboxForThread 辅助函数。当 LangGraph SDK 是唯一入口点时, “进入生产环境”指南改为使用提供商标签 查找。

预置项目文件

在智能体运行之前,使用 uploadFiles / upload_files 上传起始文件。有关预置模式、提供商示例以及将 记忆技能同步到 沙箱,请参阅文件传输。 对于 LangSmith 沙箱,在创建容器时从 沙箱快照传入 templateName

TIP

在上传 package.json 之后,运行 sandbox.execute("cd /app && npm install"), 这样依赖项会在智能体的第一次轮次之前就绪。

添加文件浏览 API

智能体可以读写文件,但前端还需要直接访问 沙箱文件系统来进行浏览。添加一个自定义 Hono API 服务器, 并通过 langgraph.json 中的 http.app 字段将其暴露出来。

智能体可以读写文件,但前端还需要直接访问 沙箱文件系统来进行浏览。添加一个自定义 FastAPI API 服务器, 并通过 langgraph.json 中的 http.app 字段将其暴露出来。

创建 API 服务器

沙箱 API 端点使用线程 ID 作为 URL 路径参数。这 确保前端始终为当前 对话访问正确的沙箱,并使用与智能体后端相同的 get_or_create_sandbox_for_thread 函数 对话访问正确的沙箱,并使用与智能体后端相同的 getOrCreateSandboxForThread 函数

ts
// src/api/app.ts
import { Hono } from "hono";
import { getOrCreateSandboxForThread } from "./utils.js";

export const app = new Hono();

app.get("/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("/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 });
});
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()}

INFO

智能体的后端和 API 服务器都调用同一个 get_or_create_sandbox_for_thread 函数。这确保它们始终解析 getOrCreateSandboxForThread 函数。这确保它们始终解析 到给定线程的同一个沙箱。线程元数据中的沙箱 ID 是唯一的事实来源 — 无需内存缓存。

配置 langgraph.json

同时注册智能体图和 API 服务器。http.app 字段告诉 LangGraph 平台在与默认路由一起提供您的自定义路由。 有关完整的 langgraph.json 选项,请参阅 应用结构LangSmith Deployments

json
{
  "node_version": "22",
  "graphs": {
    "deep_agent_ide": "./src/agents/deep-agent-ide.ts:agent"
  },
  "env": ".env",
  "http": {
    "app": "./src/api/app.ts:app"
  }
}
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

INFO

http.app 中定义的自定义路由优先于默认的 LangGraph 路由。这意味着您可以 在需要时遮蔽内置端点,但要小心不要意外覆盖 /threads/runs 之类的路由。

构建前端

前端有三个面板:文件树侧边栏、代码/diff 查看器和 聊天面板。它使用 useStream 处理智能体对话,并使用自定义 API 端点进行文件浏览。

对于生产环境部署,请将 apiUrl 指向您的 LangSmith Deployment,并在每次运行时传入稳定的 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,
  });

  // 首次挂载时创建线程
  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,这样下次挂载时就会创建一个 全新的线程(和沙箱):

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_fileedit_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(() => {
    // 从 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 ?? 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;
}

当用户选中一个更改过的文件时,默认显示 diff 视图,这样他们 能立即看到智能体修改了什么。

显示 diff

使用与框架适配的 diff 库来渲染统一 diff:

Framework组件
React@pierre/diffs<FileDiff>parseDiffFromFile 配合
Vue@git-diff-view/vue<DiffView> 与来自 @git-diff-view/filegenerateDiffFile 配合
Svelte@git-diff-view/svelte<DiffView> 与来自 @git-diff-view/filegenerateDiffFile 配合
Angularngx-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");
    // 通过比较行来计算新增与删除
    return { path, additions, deletions };
  });

  return (
      <h3>{stats.length} Files Changed</h3>
      {stats.map((file) => (
        <button key={file.path} onClick={() => onSelect(file.path)}>
          {file.path}
          +{file.additions}
          -{file.deletions}
        </button>
      ))}
  );
}

使用场景

在以下情况下,沙箱是合适的选择:

  • 编码智能体:创建、修改和运行代码的智能体需要超越聊天的可视化界面
  • 代码审查工作流:智能体建议更改,用户在接受之前审查 diff
  • 教程或学习应用:AI 助手帮助用户一步步构建 项目,并以上下文方式展示更改
  • 原型设计工具:用户用自然语言描述功能, 并实时观看智能体实现它们

最佳实践

前端相关:

  • sessionStorage 中持久化 threadId,这样页面刷新时会重新连接到 同一个线程和沙箱,而不是创建新的。

  • 在每次相关的工具调用时同步文件,而不仅仅是在运行结束时。监视 write_fileedit_filedeleteexecute 工具消息,并立即刷新。

  • 在每次相关的工具调用时同步文件,而不仅仅是在运行结束时。监视 write_fileedit_fileexecute 工具消息,并立即刷新。

  • 对更改过的文件默认显示 diff 视图。当用户点击一个被 智能体修改过的文件时,首先显示 diff — 这才是他们关心的。

  • 对只读操作显示紧凑的工具结果。与其在聊天中倾倒 read_file 的完整输出,不如显示一行摘要,例如 Read router.js L1-42。将完整输出显示保留给修改性工具。

  • 从文件树中过滤掉 node_modules。没有人想浏览 成千上万个依赖文件。在获取文件树时将它们过滤掉。

后端和沙箱相关:

  • 对生产环境应用使用线程级沙箱。请参阅 沙箱生命周期
  • 在智能体后端和 API 服务器之间共享沙箱解析,通过 线程元数据让两者解析到同一个环境,无需内存缓存。
  • 用真实项目预置沙箱。请参阅 文件传输
  • 保持机密信息不进入沙箱。对于 API 密钥,使用 沙箱身份验证代理 而不是环境变量或文件上传。
  • 在发布前添加护栏。为自主编码智能体配置 速率限制错误处理数据隐私中间件。

相关

  • 进入生产环境 — 使用持久化沙箱、身份验证、护栏和生产环境 useStream 设置部署智能体。
  • 沙箱 — 沙箱提供商、安全模型和文件传输 API。
  • 前端概述 — 其他深度智能体界面模式:子智能体流式输出、待办列表和自定义状态。
  • 应用结构 — 完整的 langgraph.json 参考,包括自定义 http.app 路由。