子代理流式渲染
将协调器消息与子 Agent 流式输出分别渲染,展示可折叠进度卡片
从"交错混乱"到"井然有序"的前端流式渲染方案消息流混乱
协调器派出子 Agent 后,前端会收到什么?
当协调器 Agent 派生出研究员、分析师、撰稿人等多个子 Agent 时,每个子 Agent 生成的 token 会交错出现在同一个消息流中。
不过滤的混乱
- 协调器的推理 + 子 Agent 的输出混在一起
- 用户看到的内容上下文跳跃,难以阅读
- 无法知道哪段文字来自哪个子 Agent
- 无法为每个子 Agent 独立展示进度
过滤后的清晰
stream.messages只含协调器消息- 子 Agent 内容通过
stream.subagents独立访问 - 用
getSubagentsByMessage精确关联 - 每个子 Agent 渲染为可折叠进度卡片
filterSubagentMessages: true,将子 Agent 的 token 从主消息流中剥离。协调器的推理与各专家的工作成果各归其位。
数据流架构
从后端 Agent 到前端 UI 的完整数据流
整个渲染方案涉及三个关键角色的协作:
后端 Agent
协调器 + 子 Agent 在 LangGraph 中运行。
通过 streamSubgraphs: true 启用子图流式传输。
LangGraph Server
useStream Hook
前端通过 useStream 连接 Agent API。
设置 filterSubagentMessages: true 分离消息。
@langchain/react
UI 组件
MessageWithSubagents 渲染协调器消息。
SubagentCard 渲染每个子 Agent 的进度卡片。
React 组件
// 数据流:后端 → useStream → UI 组件
LangGraph Server
↓ streamSubgraphs: true
useStream({ filterSubagentMessages: true })
↓ stream.messages → 协调器消息
↓ stream.subagents → 子 Agent 数据
↓ getSubagentsByMessage(msgId) → 按消息关联
MessageWithSubagents → SubagentCard
配置 useStream
一行配置开启子 Agent 消息过滤
使用 useStream 连接 LangGraph Agent API,传入 typeof myAgent 获得类型安全的状态访问。
import { useStream } from "@langchain/react";
import type { myAgent } from "./agent";
const AGENT_URL = "http://localhost:2024";
export function DeepAgentChat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL, // LangGraph Server å°å
assistantId: "deep_agent_subagent_cards", // Agent æ è¯
filterSubagentMessages: true, // å
³é®ï¼å¼å¯åå Agent token ä»ä¸»æ¶æ¯æµä¸å¥ç¦»关键配置
});
return (
<div>
{stream.messages.map((msg) => (
<MessageWithSubagents
key={msg.id}
message={msg}
subagents={stream.getSubagentsByMessage(msg.id)}
/>
))}
</div>
);
}
关键参数
apiUrl:LangGraph Server 地址assistantId:Agent 的唯一标识filterSubagentMessages:开启子 Agent 过滤
类型安全
- 传入
typeof myAgent泛型参数 stream.messages自动获得正确的消息类型- 编译时捕获状态字段错误
提交与流式
启用子图流式传输并设置递归限制
提交消息时需要启用 streamSubgraphs,否则前端收不到子 Agent 的中间输出。
// 提交用户消息,同时启用子图流式传输
stream.submit(
{ messages: [{ type: "human", content: text }] }, // 用户输入
{ streamSubgraphs: true } // 必须开启,否则前端收不到子 Agent 的中间输出
);
streamSubgraphs
设为 true 后,LangGraph 会推送每个子图的实时输出。如果不开启,子 Agent 的中间 token 不会到达前端。
递归限制
Deep Agents 默认递归限制为 10,000,对多数多专家配置已足够。如有需要可通过 config.recursionLimit 覆盖。
streamSubgraphs 是否设为 true;(2) 后端 Agent 是否使用了子图结构(task 工具派生子 Agent)。
核心接口
SubagentStreamInterface——每个子 Agent 的元数据容器
每个子 Agent 都暴露一个 SubagentStreamInterface 对象,包含任务描述、状态、消息流和时间信息。
interface SubagentStreamInterface {
id: string; // 子 Agent 实例的唯一标识
status: "pending" | "running" | "complete" | "error"; // 生命周期状态
messages: BaseMessage[]; // 子 Agent 的流式消息,实时更新
result: string | undefined; // 最终输出,仅 complete 时可用
toolCall: { // 触发此子 Agent 的 task 工具调用
id: string;
name: string;
args: {
description: string; // 任务描述
subagent_type: string; // 专家类型名称
[key: string]: unknown;
};
};
startedAt: number | undefined; // 启动时间戳
completedAt: number | undefined; // 完成时间戳
}
| 属性 | 说明 |
|---|---|
id | 子 Agent 实例的唯一标识 |
status | 生命周期:pending → running → complete 或 error |
messages | 子 Agent 自身的消息流,实时更新 |
result | 最终输出,仅 complete 时可用 |
toolCall.args | 任务描述 + 专家类型名称 |
startedAt / completedAt | 执行时间戳,用于计算耗时 |
关联消息
getSubagentsByMessage——将子 Agent 精确挂到对应的协调器消息下方
协调器的一条 AI 消息可能同时派生多个子 Agent。通过消息 ID 查询,可以将这些子 Agent 的卡片精确地渲染在触发它们的协调器消息之后。
// 获取某条消息派生的所有子 Agent
const turnSubagents = stream.getSubagentsByMessage(msg.id);
// 返回 SubagentStreamInterface[]
// 如果该消息没有派生任何子 Agent,返回空数组
渲染逻辑
{stream.messages.map((msg) => (
<MessageWithSubagents
key={msg.id}
message={msg}
subagents={
stream.getSubagentsByMessage(msg.id)
}
/>
))}
布局效果
- 协调器消息正常渲染
- 如果该消息派生了子 Agent,在下方渲染缩进的卡片组
- 左侧用蓝色竖线标记"这些是子任务"
- 没有派生子 Agent 的消息不受影响
卡片组件
SubagentCard——展示专家名称、任务描述、流式内容和耗时
每个子 Agent 用一张可折叠卡片呈现。卡片头部显示专家类型、任务描述和状态,展开后显示实时内容或最终结果。
import { AIMessage } from "@langchain/core/messages";
function SubagentCard({ subagent }: { subagent: SubagentStreamInterface }) {
const [expanded, setExpanded] = useState(true);
// ä» task å·¥å
·è°ç¨åæ°ä¸æåä¸å®¶ç±»ååä»»å¡æè¿°
const title = subagent.toolCall?.args?.subagent_type
?? `Agent ${subagent.id}`;
const description = subagent.toolCall?.args?.description ?? "";
// 取子 Agent 最后一条 AI 消息作为流式内容
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">
{/* 头部:专家名 + 描述 + 状态 + 耗时 */}
{/* 内容:流式文本或最终结果 */}
{/* 运行中时末尾显示闪烁光标 */}
</div>
);
}
status === "complete" 时用 subagent.result,否则用 lastAIMessage.content。不要始终用 messages,因为完成时消息可能不包含最终结果。
状态指示
StatusIcon + StatusBadge——一眼识别子 Agent 生命周期
一致的视觉指示器帮助用户立即了解每个子 Agent 当前处于哪个阶段。
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>
);
}
| 状态 | 图标 | 徽章颜色 | 含义 |
|---|---|---|---|
pending | ○ 灰色 | 灰色 | 已创建,等待执行 |
running | ◉ 蓝色旋转 | 蓝色 | 正在生成输出 |
complete | ✓ 绿色 | 绿色 | 执行完成,结果可用 |
error | ✕ 红色 | 红色 | 执行出错 |
进度追踪
SubagentProgress——让用户知道"完成了几个"
当有多个子 Agent 并行工作时,一个进度条 + 计数器能显著减少用户的焦虑感。
function SubagentProgress({ subagents }: { subagents: SubagentStreamInterface[] }) {
// 统计已完成的子 Agent 数量(error 不计入)
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>子 Agent 进度</span>
<span>{completed}/{total} 已完成</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>
);
}
设计要点
completed/total格式直观- 进度条用 CSS transition 平滑过渡
- 放在卡片组顶部,一眼看到全局进度
边界情况
- total 为 0 时不显示进度条
- 百分比整数取整,避免小数
- error 状态不计入 completed
耗时计算
getElapsedTime——展示每个子 Agent 的执行时间
耗时数据帮助用户了解性能特征,也便于识别哪个子 Agent 是瓶颈。
function getElapsedTime(startedAt: number | undefined, completedAt: number | undefined) {
if (!startedAt) return null; // 尚未启动,不显示耗时
// 运行中用 Date.now() 作为终点,每次重渲染自动更新
const end = completedAt ?? Date.now();
const seconds = Math.round((end - startedAt) / 1000);
// 超过 60 秒切换为"分+秒"格式
if (seconds < 60) return `${seconds}s`;
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
}
// 使用:
const elapsed = getElapsedTime(
subagent.startedAt,
subagent.completedAt
);
运行中
completedAt 为 undefined,自动用 Date.now() 作为终点。每次组件重渲染,耗时都会更新——形成实时计时效果。
已完成
completedAt 有值,直接算出差值,显示固定耗时。超过 60 秒自动切换为"分 + 秒"格式。
useStream 持续推送更新,子 Agent 卡片会频繁重渲染。Date.now() 在每次渲染时取值,自然形成动态计时——无需额外 setInterval。
组装消息
MessageWithSubagents——协调器消息 + 子 Agent 卡片的布局模式
这是最关键的布局组件:渲染协调器消息,如果该消息派生了子 Agent,则在下方用蓝色竖线包裹卡片组。
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>
)}
{/* 子 Agent 卡片组 */}
{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>
);
}
ml-4 + border-l-2 border-blue-200 + pl-4 创建了一个缩进的蓝色竖线区域。视觉上清晰表达"这些子任务属于上方那条协调器消息"。
综合指示
SynthesisIndicator——子 Agent 全部完成后,协调器正在综合结果
所有子 Agent 完成后,协调器需要时间将它们的结果综合为最终响应。此时应显示清晰的指示器,避免用户误以为 Agent 已停滞。
function SynthesisIndicator({ subagents, isLoading }: { subagents: SubagentStreamInterface[]; isLoading: boolean }) {
// 判断所有子 Agent 是否都已结束(无论成功或失败)
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>
正在综合 {subagents.length} 个子 Agent
{subagents.length !== 1 ? "们" : ""}的结果...
</div>
);
}
没有指示器
- 所有子 Agent 显示"已完成"
- 页面看起来"卡住了"
- 用户不知道协调器在做什么
- 可能重复提交请求
有指示器
- 紫色提示"正在综合结果..."
- 旋转图标暗示"还在工作"
- 用户安心等待最终响应
- 配合子 Agent 数量给出预期
全局访问
通过 stream.subagents 一次性访问所有子 Agent
除了按消息查找,你还可以用 stream.subagents 获取全部子 Agent 的 Map,用于构建全局仪表盘或进度汇总。
// 访问所有子 Agent(subagents 是 Map,需转为数组)
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");
// 构建全局摘要
const summary = `
总计: ${allSubagents.length} 个子 Agent
运行中: ${running.length}
已完成: ${completed.length}
出错: ${errors.length}
`;
按消息查找
getSubagentsByMessage(msgId)
适合:消息级渲染,子 Agent 卡片紧跟协调器消息。
全局访问
stream.subagents.values()
适合:全局进度指示器、仪表盘、汇总面板。
stream.subagents 返回的是 Map 对象,需要用 [...map.values()] 或 Array.from() 转为数组才能使用 filter 等数组方法。
调试技巧
开发阶段可临时关闭过滤,查看原始交错输出
当你怀疑子 Agent 的 token 没有正确流转时,可以将 filterSubagentMessages 临时设为 false。
过滤模式(生产用)
useStream({
filterSubagentMessages: true
});
// stream.messages → 只有协调器消息
// 子 Agent 内容在 stream.subagents 中
原始模式(调试用)
useStream({
filterSubagentMessages: false
});
// stream.messages → 所有消息交错
// 可以看到子 Agent 的原始输出顺序
true。
使用场景
哪些工作流最适合子 Agent 卡片式渲染?
深度研究
协调器派遣研究员调查问题的不同方面,然后综合他们的发现。每个研究员一张卡片,任务描述清晰可见。
多专家分析
法律、金融、技术等领域专家各自贡献观点。用户能同时看到各专家的实时输出,不会混淆。
复杂任务分解
规划器将大型任务分解为子任务,每个子任务分配给专业工作者。进度条让用户掌握全局。
代码审查流水线
不同 Agent 分别处理安全审查、风格检查、性能分析和文档审查。出错时只影响对应卡片,不崩溃全局。
最佳实践
六条经过实战验证的子 Agent 渲染准则
| # | 准则 | 为什么 |
|---|---|---|
| 1 | 始终设置 filterSubagentMessages: true |
未过滤的流会产生不可读的交错内容 |
| 2 | 展示 toolCall.args.description |
让用户知道每个子 Agent 被要求做什么 |
| 3 | 使用可折叠卡片 | 5 个以上子 Agent 时自动折叠,聚焦进行中的任务 |
| 4 | 展示耗时数据 | 帮助用户识别性能瓶颈 |
| 5 | 设置适当的递归限制 | 嵌套子图需要更高限制,建议从 100 开始 |
| 6 | 按子 Agent 处理错误 | 一个失败不影响其他子 Agent 和整个 UI |
SubagentCard 内部根据 subagent.status === "error" 展示错误信息,让其他子 Agent 继续运行。
多框架适配
useStream 不只是 React——Vue、Svelte、Angular 都能用
LangChain 为每个主流前端框架提供了 useStream 实现,核心 API 完全一致。
Vue
import { useStream } from "@langchain/vue";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
filterSubagentMessages: true,
});
// 模板中用 stream.messages.value
Svelte
import { useStream }
from "@langchain/svelte";
const { messages,
getSubagentsByMessage } =
useStream<typeof myAgent>({
filterSubagentMessages: true,
});
// 用 $messages 响应式访问
Angular
import { useStream }
from "@langchain/angular";
// 在 Component 中
stream = useStream<typeof myAgent>({
filterSubagentMessages: true,
});
// 模板中用 stream.messages()
filterSubagentMessages 和 getSubagentsByMessage 的概念在所有框架中完全相同。区别仅在于响应式语法——React 用 .map,Vue 用 .value,Svelte 用 $,Angular 用 Signal。
完整框架示例
Vue / Svelte / Angular 中 useStream 的完整写法
Vue 3 — Composition API
<script setup lang="ts">
import { useStream } from "@langchain/vue";
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "deep_agent_subagent_cards",
filterSubagentMessages: true,
});
</script>
<template>
<div>
<MessageWithSubagents
v-for="msg in stream.messages.value"
:key="msg.id"
:message="msg"
:subagents="stream.getSubagentsByMessage(msg.id)"
/>
</div>
</template>
Svelte 5
<script lang="ts">
import { useStream }
from "@langchain/svelte";
const AGENT_URL = "http://localhost:2024";
const { messages,
getSubagentsByMessage,
submit } = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId:
"deep_agent_subagent_cards",
filterSubagentMessages: true,
});
</script>
<div>
{#each $messages as msg (msg.id)}
<MessageWithSubagents
message={msg}
subagents={getSubagentsByMessage(msg.id)}
/>
{/each}
</div>
Angular 17+
import { Component }
from "@angular/core";
import { useStream }
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-with-subagents
[message]="msg"
[subagents]="stream
.getSubagentsByMessage(msg.id)"
/>
}
`,
})
export class DeepAgentChatComponent {
stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId:
"deep_agent_subagent_cards",
filterSubagentMessages: true,
});
}
各框架关键差异
| 框架 | 消息访问 | 列表渲染 |
|---|---|---|
| React | stream.messages | .map() |
| Vue | stream.messages.value | v-for |
| Svelte | $messages | {#each} |
| Angular | stream.messages() | @for |
apiUrl、assistantId、filterSubagentMessages 三个参数完全一致,只是响应式绑定语法不同。
组件全貌
从前端入口到最终渲染的完整组件树
DeepAgentChat // 顶层:useStream 连接后端
├── MessageWithSubagents // 每条协调器消息
│ ├── HumanMessage // 用户消息(直接渲染)
│ └── div.prose // 协调器 AI 消息
│ └── SubagentProgress // 进度条 + 计数器
│ └── SubagentCard[] // 子 Agent 卡片组
│ ├── StatusIcon // 状态图标(○ ◉ ✓ ✕)
│ ├── StatusBadge // 状态徽章
│ └── div.prose // 流式内容 + 闪烁光标
├── SynthesisIndicator // "正在综合结果..."
└── 全局仪表盘(可选)
└── stream.subagents // 所有子 Agent 汇总
核心数据源
stream.messages→ 协调器消息stream.subagents→ 全局子 Agent MapgetSubagentsByMessage()→ 按消息查找
核心交互
- 卡片可折叠/展开
- 运行中显示闪烁光标
- 完成后显示固定结果 + 耗时
- 综合阶段显示紫色指示器
本章回顾
到这里,你已经掌握了子 Agent 流式渲染的完整方案
你学到了什么
filterSubagentMessages: true分离协调器与子 Agent 消息SubagentStreamInterface接口的完整结构getSubagentsByMessage精确关联消息与子 Agent- 可折叠卡片、状态图标、进度条、耗时计算
- 综合指示器防止用户误判"卡住"
- 六条最佳实践确保生产可用
后续章节预告
- 前端 Todo 列表:在 UI 中管理和跟踪 Agent 任务
- 前端沙箱:安全地展示 Agent 的代码执行结果
- ACP 协议:Agent Communication Protocol 的集成
- 代码模式:用编程方式构建和部署 Deep Agent