서브에이전트 스트리밍
서브에이전트 스트리밍 (Subagent streaming)
전문 서브에이전트를 스트리밍 콘텐츠, 진행 추적, 접을 수 있는 카드로 표시하세요
코디네이터 에이전트가 전문 서브에이전트(리서처, 애널리스트, 라이터)를 생성할 때, 오케스트레이터의 메시지를 각 서브에이전트의 스트리밍 출력과 별도로 렌더링해야 합니다. v1 SDK는 코디네이터 메시지를 루트 스트림에 유지하고 서브에이전트를 discovery 스냅샷으로 노출합니다. useMessages(stream, subagent) 같은 셀렉터 훅이나 컴포저블에 스냅샷을 전달하면 전문가의 범위 지정(scoped) 스트림을 렌더링할 수 있습니다.
여기서 LangChain 프론트엔드 SDK는 평평한 채팅 대화록을 넘어섭니다: 서브에이전트는 고유한 상태, 메시지, 도구 호출 메타데이터, 결과를 가진 일급 스트림 엔터티입니다. 여러분의 UI는 사용자가 모든 워커의 인터리브된 토큰을 읽게 하지 않고도 위임, 진행, 오류, 최종 종합을 보여줄 수 있습니다.
왜 셀렉터 기반 서브에이전트 스트림인가 (Why selector-based subagent streams)
루트 스트림은 코디네이터 대화에 집중된 상태를 유지합니다:
stream.messages는 코디네이터의 메시지만 포함합니다stream.subagents는 정체성, 네임스페이스, 상태를 가진 discovery 스냅샷을 포함합니다- 각 서브에이전트의 메시지, 도구 호출, 값은 셀렉터 헬퍼로 읽습니다
- UI는 깨끗하게 유지됩니다: 코디네이터의 추론은 전문가의 작업과 분리됩니다
이 분리를 통해 오케스트레이터의 메시지를 한 곳에서 렌더링하고, 사용자가 전문가 작업을 볼 필요가 있을 때만 서브에이전트 카드를 마운트할 수 있습니다.
대규모 작업에서도 이는 UI를 확장 가능하게 유지합니다. 사용자는 코디네이터의 상위 수준 계획을 훑어보고, 관심 있는 전문가 작업만 펼치며, 디버깅·감사·재생을 위한 전체 서브에이전트 추적은 그대로 보존할 수 있습니다.
useStream 설정 (Setting up useStream)
추가적인 스트림 옵션은 필요하지 않습니다. 스트림을 deep agent를 가리키도록 설정하고, stream.messages에서 코디네이터 메시지를 렌더링하며, stream.subagents를 사용해 활성 전문가를 위한 카드를 마운트하세요. 채팅 레이아웃에서는 각 카드가 작업을 위임한 코디네이터 턴 아래에 나타나도록 서브에이전트를 생성한 도구 호출 ID로 인덱싱하세요.
const AGENT_URL = "http://localhost:2024";
export function DeepAgentChat() {
const stream = useStream
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 Vue theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
<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>
<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>
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()]);
}
메시지 제출 (Submitting messages)
메시지는 루트 스트림을 통해 제출하세요. deep agent 워크플로우는 종종 여러 겹의 중첩 서브그래프를 포함하므로, 에이전트가 깊게 위임할 수 있다면 적절한 재귀 한도를 설정하세요:
stream.submit(
{ messages: [{ type: "human", content: text }] },
{ config: { recursion_limit: 100 } }
);
SubagentDiscoverySnapshot
각 SubagentDiscoverySnapshot은 스레드 안에서 실행되는 서브에이전트에 대한 가벼운 discovery 레코드입니다. 여러분의 UI에 서브에이전트가 존재한다는 것, 서브에이전트 트리에서 어디에 위치하는지, 어떤 수명 주기 상태에 있는지 알려줍니다.
스냅샷은 서브에이전트의 스트리밍된 메시지나 도구 호출을 포함하지 않습니다. 대신 useMessages(stream, subagent) 또는 useToolCalls(stream, subagent) 같은 셀렉터 훅에 스냅샷을 전달하세요. 이 훅들은 해당 카드나 패널이 마운트될 때만 스냅샷 네임스페이스를 사용해 서브에이전트의 스트림 프리미티브를 구독합니다.
SubagentCard 만들기 (Building the SubagentCard)
각 서브에이전트 카드는 전문가의 이름, 상태, 스트리밍 콘텐츠, 도구 호출을 표시합니다. 셀렉터 훅을 사용해 서브에이전트 네임스페이스를 구독하세요:
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)
사용자가 몇 개의 서브에이전트가 완료되었는지 알 수 있도록 진행률 표시줄과 카운터를 표시하세요:
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)
핵심 레이아웃 패턴은 루트 스트림에서 코디네이터 메시지를 렌더링하고, 해당 도구 호출이 서브에이전트를 생성한 AI 메시지에 서브에이전트 카드를 연결하는 것입니다:
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를 사용하세요. 그러면 사용자에게 지역적 컨텍스트와 전체 실행을 한눈에 보는 뷰를 모두 제공합니다.
모범 사례 (Best practices)
- 필요한 곳에만 셀렉터를 마운트하세요. 카드가
useMessages(stream, subagent)또는useToolCalls(stream, subagent)를 호출할 때 범위 지정된 메시지와 도구 호출이 스트리밍됩니다. - 전문가 이름을 표시하세요.
subagent.name은 어떤 워커가 활성인지 사용자에게 알려줍니다. - 접을 수 있는 카드를 사용하세요. 서브에이전트가 5개 이상인 워크플로우에서는 완료된 카드를 자동으로 접어 사용자가 활성 작업에 집중하게 하세요.
- 필요할 때만 재귀를 재정의하세요. Deep Agents는 높은 기본 재귀 한도를 설정합니다. 비정상적으로 깊은 커스텀 워크플로우에서만
config.recursion_limit을 전달하세요. - 서브에이전트별로 오류를 처리하세요. 한 서브에이전트의 실패가 전체 UI를 중단시키면 안 됩니다. 다른 서브에이전트가 계속 실행되는 동안 그 서브에이전트의 카드에 오류를 표시하세요.
관련 LangChain 가이드 (Related LangChain guides)
이 LangChain 프론트엔드 패턴들은 단일 에이전트 스트림과 같은 방식으로 서브에이전트 카드와 함께 작동합니다. Deep Agents는 동일한 useStream API 위에 구축되므로, 이 가이드들은 직접 적용됩니다:
더 알아보기 (Learn more)
- 이 문서를 MCP로 연결하면 Claude, VSCode 등에서 실시간 답변을 받을 수 있어요.
- GitHub에서 이 페이지 편집하기 또는 이슈 제출하기.