子智能体流式渲染 (Subagent streaming)
展示带有流式生成内容、进度追踪和可折叠展开卡片的专业子智能体 UI
构建支持多子智能体并行执行的前端界面时,直接将所有消息混排在同一个时间线上会导致界面极度混乱。 前端的最佳实践是:主协调器的消息保留在主对话轴上,而每个被派发的子智能体则渲染为独立的可折叠卡片(Collapsible Card),用户可以展开查看其内部流式 Token 和工具调用进度。
为什么采用基于选择器的子智能体流?
前端 SDK 提供了 useStream 配合专门的选择器辅助函数(如 useMessages(stream, subagent))。
- 按需局部重渲染:子智能体频繁输出 Token 时,仅触发该子智能体卡片内部的局部组件更新,主聊天消息列表不会发生无谓的重渲染。
- 独立生命周期管理:每个子智能体拥有属于自己的独立状态(开始、运行中、已完成、失败)。
前端 useStream 配置
在 React/Vue/Svelte 组件中初始化连接:
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "langchain";
const AGENT_URL = "http://localhost:2024";
export function DeepAgentChat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_subagent_cards",
});
const subagents = [...stream.subagents.values()];
const subagentsByCallId = new Map(subagents.map((s) => [s.id, s]));
return (
<div>
{stream.messages.map((msg) => {
const turnSubagents = AIMessage.isInstance(msg)
? (msg.tool_calls ?? [])
.map((tc) => subagentsByCallId.get(tc.id ?? ""))
.filter((s): s is NonNullable<typeof s> => !!s)
: [];
return (
<div key={msg.id}>
{HumanMessage.isInstance(msg) && <HumanBubble>{msg.text}</HumanBubble>}
{AIMessage.isInstance(msg) && msg.text.trim() && (
<AIBubble>{msg.text}</AIBubble>
)}
{turnSubagents.map((subagent) => (
<SubagentCard key={subagent.id} stream={stream} subagent={subagent} />
))}
</div>
);
})}
</div>
);
}发送用户消息 (Submitting messages)
调用 stream.submit 发送用户提问,触发智能体运行:
<script setup lang="ts">
import { computed } from "vue";
import { useStream } from "@langchain/vue";
import { AIMessage, HumanMessage } from "langchain";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_subagent_cards",
});
const subagentsByCallId = computed(
() => new Map([...stream.subagents.value.values()].map((s) => [s.id, s]))
);
function subagentsForMessage(msg: unknown) {
if (!AIMessage.isInstance(msg)) return [];
return (msg.tool_calls ?? [])
.map((tc) => subagentsByCallId.value.get(tc.id ?? ""))
.filter(Boolean);
}
</script>
<template>
<div>
<div
v-for="msg in stream.messages.value"
:key="msg.id"
>
<HumanBubble v-if="HumanMessage.isInstance(msg)">
{{ msg.text }}
</HumanBubble>
<AIBubble v-else-if="AIMessage.isInstance(msg) && msg.text.trim()">
{{ msg.text }}
</AIBubble>
<SubagentCard
v-for="subagent in subagentsForMessage(msg)"
:key="subagent.id"
:stream="stream"
:subagent="subagent"
/>
</div>
</div>
</template>子智能体发现快照 (SubagentDiscoverySnapshot)
当主智能体调用 task 派发子任务时,前端的 stream.subagents Map 会实时增加一条新的 SubagentDiscoverySnapshot 记录。 该快照包含:
id:子任务的唯一标识符。name:子智能体名称(如researcher)。description:任务描述。status:当前状态(running、completed、error)。
构建子智能体卡片组件 (SubagentCard)
实现一个优雅的可折叠卡片组件,在头部显示子智能体名称与状态徽标,展开后展示其内部的实时流式生成与工具调用结果:
<script lang="ts">
import { useStream } from "@langchain/svelte";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_subagent_cards",
});
</script>
<div>
{#each stream.messages as msg (msg.id)}
<Message {msg} />
{/each}
{#each [...stream.subagents.values()] as subagent (subagent.id)}
<SubagentCard {stream} {subagent} />
{/each}
</div>执行进度与状态追踪 (Progress tracking)
在卡片头部显示旋转加载动画(Spinner)或成功对勾图标,直观呈现子智能体的执行生命周期。
整合主对话消息与子智能体卡片 (Rendering messages)
在主聊天视图中,将协调器的发言与插入其中的子智能体卡片无缝拼接排版:
import { Component, computed } from "@angular/core";
import { injectStream } from "@langchain/angular";
const AGENT_URL = "http://localhost:2024";
@Component({
selector: "app-deep-agent-chat",
template: `
@for (msg of stream.messages(); track msg.id) {
<app-message [message]="msg" />
}
@for (subagent of subagents(); track subagent.id) {
<app-subagent-card [stream]="stream" [subagent]="subagent" />
}
`,
})
export class DeepAgentChatComponent {
stream = injectStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_subagent_cards",
});
subagents = computed(() => [...this.stream.subagents().values()]);
}前端工程最佳实践
- 默认折叠子智能体卡片:在子任务完成时自动收起细节,保持界面清爽整洁。
- 支持一键展开审计:允许有技术背景的用户随时展开查看完整的 Prompt 和工具调用入参。
- 针对海量更新启用虚拟滚动:当子任务产生大量日志时,使用虚拟列表(Virtual List)防止 DOM 节点过多引起浏览器卡顿。
stream.submit(
{ messages: [{ type: "human", content: text }] },
{ config: { recursion_limit: 100 } }
);NOTE
Deep Agents sets a default recursion limit of 10,000, which is sufficient for most multi-expert setups. You can override this via config.recursion_limit if needed.
The SubagentDiscoverySnapshot
Each SubagentDiscoverySnapshot is a lightweight discovery record for a subagent running inside the thread. It tells your UI that a subagent exists, where it sits in the subagent tree, and what lifecycle state it is in.
The snapshot does not include the subagent's streamed messages or tool calls. Instead, pass the snapshot to selector hooks such as useMessages(stream, subagent) or useToolCalls(stream, subagent). These hooks use the snapshot namespace to subscribe to the subagent's stream primitives only when the corresponding card or panel is mounted.
Building the SubagentCard
Each subagent card shows the specialist's name, status, streaming content, and tool calls. Use selector hooks to subscribe to the subagent namespace:
import { useState } from "react";
import { AIMessage } from "langchain";
import {
useMessages,
useToolCalls,
type AnyStream,
type SubagentDiscoverySnapshot,
} from "@langchain/react";
function SubagentCard({
stream,
subagent,
}: {
stream: AnyStream;
subagent: SubagentDiscoverySnapshot;
}) {
const [expanded, setExpanded] = useState(true);
const messages = useMessages(stream, subagent);
const toolCalls = useToolCalls(stream, subagent);
const lastAIMessage = messages
.filter(AIMessage.isInstance)
.at(-1);
const displayContent =
lastAIMessage?.text ?? subagent.output ?? "";
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">{subagent.name}</h4>
<p className="text-xs text-gray-500">
{toolCalls.length} tool call{toolCalls.length === 1 ? "" : "s"}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<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>
);
}Progress tracking
Show a progress bar and counter so users know how many subagents have finished:
function SubagentProgress({
subagents,
}: {
subagents: SubagentDiscoverySnapshot[];
}) {
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>
);
}Rendering messages with subagent cards
The key layout pattern is to render coordinator messages from the root stream and attach subagent cards to the AI message whose tool call spawned them:
function DeepAgentLayout({ stream }: { stream: AnyStream }) {
const subagents = [...stream.subagents.values()];
const subagentsByCallId = new Map(subagents.map((s) => [s.id, s]));
return (
<div className="space-y-3">
{stream.messages.map((message) => {
const turnSubagents = AIMessage.isInstance(message)
? (message.tool_calls ?? [])
.map((tc) => subagentsByCallId.get(tc.id ?? ""))
.filter((s): s is SubagentDiscoverySnapshot => !!s)
: [];
return (
<div key={message.id}>
<Message message={message} />
{turnSubagents.length > 0 && (
<div className="ml-4 space-y-3 border-l-2 border-blue-200 pl-4">
<SubagentProgress subagents={subagents} />
{turnSubagents.map((subagent) => (
<SubagentCard key={subagent.id} stream={stream} subagent={subagent} />
))}
</div>
)}
</div>
);
})}
</div>
);
}You can combine inline cards with a global subagent view: index subagents by the coordinator tool call that spawned them for transcript cards, and use stream.subagents for a persistent sidebar that summarizes all active workers. That gives users both local context and a bird's-eye view of the whole run.
Best practices
- Mount selectors only where needed. Scoped messages and tool calls stream when a card calls
useMessages(stream, subagent)oruseToolCalls(stream, subagent). - Show specialist names.
subagent.nametells users which worker is active. - Use collapsible cards. In workflows with 5+ subagents, auto-collapse completed cards so users can focus on active work.
- Override recursion only when needed. Deep Agents sets a high default recursion limit; pass
config.recursion_limitonly for unusually deep custom workflows. - Handle errors per subagent. One subagent failing shouldn't crash the entire UI. Show the error in that subagent's card while others continue running.
Related LangChain guides
These LangChain frontend patterns work with subagent cards the same way they work with single-agent streams. Deep Agents are built on the same useStream API, so these guides apply directly:
Render each subagent's tool calls as rich, type-safe UI cards with pending, completed, and failed states.
Display coordinator and subagent messages as formatted markdown with proper streaming support.
Pause a delegated subagent for user approval or input using the same interrupt API.
Surface model thinking inside subagent cards using collapsible reasoning blocks.
Connect these docs to your agent of choice via MCP for real-time answers.
