라우터
라우터 (Router)
라우터(router) 아키텍처에서는 라우팅 단계가 입력을 분류하고 전문화된 에이전트로 보냅니다. 서로 다른 버티컬(verticals) — 각각 자체 에이전트가 필요한 분리된 지식 도메인 — 이 있을 때 특히 유용해요. 질문이 들어오면 라우터가 "어디로 보낼까?"를 먼저 판단하고, 그다음 알맞은 에이전트를 호출하는 식이죠.
핵심 특징 (Key characteristics)
- 라우터가 질의를 분해합니다.
- 0개 이상의 전문화된 에이전트가 병렬로 호출됩니다.
- 결과가 일관된 응답으로 합성됩니다.
언제 쓰나요? (When to use)
서로 다른 버티컬(각각 자체 에이전트가 필요한 분리된 지식 도메인)이 있거나, 여러 소스를 병렬로 질의해야 하거나, 결과를 결합된 응답으로 합성하고 싶을 때 라우터 패턴을 쓰면 돼요.
기본 구현 (Basic implementation)
라우터는 질의를 분류하고 적절한 에이전트로 보냅니다. 단일 에이전트 라우팅에는 Command를, 여러 에이전트로의 병렬 팬아웃에는 Send를 사용해요.
단일 에이전트 — Command로 단일 전문화된 에이전트에 라우팅합니다:
from langgraph.types import Command
def classify_query(query: str) -> str:
"""Use LLM to classify query and determine the appropriate agent."""
# Classification logic here
...
def route_query(state: State) -> Command:
"""Route to the appropriate agent based on query classification."""
active_agent = classify_query(state["query"])
# Route to the selected agent
return Command(goto=active_agent)
다중 에이전트 (병렬) — Send로 여러 전문화된 에이전트에 병렬 팬아웃합니다:
from typing import TypedDict
from langgraph.types import Send
class ClassificationResult(TypedDict):
query: str
agent: str
def classify_query(query: str) -> list[ClassificationResult]:
"""Use LLM to classify query and determine which agents to invoke."""
# Classification logic here
...
def route_query(state: State):
"""Route to relevant agents based on query classification."""
classifications = classify_query(state["query"])
# Fan out to selected agents in parallel
return [
Send(c["agent"], {"query": c["query"]})
for c in classifications
]
무상태 vs. 상태 보존 (Stateless vs. stateful)
라우터에는 두 가지 접근이 있어요.
- 무상태 라우터 (Stateless routers): 각 요청을 독립적으로 처리합니다.
- 상태 보존 라우터 (Stateful routers): 여러 요청에 걸쳐 대화 기록을 유지합니다.
무상태 (Stateless)
각 요청은 독립적으로 라우팅됩니다 — 호출 사이에 메모리가 없어요. 다중 턴 대화는 상태 보존 라우터를 참고하세요.
Router vs. Subagents: 두 패턴 모두 여러 에이전트에 작업을 분배할 수 있지만, 라우팅 결정을 내리는 방식이 달라요.
- Router: 입력을 분류하고 에이전트에 분배하는 전용 라우팅 단계(종종 단일 LLM 호출이나 규칙 기반 로직)입니다. 라우터 자체는 대화 기록을 유지하거나 다중 턴 오케스트레이션을 수행하지 않아요 — 전처리 단계(preprocessing step)죠.
- Subagents: 메인 슈퍼바이저 에이전트가 진행 중인 대화의 일부로 어느 서브에이전트를 호출할지 동적으로 결정합니다. 메인 에이전트가 컨텍스트를 유지하고, 턴에 걸쳐 여러 서브에이전트를 호출하며, 복잡한 다단계 워크플로를 오케스트레이션합니다.
명확한 입력 카테고리가 있고 결정적이거나 가벼운 분류를 원할 때는 라우터를, 진화하는 컨텍스트를 바탕으로 LLM이 다음 행동을 결정하는 유연하고 대화 인지형 오케스트레이션이 필요할 때는 슈퍼바이저를 쓰세요.
상태 보존 (Stateful)
다중 턴 대화에서는 호출 간 컨텍스트를 유지해야 해요.
도구 래퍼 (Tool wrapper)
가장 단순한 접근: 무상태 라우터를 대화형 에이전트가 호출할 수 있는 도구로 감싸는 방식입니다. 대화형 에이전트가 메모리와 컨텍스트를 처리하고, 라우터는 무상태로 유지되죠. 여러 병렬 에이전트에 걸쳐 대화 기록을 관리하는 복잡성을 피할 수 있어요.
@tool
def search_docs(query: str) -> str:
"""Search across multiple documentation sources."""
result = workflow.invoke({"query": query})
return result["final_answer"]
# Conversational agent uses the router as a tool
conversational_agent = create_agent(
model,
tools=[search_docs],
prompt="You are a helpful assistant. Use search_docs to answer questions."
)
전체 지속 (Full persistence)
라우터 자체가 상태를 유지해야 한다면 영속성을 사용해 메시지 기록을 저장해요. 에이전트로 라우팅할 때 상태에서 이전 메시지를 가져와 에이전트의 컨텍스트에 선택적으로 포함시키는데, 이게 컨텍스트 엔지니어링의 핵심 레버입니다.
상태 보존 라우터는 커스텀 기록 관리가 필요해요. 라우터가 턴 사이에서 에이전트를 전환하면, 에이전트마다 어조나 프롬프트가 다르므로 대화가 사용자에게 매끄럽게 느껴지지 않을 수 있어요. 병렬 호출에서는 라우터 레벨에서 기록(입력과 합성된 출력)을 유지하고 이 기록을 라우팅 로직에 활용해야 합니다. 대신 핸드오프 패턴이나 서브에이전트 패턴을 고려해보세요 — 둘 다 다중 턴 대화에 더 명확한 의미를 제공합니다.