主题
子智能体流式传输
展示专家子智能体,带有流式内容、进度跟踪和可折叠卡片
当协调者智能体生成专家子智能体(研究员、 分析师、写手)时,你需要将编排器的消息与 每个子智能体的流式输出分开渲染。v1 SDK 将协调者消息保留在 根流上,并将子智能体暴露为发现快照。将快照传给 选择器钩子或组合式函数(如 useMessages(stream, subagent)),以渲染 该专家的作用域流。
这正是 LangChain 前端 SDK 超越扁平聊天记录的地方: 子智能体是一等流实体,拥有自己的状态、消息、 工具调用元数据和结果。你的 UI 可以展示委派、进度、错误 和最终综合,而无需让用户去阅读每个 工作者交错输出的 token。
在线演示
此处为官方文档中的交互式演示(PatternEmbed),静态站点无法呈现。请前往官方原文查看。
为何使用基于选择器的子智能体流
根流始终聚焦于协调者对话:
stream.messages只包含协调者的消息stream.subagents包含带有身份、命名空间和状态的发现快照- 每个子智能体的消息、工具调用和值都通过选择器辅助函数读取
- UI 保持整洁:协调者的推理与 专家的工作相互独立
这种分离让你可以在一处渲染编排器的消息,并 仅在用户需要查看专家工作时才挂载子智能体卡片。
对于大型任务,这还能让 UI 保持可扩展。用户可以浏览 协调者的高层计划,只展开他们关心的专家工作, 同时仍保留完整的子智能体追踪,用于调试、审计或重放。
设置 useStream
无需额外的流选项。将流指向你的深度智能体, 从 stream.messages 渲染协调者消息,并使用 stream.subagents 为活跃的专家挂载卡片。在聊天布局中,按 生成它们的工具调用 ID 为子智能体建立索引,使每张卡片出现在协调者轮次下方 将流指向你的深度智能体,从 stream.messages 渲染协调者消息,并使用 stream.subagents 为活跃的专家挂载卡片。在聊天布局中,按 生成它们的工具调用 ID 为子智能体建立索引,使每张卡片出现在委派了该工作的协调者轮次下方。
INFO
代码示例使用 useStream<typeof myAgent> 来实现类型安全的流状态。类型推断请参阅 Python 或 JavaScript 后端。
tsx
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>
);
}vue
<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>svelte
<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>ts
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()]);
}提交消息
通过根流提交消息。深度智能体工作流通常涉及 多层嵌套子图,因此如果你的智能体可以深度委派, 请设置合适的递归限制:
ts
stream.submit(
{ messages: [{ type: "human", content: text }] },
{ config: { recursion_limit: 100 } }
);NOTE
Deep Agents 将默认递归限制设置为 10,000,这对于 大多数多专家配置来说已经足够。如有需要,你可以通过 config.recursion_limit 覆盖它。
SubagentDiscoverySnapshot
每个 SubagentDiscoverySnapshot 都是一条轻量级的发现记录,用于描述 线程内运行的子智能体。它告诉你的 UI 存在某个子智能体、 它在子智能体树中的位置,以及它处于什么生命周期状态。
快照不包含子智能体的流式消息或工具调用。 取而代之的是,将快照传给选择器钩子,例如 useMessages(stream, subagent) 或 useToolCalls(stream, subagent)。这些钩子 使用快照的命名空间,仅在相应卡片或面板挂载时 才订阅该子智能体的流原语。
构建 SubagentCard
每个子智能体卡片显示专家的名称、状态、流式内容和 工具调用。使用选择器钩子订阅子智能体命名空间:
tsx
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>
);
}进度跟踪
显示进度条和计数器,让用户知道有多少子智能体已完成:
tsx
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>
);
}使用子智能体卡片渲染消息
关键布局模式是从根流渲染协调者消息, 并将子智能体卡片附加到其工具调用生成它们的 AI 消息上:
tsx
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>
);
}你可以将内联卡片与全局子智能体视图结合:对于对话记录卡片,按 生成它们的协调者工具调用为子智能体建立索引,并使用 stream.subagents 构建一个持久侧边栏,汇总所有活跃的工作者。 这既为用户提供了局部上下文,也提供了整个运行的鸟瞰视图。
最佳实践
- 只在需要的地方挂载选择器。当卡片调用
useMessages(stream, subagent)或useToolCalls(stream, subagent)时, 作用域消息和工具调用才会进行流式传输。 - 显示专家名称。
subagent.name告诉用户哪个工作者处于活跃状态。 - 使用可折叠卡片。在有 5 个以上子智能体的工作流中,自动折叠 已完成的卡片,以便用户可以专注于活跃的工作。
- 只在需要时覆盖递归。Deep Agents 设置了较高的默认 递归限制;仅对异常深的自定义 工作流传入
config.recursion_limit。 - 按子智能体处理错误。一个子智能体失败不应导致 整个 UI 崩溃。在该子智能体的卡片中显示错误,同时其他子智能体继续 运行。
相关 LangChain 指南
这些 LangChain 前端模式用于子智能体卡片的方式,与它们 用于单智能体流的方式相同。Deep Agents 构建于相同的 useStream API 之上,因此这些指南可直接适用:
Tool calling
将每个子智能体的工具调用渲染为丰富的、类型安全的 UI 卡片,并带有待处理、已完成和失败状态。
Markdown messages
将协调者和子智能体的消息显示为格式化的 markdown,并带适当的流式传输支持。
Human-in-the-Loop
使用相同的 interrupt API 暂停被委派的子智能体,以等待用户批准或输入。
Reasoning tokens
使用可折叠的推理块,在子智能体卡片中呈现模型的思考。