Skip to content

当协调器智能体生成专家子智能体(研究员、 分析师、写作者)时,您需要将编排器的消息与每个子智能体的流式输出分开 渲染。v1 SDK 将协调器消息保留在 根流上,并将子智能体作为发现快照暴露出来。将快照传给 选择器钩子或组合式函数,例如 useMessages(stream, subagent),以渲染 专家按作用域限定的流。

这正是 LangChain 前端 SDK 超越扁平聊天记录的地方: 子智能体是一等流实体,拥有自己的状态、消息、 工具调用元数据和结果。您的界面可以展示委派、进度、错误 和最终综合,而无需用户阅读来自每个 工作者的交错 token。

import { PatternEmbed } from "/snippets/pattern-embed.jsx"

为什么使用基于选择器的子智能体流

根流始终保持专注于协调器对话:

  • stream.messages 只包含协调器的消息
  • stream.subagents 包含带有身份、命名空间和状态的发现快照
  • 每个子智能体的消息、工具调用和值通过选择器辅助函数读取
  • 界面保持整洁:协调器的推理与 专家的工作是分开的

这种分离让您可以在一个地方渲染编排器的消息,并且 只在用户需要查看专家工作时挂载子智能体卡片。

对于大型任务,这也让界面保持可扩展性。用户可以浏览 协调器的高层计划,只展开他们关心的专家工作, 同时仍然保留完整的子智能体追踪记录(trace),用于调试、审计或重放。

设置 useStream

无需额外的流选项。将流指向您的深度智能体, 从 stream.messages 渲染协调器消息,并使用 stream.subagents 为活跃的专家挂载卡片。在聊天布局中,按生成子智能体的 工具调用 ID 为子智能体建立索引,这样每张卡片都会出现在协调器轮次下方 将流指向您的深度智能体,从 stream.messages 渲染协调器消息,并使用 stream.subagents 为活跃的专家挂载卡片。在聊天布局中,按生成子智能体的 工具调用 ID 为子智能体建立索引,这样每张卡片都会出现在委派了工作的协调器轮次下方。

INFO

The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.

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 (
      {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 (
            {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} />
            ))}
        );
      })}
  );
}
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
      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"
      />
</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>

  {#each stream.messages as msg (msg.id)}
    <Message {msg} />
  {/each}
  {#each [...stream.subagents.values()] as subagent (subagent.id)}
    <SubagentCard {stream} {subagent} />
  {/each}
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 } }
);

INFO

Deep Agents 设置了默认递归上限 10,000,这对大多数 多专家设置来说已经足够了。如有需要,您可以通过 config.recursion_limit 覆盖它。

SubagentDiscoverySnapshot

每个 SubagentDiscoverySnapshot 都是线程内运行的子智能体的 轻量级发现记录。它告诉您的界面某个子智能体存在、 它在子智能体树中的位置,以及它处于什么生命周期状态。

快照包含子智能体的流式消息或工具调用。 相反,请将快照传给选择器钩子,例如 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 (
      <button
        onClick={() => setExpanded(!expanded)}
        className="flex w-full items-center justify-between p-4"
      >
          <StatusIcon status={subagent.status} />
            <h4 className="font-semibold capitalize">{subagent.name}</h4>
              {toolCalls.length} tool call{toolCalls.length === 1 ? "" : "s"}
          <StatusBadge status={subagent.status} />
      </button>

      {expanded && displayContent && (
            {displayContent}
            {subagent.status === "running" && (
            )}
      )}
  );
}

进度跟踪

显示进度条和计数器,让用户知道有多少子智能体已经完成:

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 (
        Subagent progress
          {completed}/{total} complete
        <div
          className="h-full rounded-full bg-blue-500 transition-all duration-300"
          style={{ width: `${percentage}%` }}
        />
  );
}

使用子智能体卡片渲染消息

关键的布局模式是从根流渲染协调器消息, 并将子智能体卡片附加到其工具调用生成了这些子智能体的 AI 消息上:

tsx
function DeepAgentLayout({ stream }: { stream: AnyStream }) {
  const subagents = [...stream.subagents.values()];
  const subagentsByCallId = new Map(subagents.map((s) => [s.id, s]));

  return (
      {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 (
            <Message message={message} />
            {turnSubagents.length > 0 && (
                <SubagentProgress subagents={subagents} />
                {turnSubagents.map((subagent) => (
                  <SubagentCard key={subagent.id} stream={stream} subagent={subagent} />
                ))}
            )}
        );
      })}
  );
}

您可以将内联卡片与全局子智能体视图结合:按生成这些子智能体的 协调器工具调用为记录卡片建立索引,并使用 stream.subagents 作为汇总所有活跃工作者的持久化侧边栏。 这为用户同时提供了局部上下文和整个运行的全局视角。

最佳实践

  • 只在需要的地方挂载选择器。当卡片调用 useMessages(stream, subagent)useToolCalls(stream, subagent) 时, 按作用域限定的消息和工具调用才会流式传输。
  • 显示专家名称subagent.name 告诉用户哪个工作者处于活跃状态。
  • 使用可折叠卡片。在包含 5 个以上子智能体的工作流中,自动折叠 已完成的卡片,让用户可以专注于活跃的工作。
  • 只在需要时覆盖递归设置。Deep Agents 设置了较高的默认 递归上限;只为异常深层自定义工作流传入 config.recursion_limit
  • 按子智能体处理错误。一个子智能体失败不应导致 整个界面崩溃。在该子智能体的卡片中显示错误,同时其他子智能体继续 运行。