LangGraph 智能体不是黑盒。每个图由 **命名节点** 按顺序或并行执行:分类、研究、分析, 综合。图形执行卡片通过渲染卡片使这个管道可见 为每个节点,显示其状态,实时流式传输其内容,并 跟踪整个工作流程的完成情况。用户清楚地看到智能体 正在做什么,处于哪个步骤,以及每个步骤产生了什么。
这种模式对生产环境中的智能体特别有用,因为它将图 结构转化为产品 UX。与其将运行视为单一的助手 响应,你可以暴露相同的检查点、节点名称、状态键,以及 LangGraph 内部使用的流元数据。
图节点如何映射到 UI 卡片
LangGraph 图定义了一系列节点,每个负责 特定任务。例如,一个研究管道可能有:
- **分类**:对用户查询进行分类
- **研究**:收集相关信息
- **分析**:从研究中得出结论
- **综合**:生成最终、精炼的响应
每个节点将其输出写入图的特定状态键。在 前端,你不需要硬编码该映射,因为 @useStream在运行时发现 每个节点,并通过 stream.subgraphs 为每个观察到的步骤暴露一个 [SubgraphDiscoverySnapshot:
// Nodes are discovered automatically — no hardcoded list needed
const graphNodes = [...stream.subgraphs.values()];
// Each snapshot carries the node name and current status
graphNodes.forEach((node) => {
console.log(node.nodeName, node.status); // "classify", "running"
});
使用 node.nodeName 作为进度条和卡片标题的标签。传递每个 快照到 useMessages(stream, node) 以渲染节点范围的流式内容 而不将 UI 耦合到图状态键名称。
此映射成为图和 UI 之间的契约。后端 作者可以有意识地添加、重命名或重新排序节点,而前端作者 决定如何可视化每个状态键:状态徽章、Markdown 面板、 表格、图表、追踪视图或审批卡片。
设置 useStream
按常规方式连接 @[useStream。你将使用的关键属性是 messages (用于对话)和 subgraphs (用于在 当前运行中发现的图节点)。将每个发现的子图快照传递给选择器以读取 该节点范围内的消息。
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "graph_execution_cards",
});
const graphNodes = [...stream.subgraphs.values()];
return (
);
}
<script setup lang="ts">
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "graph_execution_cards",
});
</script>
<template>
</template>
<script lang="ts">
const AGENT_URL = "http://localhost:2024";
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "graph_execution_cards",
});
</script>
const AGENT_URL = "http://localhost:2024";
@Component({
selector: "app-pipeline-chat",
template: `
<app-pipeline-progress
[nodes]="graphNodes()"
[isLoading]="stream.isLoading()"
/>
<app-node-card-list
[nodes]="graphNodes()"
[stream]="stream"
[isLoading]="stream.isLoading()"
/>
`,
})
stream = injectStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "graph_execution_cards",
});
graphNodes = computed(() => [...this.stream.subgraphs().values()]);
}
将流式令牌路由到节点
当图流式传输时,每个发现的子图快照标识其 所属。将该快照传递给选择器钩子或组合式函数来读取 该节点的作用域消息:
function NodeCard({
node,
stream,
}: {
node: SubgraphDiscoverySnapshot;
stream: AnyStream;
}) {
const messages = useMessages(stream, node);
const lastAIMessage = messages.find(AIMessage.isInstance);
const streamingContent = lastAIMessage?.text ?? "";
return ;
}
第一个挂载的选择器会为该节点命名空间打开一个作用域订阅。 当节点卡片卸载时,订阅会自动释放。
确定节点状态
每个发现的节点都带有其当前状态。使用 node.status 直接获取; 发现快照会报告 "pending", "running", "complete", or "error":
type NodeStatus = SubgraphDiscoverySnapshot["status"];
const status: NodeStatus = node.status;
构建管道进度条
顶部的水平进度条让用户可以俯瞰整个 管道。每个步骤都是一个带标签的段落,随着节点完成而填充:
function PipelineProgress({
nodes,
isLoading,
}: {
nodes: SubgraphDiscoverySnapshot[];
isLoading: boolean;
}) {
const firstIncompleteIdx = nodes.findIndex((node) => node.status !== "complete");
return (
{nodes.map((node, i) => {
const isRunning =
isLoading && node.status !== "complete" && firstIncompleteIdx === i;
const colors = {
pending: "bg-gray-200 text-gray-500",
running: "bg-blue-400 text-white animate-pulse",
complete: "bg-green-500 text-white",
error: "bg-red-500 text-white",
};
const status = isRunning ? "running" : node.status;
return (
{node.nodeName}
{i < nodes.length - 1 && (
)}
);
})}
);
}
构建可折叠的 NodeCard 组件
每个节点都有自己的卡片,显示状态徽章、内容(流式或 最终)以及用于长输出的可折叠主体:
function NodeCard({
node,
stream,
}: {
node: SubgraphDiscoverySnapshot;
stream: AnyStream;
}) {
const [open, setOpen] = useState(node.status === "running");
const messages = useMessages(stream, node);
const lastAIMessage = messages.find(AIMessage.isInstance);
useEffect(() => {
if (node.status === "running") setOpen(true);
if (node.status === "complete") setOpen(false);
}, [node.status]);
return (
<button
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between p-4"
>
<h3 className="font-semibold">{node.nodeName}</h3>
<span className={open ? "rotate-90" : ""}>▶</span>
</button>
{open && (
{lastAIMessage?.text?.trim()
? {lastAIMessage.text}
: <p className="italic text-gray-500">Processing...</p>}
)}
);
}
流式内容与已完成内容
节点卡片读取流式内容和最终内容的作用域消息。这样 可以避免假设图节点名称与其写入的状态键相匹配(例如 , do_research 写入 research 在 playground 图中):
| 源 | 何时使用 |
|---|---|
useMessages(stream, node) | 渲染节点作用域的流式和最终消息 |
stream.values | 读取整个图状态,如最终 synthesis 字段,使用实际的状态键 |
模式是:在节点卡片中显示最近的作用域 AI 消息,并且 仅在 stream.values 需要图状态字段时才使用。
由于作用域消息与生成节点绑定,UI 可以支持 并行图路径,而无需从消息顺序猜测。每个卡片从其所属 节点的流事件更新,完成的值仍然可以通过 获取 stream.values.
function NodeContent({ stream, node }: { stream: AnyStream; node: SubgraphDiscoverySnapshot }) {
const messages = useMessages(stream, node);
const content = messages.find(AIMessage.isInstance)?.text ?? "";
return {content};
}
整合一切
这是结合了路由、状态检测和卡片的完整卡片列表 rendering:
function NodeCardList({
nodes,
stream,
isLoading,
}: {
nodes: SubgraphDiscoverySnapshot[];
stream: AnyStream;
isLoading: boolean;
}) {
const firstIncompleteIdx = nodes.findIndex((node) => node.status !== "complete");
return (
{nodes.map((node, i) => {
const isComplete = node.status === "complete";
const isRunning = isLoading && !isComplete && firstIncompleteIdx === i;
if (!isComplete && !isRunning) return null;
return ;
})}
);
}
使用案例
图执行卡片非常适合任何需要可见性的多步骤管道 matters:
- **研究管道**:分类 → 收集来源 → 分析 → 综合 报告 - **内容生成**:大纲 → 草稿 → 事实核查 → 编辑 → 发布 - **数据处理**:摄入 → 验证 → 转换 → 聚合 → 导出 - **代码生成**:理解需求 → 规划架构 → 编写 代码 → 审查 → 测试 - **决策工作流**:收集上下文 → 评估选项 → 评分 替代方案 → 推荐
处理动态流水线
并非所有图都有固定的节点集。某些流水线会基于输入添加或跳过节点 。发现图仅包含当前线程观察到的节点: 这确保您的 UI 仅显示与当前执行相关的节点卡片,
const activeNodes = [...stream.subgraphs.values()];
避免出现空的占位卡片。 如果您的图有条件分支(例如,为简单的
从流中发现节点
- **,从**渲染卡片,而非硬编码预期节点;条件性或被跳过的步骤在运行前 stream.subgraphs 不会显示。 将状态键视为 UI 契约 - **。决定哪些图输出应该**足够稳定以供前端渲染,并将这些键的文档保留在 图定义旁边。 为节点卡片使用作用域消息 - **。它们在节点流式传输时和完成后都能正常工作,**无需将 UI 卡片耦合到状态键名称。 自动折叠已完成的节点 - **。在长流水线中,自动折叠已完成的**卡片,以便用户专注于当前活跃的步骤。 显示预计时间 - **。如果您有每个节点**耗时的历史数据,请显示时间估计以设定用户期望。 添加全局进度指示器 - **。用**总体进度条(例如,"第 2 步,共 4 步")补充每个节点卡片, 放置在流水线视图的顶部。 - **每个节点单独处理错误**。如果某个节点失败,在其卡片中显示错误, 而不折叠整个流水线。其他节点可能仍会完成。 successfully.