Deep Agents 框架-前端
引言
本篇主要是了解一下前端api,了解就行,记是记不住的。用的时候再查。框架可能选择react会很好一些。vue生态可能不是太好。vue3学习成本和代码规范会比react。react纯js操作html和js脚本,看起来比较混乱。样式、html、css、js、配置、数据实际上能隔离才是最好维护的。
1 概览
构建实时可视化深度智能体工作流的前端,这些设计模式展示了如何渲染以下内容:
- 子智能体进度:显示下级智能体的工作状态。
- 任务规划:展示智能体的思考路径和计划步骤。
- 流式内容:像打字机一样实时呈现生成的内容。
- 类 IDE 的沙盒体验:提供类似代码编辑器的交互式环境。
以上功能均适用于使用 createDeepAgent 创建的智能体。
1.1 架构
深度智能体(Deep Agents)采用“协调者-工作者”架构。主智能体(协调者)负责规划任务,并将其委派给专门的子智能体(工作者),每个子智能体都在隔离的环境中运行。在前端,useStream 能够同时呈现协调者的消息以及每个子智能体的流式状态。

from deepagents import create_deep_agent
agent = create_deep_agent(
model="google_genai:gemini-3.1-pro-preview",
tools=[get_weather],
system_prompt="You are a helpful assistant",
subagents=[
{
"name": "researcher",
"description": "Research assistant",
}
],
)
在前端,连接方式与 createAgent 一样,都是使用 useStream。
但深度智能体(Deep Agent)模式会使用 useStream 的额外特性,例如:
stream.subagentsstream.values.todosfilterSubagentMessages
这些特性用于渲染子智能体专属的用户界面。
import { useStream } from "@langchain/react";
function App() {
const stream = useStream<typeof agent>({
apiUrl: "http://localhost:2024",
assistantId: "agent",
});
// Deep agent state beyond messages
const todos = stream.values?.todos;
const subagents = stream.subagents;
}
1.2 模式
子智能体流式传输
展示专家级子智能体的实时内容流,包含进度追踪和可折叠的卡片设计。
待办事项列表
通过从智能体状态同步的实时待办列表,来追踪智能体的执行进度。
沙盒
构建一个类似集成开发环境(IDE)的用户界面,包含文件浏览器、代码查看器和差异对比面板,并由沙盒环境提供支持。
1.3 相关模式
LangChain 的前端模式,包括 Markdown 消息、工具调用和人机协同,同样都适用于深度智能体(Deep Agents)。由于深度智能体是构建在相同的 LangGraph 运行时之上的,因此 useStream 提供了相同的核心 API。
2 模式
2.1 子智能体流式传输
当协调者智能体(Coordinator)生成专家级子智能体(如研究员、分析师、作家)时,你需要将协调者的消息与每个子智能体的流式输出分开渲染。
-
在
useStream中设置filterSubagentMessages: true,以清晰地将这两类流分离。 -
然后使用
getSubagentsByMessage,将每个子智能体的进度卡片挂载到触发它的那个协调者消息下方。

2.1.1 过滤子智能体消息
如果不进行过滤,每个子智能体产生的每一个字符(Token)都会交错穿插在协调者的消息流中,导致内容无法阅读。
当设置 filterSubagentMessages: true 后:
stream.messages仅包含协调者的消息。- 每个子智能体的内容可以通过
stream.subagents和stream.getSubagentsByMessage单独获取。 - 用户界面保持整洁:协调者的推理过程与专家的工作内容是分开的。
这种分离让你能够在一个地方渲染协调者的消息,并将每个子智能体的进度卡片精准地挂载到它所属的位置:即生成它的那条协调者消息下方。
2.2.2 设置 useStream
-
始终设置
filterSubagentMessages: true。
这会从主消息流中移除子智能体的字符(Tokens),这样你就可以独立渲染协调者的消息和子智能体的输出了。 -
定义一个 TypeScript 接口,使其与你智能体的状态模式(Schema)相匹配,并将其作为类型参数传递给
useStream,以实现对状态值的类型安全访问。
在下面的示例中,请将 typeof myAgent 替换为你自己的接口名称
import type { BaseMessage } from "@langchain/core/messages";
interface AgentState {
messages: BaseMessage[];
}

2.1.2 提交并开启子图流式传输
当提交消息时,请启用子图流式传输,并设置一个合适的递归限制。深度智能体的工作流通常涉及多层嵌套的子图,因此较高的递归限制可以防止任务过早终止。
stream.submit(
{ messages: [{ type: "human", content: text }] },
{ streamSubgraphs: true }
);
DeepAgents 设置的默认递归限制为 10,000,这对于大多数多专家协作的场景来说已经足够了。如果需要,你可以通过 config.recursion_limit 来覆盖这个默认值。
2.1.3 子智能体流式接口
每个子智能体都会暴露出一个 SubagentStreamInterface,其中包含关于该子智能体任务的元数据,具体包括:
- 任务:具体在做什么。
- 状态:当前进展如何。
- 计时:耗时多久。
interface SubagentStreamInterface {
id: string;
status: "pending" | "running" | "complete" | "error";
messages: BaseMessage[];
result: string | undefined;
toolCall: {
id: string;
name: string;
args: {
description: string;
subagent_type: string;
[key: string]: unknown;
};
};
startedAt: number | undefined;
completedAt: number | undefined;
}
| 属性 (Property) | 描述 (Description) |
|---|---|
| id | 此子智能体实例的唯一标识符 |
| status | 生命周期状态:pending(等待中) → running(运行中) → complete(完成)或 error(错误) |
| messages | 子智能体自己的消息流,实时更新 |
| result | 最终输出的文本,仅在状态为 complete 时可用 |
| toolCall | 生成此子智能体的工具调用对象,包含任务元数据 |
| toolCall.args.description | 协调者分配给此子智能体的任务描述 |
| toolCall.args.subagent_type | 专家的类型或名称(例如 "researcher"、"analyst") |
| startedAt | 子智能体开始执行的时间戳 |
| completedAt | 子智能体结束执行的时间戳 |
2.1.4 将子智能体与消息关联
getSubagentsByMessage 方法会返回由某条特定 AI 消息生成的子智能体。这让你能够直接在触发它们的协调者消息下方渲染子智能体卡片:
const turnSubagents = stream.getSubagentsByMessage(msg.id);
这会返回一个 SubagentStreamInterface 对象数组。如果该消息没有生成任何子智能体,则返回一个空数组。
2.1.5 构建子代理卡片
每个子代理卡片显示专家的姓名、任务描述、流式内容或最终结果,以及时间信息。
import { AIMessage } from "@langchain/core/messages";
function SubagentCard({
subagent,
}: {
subagent: SubagentStreamInterface;
}) {
const [expanded, setExpanded] = useState(true);
const title =
subagent.toolCall?.args?.subagent_type ?? `Agent ${subagent.id}`;
const description = subagent.toolCall?.args?.description ?? "";
const lastAIMessage = subagent.messages
.filter(AIMessage.isInstance)
.at(-1);
const displayContent =
subagent.status === "complete"
? subagent.result
: typeof lastAIMessage?.content === "string"
? lastAIMessage.content
: "";
const elapsed = getElapsedTime(subagent.startedAt, subagent.completedAt);
return (
<div className="rounded-lg border bg-white shadow-sm">
<button
onClick={() => setExpanded(!expanded)}
className="flex w-full items-center justify-between p-4"
>
<div className="flex items-center gap-3">
<StatusIcon status={subagent.status} />
<div>
<h4 className="font-semibold capitalize">{title}</h4>
<p className="text-xs text-gray-500">{description}</p>
</div>
</div>
<div className="flex items-center gap-2">
{elapsed && (
<span className="text-xs text-gray-400">{elapsed}</span>
)}
<StatusBadge status={subagent.status} />
</div>
</button>
{expanded && displayContent && (
<div className="border-t px-4 py-3">
<div className="prose prose-sm max-w-none line-clamp-6">
{displayContent}
{subagent.status === "running" && (
<span className="inline-block h-4 w-1 animate-pulse bg-blue-500" />
)}
</div>
</div>
)}
</div>
);
}
function getElapsedTime(
startedAt: number | undefined,
completedAt: number | undefined
): string | null {
if (!startedAt) return null;
const end = completedAt ?? Date.now();
const seconds = Math.round((end - startedAt) / 1000);
if (seconds < 60) return `${seconds}s`;
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
}
上面这个代码看起来是有点丑陋,特别是
程序设计艺术、优雅代码价值慢慢的磨灭了。现在推行的AI生成代码,更加恶化了这一现象。
2.1.5状态图标和徽章
一致的视觉指示器可帮助用户一目了然地识别子代理的状态:
function StatusIcon({ status }: { status: SubagentStreamInterface["status"] }) {
switch (status) {
case "pending":
return <span className="text-gray-400">○</span>;
case "running":
return <span className="animate-spin text-blue-500">◉</span>;
case "complete":
return <span className="text-green-500">✓</span>;
case "error":
return <span className="text-red-500">✕</span>;
}
}
function StatusBadge({ status }: { status: SubagentStreamInterface["status"] }) {
const styles = {
pending: "bg-gray-100 text-gray-600",
running: "bg-blue-100 text-blue-700",
complete: "bg-green-100 text-green-700",
error: "bg-red-100 text-red-700",
};
return (
<span className={`rounded-full px-2 py-0.5 text-xs font-medium ${styles[status]}`}>
{status}
</span>
);
}
你看上面代码要是这样写多简洁明了,估计是现在动态类型语言望尘莫及的。历史包袱可不好甩掉,几乎所有使用比较广泛的语言都在想方设法模拟对象对象和类型,不是原生的,搞得五花八门,乌七八糟,严重造成了视觉和记忆上的冲击,美其名为灵活性、简洁( •̀ ω •́ )✧。
string StatusIcon(status: SubagentStreamInterface["status"]) {
switch (status) {
case "pending":
return <span className="text-gray-400">○</span>;
case "running":
return <span className="animate-spin text-blue-500">◉</span>;
case "complete":
return <span className="text-green-500">✓</span>;
case "error":
return <span className="text-red-500">✕</span>;
}
}
2.1.6 进度追踪
显示进度条和计数器,以便用户了解已完成多少个子代理:
function SubagentProgress({
subagents,
}: {
subagents: SubagentStreamInterface[];
}) {
const completed = subagents.filter((s) => s.status === "complete").length;
const total = subagents.length;
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
return (
<div className="space-y-1">
<div className="flex items-center justify-between text-xs text-gray-500">
<span>Subagent progress</span>
<span>
{completed}/{total} complete
</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full bg-blue-500 transition-all duration-300"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
2.1.7 渲染带子代理卡片的消息
关键的布局模式是先渲染每条协调器消息,如果该消息生成了子代理,则立即在其下方渲染它们的卡片:
function MessageWithSubagents({
message,
subagents,
}: {
message: BaseMessage;
subagents: SubagentStreamInterface[];
}) {
if (message.type === "human") {
return <HumanMessage content={message.content} />;
}
return (
<div className="space-y-3">
{message.content && (
<div className="prose prose-sm max-w-none">
{message.content}
</div>
)}
{subagents.length > 0 && (
<div className="ml-4 space-y-3 border-l-2 border-blue-200 pl-4">
<SubagentProgress subagents={subagents} />
{subagents.map((subagent) => (
<SubagentCard key={subagent.id} subagent={subagent} />
))}
</div>
)}
</div>
);
}
2.1.8 综合指示器
在所有子代理完成后,协调器需要时间来综合它们的结果并生成最终响应。在此阶段显示一个清晰的指示器:
function SynthesisIndicator({
subagents,
isLoading,
}: {
subagents: SubagentStreamInterface[];
isLoading: boolean;
}) {
const allComplete =
subagents.length > 0 &&
subagents.every((s) => s.status === "complete" || s.status === "error");
if (!allComplete || !isLoading) return null;
return (
<div className="flex items-center gap-2 rounded-lg bg-purple-50 px-4 py-2 text-sm text-purple-700">
<span className="animate-spin">⟳</span>
Synthesizing results from {subagents.length} subagent
{subagents.length !== 1 ? "s" : ""}...
</div>
);
}
对于复杂的多专家工作流,综合阶段可能需要几秒钟时间。一个清晰的“正在综合结果…”指示器可以防止用户误以为代理已卡住。
2.1.9 调试未过滤的输出
在开发过程中,你可以临时将 filterSubagentMessages 设置为 false,以便在主消息流中查看所有子代理的原始交错输出。这对于验证子代理的令牌(tokens)是否正确流动非常有用,但不应在生产环境的用户界面中使用。
2.1.10 使用场景
当你的智能体工作流涉及以下情况时,深度智能体子智能体卡片是合适的选择:
- 深度研究:协调员分派研究人员调查问题的不同方面,然后综合他们的发现。
- 多专家分析:领域专家(如法律、金融、技术)各自贡献他们的观点。
- 复杂任务分解:规划者将一个大任务分解为子任务,并将每个子任务分配给专业工作者。
- 代码审查流程:不同的智能体分别处理安全审查、风格检查、性能分析和文档审查
2.1.11 访问完整的子代理映射
除了按消息查找,你还可以通过 stream.subagents 一次性访问所有子代理:
const allSubagents = [...stream.subagents.values()];
const running = allSubagents.filter((s) => s.status === "running");
const completed = allSubagents.filter((s) => s.status === "complete");
const errors = allSubagents.filter((s) => s.status === "error");
这对于构建全局进度指示器或仪表板非常有用,可以汇总所有子代理的活动,而不管它们是由哪条协调器消息生成的
2.1.12 最佳实践
- 始终设置
filterSubagentMessages: true:未过滤的流会导致协调器和子代理的令牌(tokens)混杂在一起,难以阅读。 - 显示任务描述:
toolCall.args.description字段准确地告诉用户每个子代理被要求做什么。请务必醒目地显示此信息。 - 使用可折叠卡片:在包含 5 个以上子代理的工作流中,自动折叠已完成的卡片,以便用户专注于正在进行的工作。
- 显示时间数据:展示每个子代理的耗时有助于用户了解性能特征并识别瓶颈。
- 设置适当的递归限制:具有嵌套子图的深度智能体工作流需要比默认值 25 更高的限制。建议从 100 开始。
- 单独处理子代理错误:一个子代理失败不应导致整个界面崩溃。应在该子代理的卡片中显示错误,同时让其他代理继续运行
2.2 待办事项列表
并非所有的智能体交互都是聊天。有时,智能体在执行一个多步骤计划,而展示进度的最佳方式是实时更新待办列表。深度智能体待办列表模式直接从智能体状态中读取待办事项数组,并在智能体执行计划时渲染每个项目的当前状态。这是一个基于你用于聊天的 useStream 钩子构建的进度仪表板。它表明智能体状态可以为任何用户界面提供支持,而不仅仅是消息气泡。

2.2.1 工作原理
在 LangGraph 智能体中,状态并不局限于消息。你可以定义自定义状态键来保存任意数据,在本例中即为一个待办事项数组。当智能体执行其计划时,它会将每个待办事项的状态从“待处理”更新为“进行中”,再到“已完成”。useStream 钩子通过 stream.values 暴露这些自定义状态值,你的用户界面则会响应式地渲染它们。
流程如下:
- 用户提交请求
- 智能体制定计划并在其状态中填充待办事项
- 智能体开始执行每个待办事项,状态依次转换:待处理 → 进行中 → 已完成
- 随着智能体的推进,
stream.values.todos实时更新 - 你的用户界面根据当前状态重新渲染待办列表
2.2.2 设置上游流
你不需要做任何特殊的配置。只需将 useStream 指向你的智能体,并从 stream.values 中读取待办事项即可。为了获得更好的开发体验,建议定义一个与智能体状态结构匹配的 TypeScript 接口,并将其作为类型参数传递给 useStream。这样,包括 todos 在内的自定义状态键都能获得类型安全的访问权限。在下面的示例中,请将 typeof myAgent 替换为你自己的接口名称:
import type { BaseMessage } from "@langchain/core/messages";
interface TodoItem {
title: string;
status: "pending" | "in_progress" | "completed";
description?: string;
}
interface AgentState {
messages: BaseMessage[];
todos: TodoItem[];
}

2.2.3 代办任务接口
数组中的每个待办事项都具有简单的结构:
待办列表会根据当前状态渲染每个项目,通过状态图标、颜色编码和视觉样式来反映进度:
- status:当前任务的状态。选项包括:
pending(未开始)、in_progress(智能体正在处理)、completed(已完成)。 - content:任务内容的易读描述。
智能体在制定计划时会填充这个数组,然后在执行每个步骤时更新单个项目。
2.2.4 构建代办列表组件
待办列表会根据当前状态渲染每个项目,通过状态图标、颜色编码和视觉样式来反映进度:
function TodoList({ todos }: { todos: Todo[] }) {
const completed = todos.filter((t) => t.status === "completed").length;
const percentage = todos.length
? Math.round((completed / todos.length) * 100)
: 0;
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold">Agent Progress</h2>
<span className="text-sm text-gray-500">
{completed}/{todos.length} tasks
</span>
</div>
<ProgressBar percentage={percentage} />
<ul className="mt-4 space-y-2">
{todos.map((todo, i) => (
<TodoItem key={i} todo={todo} />
))}
</ul>
</div>
);
}
2.2.5 进度条
一个可视化的进度条能够为用户提供整体完成情况的概览,让人一眼就能掌握当前进度
function ProgressBar({ percentage }: { percentage: number }) {
return (
<div className="space-y-1">
<div className="flex items-center justify-between text-xs text-gray-500">
<span>Progress</span>
<span>{percentage}%</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-gray-200">
<div
className="h-full rounded-full bg-green-500 transition-all duration-500"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
}
2.2.6 独立的待办事项
待办列表中的每个项目都会通过状态图标、颜色编码的文字以及完成后的删除线样式来直观地反映其当前状态。
function TodoItem({ todo }: { todo: Todo }) {
const config = {
pending: {
icon: "○",
textClass: "text-gray-600",
bgClass: "bg-gray-50",
iconClass: "text-gray-400",
},
in_progress: {
icon: "◉",
textClass: "text-amber-800",
bgClass: "bg-amber-50 border-amber-200",
iconClass: "text-amber-500 animate-pulse",
},
completed: {
icon: "✓",
textClass: "text-green-800 line-through",
bgClass: "bg-green-50 border-green-200",
iconClass: "text-green-500",
},
};
const style = config[todo.status];
return (
<li
className={`flex items-start gap-3 rounded-md border px-3 py-2 ${style.bgClass}`}
>
<span className={`mt-0.5 text-lg leading-none ${style.iconClass}`}>
{style.icon}
</span>
<span className={`text-sm ${style.textClass}`}>{todo.content}</span>
</li>
);
}
2.2.7 计算进度
进行中的图标会利用 animate-pulse 效果,以吸引用户注意到当前正在执行的任务
const todos = stream.values?.todos ?? [];
const completed = todos.filter((t) => t.status === "completed").length;
const inProgress = todos.filter((t) => t.status === "in_progress").length;
const pending = todos.filter((t) => t.status === "pending").length;
const percentage = todos.length
? Math.round((completed / todos.length) * 100)
: 0;
你可以直接从 todos 数组中派生出进度指标,无需智能体额外发送状态更新。这完全可以在前端通过简单的计算实时完成
2.2.8 整合聊天消息
待办列表可以与常规聊天界面协同工作。一个实用的布局是将待办列表作为固定的侧边栏或顶部面板,聊天消息则显示在下方。
function TodoAgentLayout() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_todo_list",
});
const todos = stream.values?.todos ?? [];
return (
<div className="flex h-screen flex-col">
{todos.length > 0 && (
<div className="border-b bg-gray-50 p-4">
<TodoList todos={todos} />
</div>
)}
<main className="flex-1 overflow-y-auto p-6">
<div className="mx-auto max-w-2xl space-y-4">
{stream.messages.map((msg) => (
<Message key={msg.id} message={msg} />
))}
</div>
</main>
<ChatInput
onSubmit={(text) =>
stream.submit({ messages: [{ type: "human", content: text }] })
}
isLoading={stream.isLoading}
/>
</div>
);
}
在智能体生成计划之前展示一个空的待办列表组件纯属浪费空间。只有当 todos.length > 0 时才渲染它,能保持界面的整洁
2.2.9 待办事项之外的自定义状态
这种模式展示了一个强大的原则:stream.values 可以暴露你的智能体定义的任何自定义状态,而不仅仅是消息。todos 数组只是其中一个例子。你可以用同样的方法来实现:
- 进度指标:
stream.values.progress包含数字化的完成度数据 - 生成的产物:
stream.values.document包含智能体正在构建的结构化文档 - 决策日志:
stream.values.decisions追踪智能体做出的每一个选择 - 资源列表:
stream.values.sources包含智能体找到的链接和参考资料
// Any custom state key your agent defines is accessible
const document = stream.values?.document;
const sources = stream.values?.sources ?? [];
const confidence = stream.values?.confidence_score;
自定义状态键是在 LangGraph 图的状态模式(state schema)中定义的。useStream 钩子会自动将它们包含在 stream.values 中,无需任何额外的客户端配置。
这意味着你只需要在后端的图状态定义里添加字段(比如 todos、progress 或 document),前端就能直接通过 stream.values 访问到这些数据,实现了后端状态到前端 UI 的无缝同步。
2.2.10 动画过渡效果
待办事项的状态转换是实时发生的,流畅的动画让这些变化看起来更加精致,而不是生硬突兀
function TodoItem({ todo }: { todo: Todo }) {
return (
<li
className={`
flex items-start gap-3 rounded-md border px-3 py-2
transition-all duration-300 ease-in-out
${getStatusStyles(todo.status)}
`}
>
<span
className={`
mt-0.5 text-lg leading-none transition-colors duration-300
${getIconStyles(todo.status)}
`}
>
{getStatusIcon(todo.status)}
</span>
<span
className={`
text-sm transition-all duration-300
${todo.status === "completed" ? "line-through opacity-60" : ""}
`}
>
{todo.content}
</span>
</li>
);
}
transition-all duration-300 类确保了颜色变化、删除线和透明度的切换都能平滑地进行动画过渡
2.2.11 使用场景
待办列表模式适用于智能体执行结构化计划的任何场景:
- 项目规划:智能体将项目分解为任务,并按顺序逐一完成。
- 研究工作流:每个研究问题都变成一个待办事项,由智能体进行调查并完成。
- 数据处理:数据摄入、验证、转换和导出等步骤,每个都有自己的待办事项。
- 新手引导流程:智能体逐步完成设置步骤,在配置服务时逐一核对勾选。
- 报告生成:报告的各个部分变成待办事项:收集数据、分析趋势、撰写摘要、格式化输出。
2.2.12 处理空置和加载装填
在智能体生成计划之前,界面上应该只显示聊天区域,保持简洁。
function TodoList({ todos, isLoading }: { todos: Todo[]; isLoading: boolean }) {
if (todos.length === 0 && !isLoading) {
return null;
}
if (todos.length === 0 && isLoading) {
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
<div className="flex items-center gap-2 text-sm text-gray-500">
<span className="animate-spin">⟳</span>
Agent is creating a plan...
</div>
</div>
);
}
return (
<div className="rounded-lg border bg-white p-4 shadow-sm">
{/* ... full todo list rendering */}
</div>
);
}
2.2.13 最佳实践
待办列表是展示智能体进度的核心,一定要放在显眼的位置,别让用户费劲去找。同时,加上平滑的动画效果,智能体看起来会更灵敏。
这里有几个设计要点需要注意:
- 突出显示:把它放在首屏,别藏在页面底部。
- 平滑过渡:利用 CSS 给背景色、文字装饰和透明度加上过渡动画。
- 单一焦点:只高亮显示一个“进行中”的任务(比如让它闪烁),避免界面显得杂乱。
- 弱化已完成项:随着列表变长,把已完成的任务折叠或变暗,让用户的注意力集中在当前进度上。
- 显示百分比:直接展示“67% 已完成”这样的数字,一目了然。
- 保持同步:利用
stream.values的响应式特性,列表会自动更新,不需要写额外的轮询代码。
2.3 沙箱
编程智能体需要的不仅仅是一个聊天窗口。它们需要文件浏览器、代码查看器和差异对比面板——也就是一种集成开发环境(IDE)的体验。这种模式将深度智能体连接到一个沙箱环境,使其能够在隔离的环境中读取、写入和执行代码,然后通过自定义 API 服务器暴露沙箱的文件系统,以便前端能够在智能体工作时实时显示文件
2.3.1 架构
沙箱模式包含三个层级:
- 带沙箱后端的深度智能体:智能体通过沙箱自动获得文件系统工具(读取文件、写入文件、编辑文件、执行命令)。
- 自定义 API 服务器:一个 FastAPI 应用,通过
langgraph.json的http.app字段暴露出来,提供前端可调用的文件浏览接口。 - IDE 前端:采用三栏布局(文件树、代码/差异查看器、聊天窗口),在智能体进行修改时实时同步文件。

2.3.2 沙箱生命周期
在深入代码之前,理解沙箱的作用域至关重要。作用域策略决定了谁共享沙箱、沙箱的生命周期以及运行时如何解析它。
🧵 线程级沙箱(推荐)
每个 LangGraph 线程拥有独立的沙箱。沙箱 ID 存储在线程的元数据中,并在运行时通过 getConfig() 解析。这是大多数应用的推荐方案:
- 对话隔离:一个线程中的文件更改不会影响另一个线程。
- 状态持久化:页面刷新后沙箱状态依然存在(同一线程 = 同一沙箱)。
- 清理简单:删除线程时,其对应的沙箱也可以一并删除。

智能体级沙箱
同一个助手下的所有线程共享一个沙箱。这种模式适用于持久化的项目环境,特别是当你希望更改能在不同对话之间保留时:
- 跨会话持久化:无论开启多少个对话线程,操作的都是同一个项目环境,上下文不丢失。
- 共享状态:适合团队协作或多轮次开发同一项目的场景。
from langgraph.config import get_config
def get_sandbox_backend_for_assistant():
config = get_config()
assistant_id = config.get("metadata", {}).get("assistant_id")
return get_or_create_sandbox_for_assistant(assistant_id)
用户级沙箱
每个用户跨越所有线程拥有独立的沙箱。这需要自定义身份验证和用户识别机制:
- 用户隔离:无论用户使用哪个助手或开启多少线程,所有操作都局限在该用户专属的环境中。
- 多项目共享:适合 SaaS 场景,一个用户可以在不同项目(助手)间切换,但文件系统保持统一且与其他用户隔离。
- 依赖外部认证:必须通过 API 网关或中间件解析用户身份(如 JWT),并将其注入到配置中。
from langgraph.config import get_config
def get_sandbox_backend_for_user():
config = get_config()
user_id = config.get("configurable", {}).get("user_id")
return get_or_create_sandbox_for_user(user_id)
会话级沙箱(客户端)
适用于没有 LangGraph 线程的简单应用,前端可以生成一个会话 ID 并直接传递。这种方法不会在浏览器会话之间持久化,最适合演示或原型开发:
- 轻量级:无需后端管理线程或用户,前端直接控制生命周期。
- 临时性:刷新页面或关闭浏览器后沙箱即失效(除非前端自行持久化 ID)。
- 快速原型:适合黑客马拉松、Demo 展示或一次性代码生成工具。
import uuid
import urllib.parse
import urllib.request
session_id = str(uuid.uuid4())
query = urllib.parse.urlencode({"sessionId": session_id})
urllib.request.urlopen(f"http://localhost:2024/api/sandbox/tree?{query}")
本指南的其余部分将以线程级沙箱作为主要示例
2.3.3 设置智能体
选择沙箱提供商
Deep Agents 支持多种沙箱提供商。任何实现了 SandboxBackendProtocol 接口的提供商都可以使用:
from deepagents import create_deep_agent
from deepagents.sandbox import LangSmithSandbox # or DaytonaSandbox, etc.
sandbox = LangSmithSandbox.create()
agent = create_deep_agent(model="google_genai:gemini-3.1-pro-preview", backend=sandbox)
智能体会自动获得文件系统工具(读取文件、写入文件、编辑文件、列出目录、通配符查找、内容搜索)以及用于运行 Shell 命令的执行工具。无需进行任何工具配置。
为每个线程解析沙箱
不要在模块级别创建沙箱(这会导致所有线程共享同一个沙箱,且可能过期),而是在运行时为每个线程动态解析沙箱。沙箱通过 getConfig() 从 LangGraph 配置中读取 thread_id:
from deepagents import create_deep_agent
from deepagents.sandbox import LangSmithSandbox
from langgraph.config import get_config
def get_or_create_sandbox_for_thread(thread_id: str) -> LangSmithSandbox:
# Look up or create sandbox based on thread_id
...
sandbox = LangSmithSandbox(
resolve=lambda: get_or_create_sandbox_for_thread(
get_config()["configurable"]["thread_id"]
),
)
agent = create_deep_agent(
model="google_genai:gemini-3.1-pro-preview",
backend=sandbox,
)
在智能体运行之前,使用 uploadFiles 将你的项目文件填充到沙箱中:
基于快照初始化:对于 LangSmith 沙箱,容器镜像和资源限制来自于沙箱快照。
指定模板:创建沙箱时请传入 templateName(参见上文的 get_or_create_sandbox_for_thread)。
运行时注入:upload_files 会在该镜像的基础上,在运行时植入或更新项目文件。
填充沙箱
在智能体运行之前,使用 uploadFiles 将你的项目文件填充到沙箱中:
- 基于快照初始化:对于 LangSmith 沙箱,容器镜像和资源限制来自于沙箱快照。
- 指定模板:创建沙箱时请传入
templateName(参见上文的get_or_create_sandbox_for_thread)。 - 运行时注入:
upload_files会在该镜像的基础上,在运行时植入或更新项目文件
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") 来安装依赖项。
2.3.4 添加文件浏览API
智能体虽然可以读写文件,但前端也需要直接访问沙箱文件系统以便浏览。为此,你需要添加一个自定义的 FastAPI API 服务器,并通过 langgraph.json 中的 http.app 字段将其暴露出来。
创建 API 服务器
沙箱的 API 端点使用线程 ID 作为 URL 路径参数。这能确保前端始终访问当前会话对应的正确沙箱,其背后使用的 get_or_create_sandbox_for_thread 函数与智能体后端完全一致:
# src/api/server.py
from fastapi import FastAPI, Query, Path
from utils import get_or_create_sandbox_for_thread
app = FastAPI()
@app.get("/api/sandbox/{thread_id}/tree")
async def list_tree(
thread_id: str = Path(...),
path: str = Query("/app"),
):
sandbox = await get_or_create_sandbox_for_thread(thread_id)
result = await sandbox.aexecute(
f"find {path} -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": path, "entries": entries, "sandbox_id": sandbox.id}
@app.get("/api/sandbox/{thread_id}/file")
async def read_file(
thread_id: str = Path(...),
path: str = Query(...),
):
sandbox = await get_or_create_sandbox_for_thread(thread_id)
results = await sandbox.adownload_files([path])
return {"path": path, "content": results[0].content.decode()}
智能体的后端和 API 服务器都调用同一个 get_or_create_sandbox_for_thread 函数。这确保了对于给定的线程,它们总是解析到同一个沙箱。线程元数据中的沙箱 ID 是唯一的真实来源——无需内存缓存。
配置 langgraph.json
同时注册智能体图和 API 服务器。http.app 字段指示 LangGraph 平台在默认路由之外提供你的自定义路由:
{
"graphs": {
"coding_agent": "./src/agents/my_agent.py:agent"
},
"env": ".env",
"http": {
"app": "./src/api/server.py:app"
}
}
你的自定义路由与 LangGraph API 位于同一主机下。使用 langgraph dev 进行本地开发时,地址为 http://localhost:2024。
在 http.app 中定义的自定义路由优先于默认的 LangGraph 路由。这意味着你可以根据需要覆盖内置端点,但要小心不要意外覆盖 /threads 或 /runs 等关键路由
2.3.5 构建前端
前端包含三个面板:文件树侧边栏、代码/差异查看器和聊天面板。它使用 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,
});
// 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,以便下次挂载时创建一个新的线程(以及沙箱):
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 命令可能会修改任何文件)

#react
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(() => {
// 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);
} 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 的输出:
2.3.6 三方面板布局
DE 布局将三个面板并排排列:
表格
| 面板 | 宽度 | 用途 |
|---|---|---|
| 文件树 | 固定 (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),并在修改过的文件上显示琥珀色圆点。选择修改过的文件会自动切换到差异标签页。
2.3.7 使用场景
沙箱(Sandbox)在以下场景中是最佳选择:
- 需要为编写、修改和运行代码的智能体提供超越纯聊天的可视化界面时。
- 在代码审查工作流中,智能体建议更改,用户在接受前审查差异。
- 在教程或学习类应用中,AI 助手逐步帮助用户构建项目,并在上下文中展示更改。
- 在原型设计工具中,用户用自然语言描述功能,并实时观看智能体实现它们。
2.3.8 最佳实践
-
采用线程级作用域的沙箱
在生产环境应用中,请使用线程级作用域的沙箱。将沙箱 ID 存储在线程元数据中,并在运行时通过getConfig()进行解析。这样可以避免模块级的状态污染,并确保每个对话的沙箱都是相互隔离的。 -
共享沙箱获取逻辑
在 Agent 后端和 API 服务器之间共享getOrCreateSandboxForThread函数。两者都应该通过线程元数据以相同的方式解析沙箱,从而确保单一事实来源,无需依赖内存缓存。 -
持久化线程 ID
将threadId持久化存储在sessionStorage中。这样页面刷新后就能重新连接到同一个线程和沙箱,而不是重新创建新的实例。 -
实时同步文件
在每一个相关的工具调用时同步文件,而不仅仅是在运行结束时。这能让 IDE 体验更具“实时感”。请监听write_file、edit_file和execute工具消息,并立即刷新文件状态。 -
默认使用差异视图
对于变更的文件,默认展示差异视图。当用户点击被 Agent 修改过的文件时,首先展示差异对比——这才是他们真正关心的内容。 -
精简只读操作的结果展示
对于只读操作,显示紧凑的工具结果。不要在聊天界面中直接转储read_file的全部输出,而是显示一行摘要,例如“已读取 router.js L1-42”。仅对变更类工具才保留完整输出的显示。 -
预置真实项目
用真实的项目来初始化沙箱。从一个空沙箱开始会让用户感到困惑。请上传一个可运行的入门项目,以便用户和 Agent 能立即拥有上下文环境。 -
过滤 node_modules
在文件树中过滤掉node_modules。没人想浏览成千上万个依赖文件。在获取文件树时请将其过滤掉
总结
本篇主要介绍前端的架构、实现需要处理的问题、最佳实践等。下篇主要介绍一下开发部署
更多推荐



所有评论(0)