引言
本篇主要是了解一下前端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构建子代理卡片
每个子代理卡片显示专家的姓名、任务描述、流式内容或最终结果,以及时间信息。
html
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状态图标和徽章
一致的视觉指示器可帮助用户一目了然地识别子代理的状态:
html
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>
);
}
你看上面代码要是这样写多简洁明了,估计是现在动态类型语言望尘莫及的。历史包袱可不好甩掉,几乎所有使用比较广泛的语言都在想方设法模拟对象对象和类型,不是原生的,搞得五花八门,乌七八糟,严重造成了视觉和记忆上的冲击,美其名为灵活性、简洁( •̀ ω •́ )✧。
html
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进度追踪
显示进度条和计数器,以便用户了解已完成多少个子代理:
html
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渲染带子代理卡片的消息
关键的布局模式是先渲染每条协调器消息,如果该消息生成了子代理,则立即在其下方渲染它们的卡片:
html
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综合指示器
在所有子代理完成后,协调器需要时间来综合它们的结果并生成最终响应。在此阶段显示一个清晰的指示器:
html
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 一次性访问所有子代理:
html
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 钩子构建的进度仪表板。它表明智能体状态可以为任何用户界面提供支持,而不仅仅是消息气泡。
