2장 — 라우팅

패턴 개요

1장의 프롬프트 체이닝은 결정론적이고 순서대로 동작하는 워크플로를 만드는 기초 기법이지만, 상황에 맞춰 유연하게 대응해야 하는 시나리오에서는 적용 범위가 제한됩니다. 실제 에이전틱 시스템은 환경 상태, 사용자 입력, 이전 작업 결과 등 상황에 따라 달라지는 요인을 바탕으로 여러 잠재 동작 가운데 하나를 선택해야 하는 경우가 많습니다.

이처럼 상황에 맞게 결정을 내리고 제어 흐름을 특화된 함수·도구·하위 프로세스로 분기하는 메커니즘이 라우팅(Routing) 입니다. 라우팅을 도입하면 에이전트는 고정된 실행 경로에서 벗어나, 특정 기준을 상황에 맞게 평가한 뒤 가능한 후속 동작 가운데 적절한 것을 선택하는 모델로 전환됩니다.

고객 문의를 처리하는 에이전트를 예로 들면 이렇게 동작합니다.

  1. 사용자 질의를 분석한다.
  2. 질의 의도에 따라 라우팅한다.
    • “주문 상태 확인”이면 주문 데이터베이스와 상호작용하는 하위 에이전트 또는 도구 체인으로 보낸다.
    • “제품 정보”면 제품 카탈로그를 검색하는 하위 에이전트 또는 체인으로 보낸다.
    • “기술 지원”이면 문제 해결 가이드에 접근하거나 사람에게 이관하는 별도 체인으로 보낸다.
    • 의도가 불분명하면 명확화를 위한 하위 에이전트 또는 프롬프트 체인으로 보낸다.

라우팅 메커니즘 네 가지

라우팅 패턴의 핵심 구성 요소는 입력을 평가하고 흐름을 분기하는 메커니즘입니다. 책은 네 가지 구현 방식을 구분합니다.

방식 결정 주체 특징
LLM 기반 라우팅 추론 시점의 생성 모델 프롬프트로 입력을 분석해 범주 식별자를 출력하게 한다
임베딩 기반 라우팅 벡터 유사도 질의 임베딩을 각 라우트 임베딩과 비교한다. 키워드가 아닌 의미로 판단하는 시맨틱 라우팅
규칙 기반 라우팅 사전 정의된 로직 키워드·패턴·구조화된 데이터 기반의 if-else, switch. 빠르고 결정론적이지만 유연성이 떨어진다
머신러닝 모델 기반 라우팅 파인튜닝된 판별 모델 소규모 레이블링 코퍼스로 학습한 분류기를 쓴다

LLM 기반 라우팅은 예를 들어 “다음 사용자 질의를 분석하고 범주만 출력하시오: ‘Order Status’, ‘Product Info’, ‘Technical Support’, 또는 ‘Other’” 처럼 지시하고, 시스템이 그 출력을 읽어 워크플로를 분기합니다.

머신러닝 모델 기반 라우팅은 임베딩 기반 방식과 개념적으로 비슷해 보이지만 핵심 차이가 있습니다. 지도 학습 과정을 거쳐 모델 파라미터를 조정해 특화된 라우팅 함수를 만든다는 점입니다. LLM 기반 라우팅과 구별되는 이유는, 의사 결정 요소가 추론 시점에 프롬프트를 실행하는 생성 모델이 아니라는 데 있습니다. 라우팅 로직은 파인튜닝된 모델의 학습 가중치에 저장됩니다. LLM은 학습 데이터셋을 보강할 합성 데이터를 만드는 전처리 단계에서 쓰일 수는 있으나, 실시간 라우팅 결정 자체에는 관여하지 않습니다.

라우팅을 배치하는 지점

라우팅 메커니즘은 에이전트 운영 주기의 여러 지점에 놓을 수 있습니다.

  • 시작 단계 — 기본 작업을 분류한다
  • 처리 체인의 중간 — 후속 동작을 결정한다
  • 서브루틴 안 — 주어진 도구 집합에서 가장 적절한 도구를 선택한다

LangChain, LangGraph, 구글 ADK 같은 연산 프레임워크는 이러한 조건 로직을 정의하고 관리할 명시적 구성 요소를 갖추고 있습니다. 특히 상태 기반 그래프 아키텍처를 갖춘 LangGraph는 시스템 전체의 누적 상태에 따라 결정이 달라지는 복잡한 라우팅 시나리오에 적합합니다.

실제 적용 및 활용 사례

도메인 라우팅이 하는 일
가상 비서 · AI 튜터 자연어 질의를 해석해 후속 동작을 결정한다. 정보 검색 도구 호출, 사람 운영자 이관, 성과에 따른 다음 커리큘럼 모듈 선택
데이터 · 문서 처리 파이프라인 이메일·지원 티켓·API 페이로드를 콘텐츠와 형식에 따라 분석해 해당 워크플로로 분기한다. 영업 리드 수집, JSON/CSV 변환, 긴급 이슈 이관 경로
멀티 에이전트 리서치 시스템 상위 디스패처 역할. 검색·요약·분석 담당 에이전트 중 현재 목표에 가장 적합한 쪽에 작업을 배정한다
AI 코딩 도우미 프로그래밍 언어와 사용자 의도(디버그·설명·변환)를 식별한 뒤 코드 스니펫을 알맞은 전문 도구로 전달한다

궁극적으로 라우팅은 에이전트를 사전 정의된 시퀀스를 정적으로 실행하는 존재에서, 목표 달성에 가장 효과적인 방법을 스스로 결정하는 동적 시스템으로 전환합니다.

실습 코드 예제 (LangChain)

요청 의도(예약·정보·불분명)에 따라 사용자 요청을 시뮬레이션된 하위 에이전트 핸들러로 라우팅하는 “코디네이터”를 만듭니다. 멀티 에이전트 아키텍처에서 흔히 볼 수 있는 기본 위임 패턴을 시뮬레이션한 것입니다.

pip install langchain langgraph google-cloud-aiplatform langchain-google-genai google-adk deprecated pydantic
# Copyright (c) 2025 Marco Fago — MIT License

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableBranch

# --- 구성 ---
# API 키 환경 변수가 설정되어 있는지 확인한다 (예: GOOGLE_API_KEY)
try:
    llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0)
    print(f"Language model initialized: {llm.model}")
except Exception as e:
    print(f"Error initializing language model: {e}")
    llm = None

# --- 시뮬레이션 서브 에이전트 핸들러 정의 (ADK의 sub_agents 에 해당) ---

def booking_handler(request: str) -> str:
    """예약 에이전트가 요청을 처리하는 동작을 시뮬레이션한다."""
    print("\n--- DELEGATING TO BOOKING HANDLER ---")
    return f"Booking Handler processed request: '{request}'. Result: Simulated booking action."

def info_handler(request: str) -> str:
    """정보 에이전트가 요청을 처리하는 동작을 시뮬레이션한다."""
    print("\n--- DELEGATING TO INFO HANDLER ---")
    return f"Info Handler processed request: '{request}'. Result: Simulated information retrieval."

def unclear_handler(request: str) -> str:
    """위임할 수 없는 요청을 처리한다."""
    print("\n--- HANDLING UNCLEAR REQUEST ---")
    return f"Coordinator could not delegate request: '{request}'. Please clarify."

# --- 코디네이터 라우터 체인 정의 (ADK 코디네이터의 지시에 해당) ---
# 이 체인은 어떤 핸들러에 위임할지 결정한다.
coordinator_router_prompt = ChatPromptTemplate.from_messages([
    ("system", """Analyze the user's request and determine which specialist handler should process it.
 - If the request is related to booking flights or hotels, output 'booker'.
 - For all other general information questions, output 'info'.
 - If the request is unclear or doesn't fit either category, output 'unclear'.
ONLY output one word: 'booker', 'info', or 'unclear'."""),
    ("user", "{request}")
])

if llm:
    coordinator_router_chain = coordinator_router_prompt | llm | StrOutputParser()

# --- 위임 로직 정의 (ADK의 sub_agents 기반 Auto-Flow 에 해당) ---
# RunnableBranch 를 사용해 라우터 체인의 출력에 따라 라우팅한다.
branches = {
    "booker": RunnablePassthrough.assign(output=lambda x: booking_handler(x['request']['request'])),
    "info": RunnablePassthrough.assign(output=lambda x: info_handler(x['request']['request'])),
    "unclear": RunnablePassthrough.assign(output=lambda x: unclear_handler(x['request']['request'])),
}

# 원래 입력('request')을 해당 핸들러로 라우팅한다.
delegation_branch = RunnableBranch(
    (lambda x: x['decision'].strip() == 'booker', branches["booker"]),
    (lambda x: x['decision'].strip() == 'info', branches["info"]),
    branches["unclear"]  # 'unclear' 또는 그 밖의 출력에 대한 기본 분기
)

# 라우터 체인과 위임 분기를 하나의 runnable 로 결합
# 라우터 체인의 출력('decision')을 원래 입력('request')과 함께 전달해 delegation_branch 로 넘긴다.
coordinator_agent = {
    "decision": coordinator_router_chain,
    "request": RunnablePassthrough()
} | delegation_branch | (lambda x: x['output'])  # 최종 출력 추출

# --- 사용 예 ---
def main():
    if not llm:
        print("\nSkipping execution due to LLM initialization failure.")
        return

    print("--- Running with a booking request ---")
    request_a = "Book me a flight to London."
    result_a = coordinator_agent.invoke({"request": request_a})
    print(f"Final Result A: {result_a}")

    print("\n--- Running with an info request ---")
    request_b = "What is the capital of Italy?"
    result_b = coordinator_agent.invoke({"request": request_b})
    print(f"Final Result B: {result_b}")

    print("\n--- Running with an unclear request ---")
    request_c = "Tell me about quantum physics."
    result_c = coordinator_agent.invoke({"request": request_c})
    print(f"Final Result C: {result_c}")

if __name__ == "__main__":
    main()

핵심 구성 요소는 coordinator_router_chain 입니다. ChatPromptTemplate 로 언어 모델에게 수신된 요청을 booker, info, unclear 세 범주 가운데 하나로 분류하도록 지시합니다. 이 라우터 체인의 출력을 받아 RunnableBranch 가 원래 요청을 해당 핸들러 함수에게 위임합니다. coordinator_agent 는 이 컴포넌트를 결합해, 먼저 요청을 라우팅해 결정을 내린 다음 선택된 핸들러로 전달하고 최종 출력을 핸들러 응답에서 추출합니다.

실습 코드 예제 (구글 ADK)

Agent Development Kit(ADK)는 에이전트 기능과 행동을 정의하는 프레임워크입니다. LangGraph 같은 명시적 연산 그래프 기반 아키텍처와 달리, ADK 패러다임에서 라우팅은 보통 에이전트 기능을 나타내는 개별 “도구” 집합을 정의하는 방식으로 구현합니다. 사용자 질의에 맞는 적절한 도구를 선택하는 일은 프레임워크 내부 로직이 관리하며, 기저 모델을 활용해 사용자 의도를 알맞은 기능 핸들러에 매칭합니다.

# Copyright (c) 2025 Marco Fago — MIT License

import uuid
from typing import Dict, Any, Optional
from google.adk.agents import Agent
from google.adk.runners import InMemoryRunner
from google.adk.tools import FunctionTool
from google.genai import types
from google.adk.events import Event

# --- 도구 함수 정의 ---
# 이 함수들은 특화 에이전트의 동작을 시뮬레이션한다.

def booking_handler(request: str) -> str:
    """
    항공편과 호텔 예약 요청을 처리한다.
    Args:
        request: 예약에 대한 사용자 요청.
    Returns:
        예약이 처리되었음을 확인하는 메시지.
    """
    print("------------- Booking Handler Called -------------")
    return f"Booking action for '{request}' has been simulated."

def info_handler(request: str) -> str:
    """
    일반 정보 요청을 처리한다.
    Args:
        request: 사용자의 질문.
    Returns:
        정보 요청이 처리되었음을 나타내는 메시지.
    """
    print("------------- Info Handler Called -------------")
    return f"Information request for '{request}'. Result: Simulated information retrieval."

def unclear_handler(request: str) -> str:
    """위임할 수 없는 요청을 처리한다."""
    return f"Coordinator could not delegate request: '{request}'. Please clarify."

# --- 함수로부터 도구 생성 ---
booking_tool = FunctionTool(booking_handler)
info_tool = FunctionTool(info_handler)

# 각 도구를 장착한 특화 서브 에이전트 정의
booking_agent = Agent(
    name="Booker",
    model="gemini-2.0-flash",
    description="A specialized agent that handles all flight and hotel booking requests by calling the booking tool.",
    tools=[booking_tool]
)

info_agent = Agent(
    name="Info",
    model="gemini-2.0-flash",
    description="A specialized agent that provides general information and answers user questions by calling the info tool.",
    tools=[info_tool]
)

# 명시적 위임 지시를 갖는 상위 에이전트 정의
coordinator = Agent(
    name="Coordinator",
    model="gemini-2.0-flash",
    instruction=(
        "You are the main coordinator. Your only task is to analyze incoming user requests "
        "and delegate them to the appropriate specialist agent. Do not try to answer the user directly.\n"
        "- For any requests related to booking flights or hotels, delegate to the 'Booker' agent.\n"
        "- For all other general information questions, delegate to the 'Info' agent."
    ),
    description="A coordinator that routes user requests to the correct specialist agent.",
    # sub_agents 가 있으면 기본적으로 LLM 기반 위임(Auto-Flow)이 활성화된다.
    sub_agents=[booking_agent, info_agent]
)

# --- 실행 로직 ---

async def run_coordinator(runner: InMemoryRunner, request: str):
    """주어진 요청으로 코디네이터 에이전트를 실행하고 위임한다."""
    print(f"\n--- Running Coordinator with request: '{request}' ---")
    final_result = ""
    try:
        user_id = "user_123"
        session_id = str(uuid.uuid4())
        await runner.session_service.create_session(
            app_name=runner.app_name, user_id=user_id, session_id=session_id
        )
        for event in runner.run(
            user_id=user_id,
            session_id=session_id,
            new_message=types.Content(
                role='user',
                parts=[types.Part(text=request)]
            ),
        ):
            if event.is_final_response() and event.content:
                # event.content 에서 텍스트를 직접 가져오려고 시도한다 (parts 를 순회하지 않기 위해)
                if hasattr(event.content, 'text') and event.content.text:
                    final_result = event.content.text
                elif event.content.parts:
                    # 폴백: parts 를 순회하며 텍스트를 추출한다 (경고가 발생할 수 있음)
                    text_parts = [part.text for part in event.content.parts if part.text]
                    final_result = "".join(text_parts)
                # 최종 응답 이후 루프를 종료한다고 가정한다
                break
        print(f"Coordinator Final Response: {final_result}")
        return final_result
    except Exception as e:
        print(f"An error occurred while processing your request: {e}")
        return f"An error occurred while processing your request: {e}"

async def main():
    """ADK 예제를 실행하는 메인 함수."""
    print("--- Google ADK Routing Example (ADK Auto-Flow Style) ---")
    print("Note: This requires Google ADK installed and authenticated.")
    runner = InMemoryRunner(coordinator)

    # 사용 예
    result_a = await run_coordinator(runner, "Book me a hotel in Paris.")
    print(f"Final Output A: {result_a}")

    result_b = await run_coordinator(runner, "What is the highest mountain in the world?")
    print(f"Final Output B: {result_b}")

    result_c = await run_coordinator(runner, "Tell me a random fact.")  # Info 로 라우팅되어야 한다
    print(f"Final Output C: {result_c}")

    result_d = await run_coordinator(runner, "Find flights to Tokyo next month.")  # Booker 로 라우팅되어야 한다
    print(f"Final Output D: {result_d}")

if __name__ == "__main__":
    import nest_asyncio
    nest_asyncio.apply()
    await main()

Coordinator 에이전트의 주요 역할은 지시에 정의된 대로 수신 사용자 메시지를 분석하여 Booker 또는 Info 에이전트에게 위임하는 것입니다. Coordinator 에 sub_agents 가 정의되어 있으므로 ADK 의 Auto-Flow 메커니즘이 이 위임을 자동으로 처리합니다. unclear_handler 는 코디네이터가 위임할 수 없는 요청의 폴백으로 포함했으나, 현재 코디네이터 로직의 메인 run_coordinator 함수에서는 위임 실패 시 명시적으로 사용하지 않습니다.

라우팅 패턴 개요도

                  ┌──► 에이전트 ──► 출력 ─┐
                  │                        │
  사용자 ──► 프롬프트 n ──► [ LLM 라우터 ] ──┼──►  · · ·             │
     ▲                                     │                        │
     │            └──► 에이전트 ──► 출력 ─┤                        │
     └─────────────────────────────────────┴────────────────────────┘

그림 2.1 — LLM을 라우터로 사용하는 라우팅 패턴

정리

라우팅이란 무엇인가?

에이전틱 시스템은 단일한 선형 프로세스로는 처리할 수 없는 매우 다양한 입력과 상황에 대응해야 하는 경우가 많습니다. 단순한 순차 워크플로에는 맥락에 따른 의사결정 능력이 없습니다. 특정 작업에 적합한 도구나 하위 프로세스를 선택하는 메커니즘이 없으면 시스템은 경직되어 적응력을 갖추지 못합니다.

왜 사용하는가?

라우팅 패턴은 에이전트 운영 프레임워크에 조건 로직을 도입하는 표준화된 해법입니다. 시스템이 먼저 수신된 질의를 분석해 의도나 성격을 판별하고, 그 결과에 따라 가장 적절한 전문 도구·함수·하위 에이전트로 제어 흐름을 분기합니다. 이 결정은 LLM 프롬프팅, 사전 정의된 규칙, 임베딩 기반 시맨틱 유사도 등 다양한 방법으로 이루어집니다.

언제 사용해야 하는가?

에이전트가 사용자 입력이나 현재 상태에 따라 여러 워크플로·도구·하위 에이전트 가운데 하나를 결정해야 할 때 사용합니다. 수신 요청을 분류해 서로 다른 유형의 작업을 처리해야 하는 애플리케이션에 필수적입니다. 고객 지원 봇이 영업 문의, 기술 지원, 계정 관리 질문을 구분하는 경우가 대표적입니다.

핵심 정리

  • 라우팅을 적용하면 에이전트가 조건에 따라 워크플로의 다음 단계를 유연하게 결정합니다.
  • 에이전트가 다양한 입력을 처리하고 행동을 조정하여 고정된 작업 흐름에서 벗어날 수 있게 합니다.
  • 라우팅 로직은 LLM, 규칙 기반 시스템, 임베딩 유사도를 활용해 구현합니다.
  • LangGraph 와 구글 ADK 같은 프레임워크는 아키텍처 접근 방식은 다르지만, 에이전트 워크플로에서 라우팅을 정의하고 관리하는 구조화된 방법을 갖추고 있습니다.

결론

라우팅 패턴은 유연하고 민첩하게 반응하는 에이전틱 시스템을 구축하는 핵심 수단입니다. 라우팅을 구현하면 단순하고 정해진 순서대로만 진행되는 실행 흐름을 넘어, 에이전트가 정보를 처리하고 사용자 입력에 응답하고 사용 가능한 도구나 하위 에이전트를 활용하는 방법을 지능적으로 결정할 수 있게 됩니다.

LangChain 과 구글 ADK 예제는 서로 다르지만 효과적인 두 가지 접근법을 보여줍니다.

  • LangGraph — 그래프 기반 구조로 상태와 전이를 시각적이고 명시적으로 정의할 수 있어, 복잡한 라우팅 로직을 가진 다단계 워크플로에 적합합니다.
  • 구글 ADK — 개별 기능(도구)을 정의하는 데 초점을 맞추고 요청을 적절한 도구 핸들러로 라우팅하는 프레임워크 능력에 의존합니다. 개별 행동이 잘 정의된 에이전트라면 ADK 방식이 더 단순할 수 있습니다.

참고 문헌