본문 바로가기

블로그/딥러닝

멀티 에이전트 시스템과 Context

LLM 기반 시스템을 설계하다 보면 결국 하나의 질문으로 수렴한다.
“모델에게 어떤 Context를 줄 것인가?”이다.

 

LLM은 단순히 질문에 답하는 엔진이 아니라, 입력된 Context를 기반으로 다음 토큰을 생성하는 구조다. 즉, 모델의 성능은 모델 자체보다 어떤 정보를 어떤 방식으로 주입하느냐에 더 크게 영향을 받는다. 이때 Context는 단순한 프롬프트가 아니라, 모델이 참조하는 모든 정보의 집합이며 LLM은 이를 토큰 단위로 처리한다.

 

일반적으로 Context Window에는 지시문, 지식, 그리고 도구가 포함된다. 이 세 요소가 어떻게 구성되는지에 따라 모델의 행동이 달라지며, 특히 Tool 사용 방식과 정보 전달 전략은 시스템 아키텍처를 결정짓는 핵심 요소가 된다.

 

이러한 배경에서 등장한 개념이 Agentic AI다. Agentic AI는 단순 응답 모델이 아니라, 목표를 달성하기 위해 스스로 계획하고 행동하는 구조를 가진다. 환경을 인식하고, 판단을 통해 계획을 세운 뒤, 행동을 수행하고, 필요 시 다시 계획을 수정하는 루프를 반복한다. 이 과정은 인식–판단–실행–재계획(Loop)이라는 흐름으로 설명할 수 있다.

 

하지만 하나의 에이전트로 모든 작업을 처리하려 하면 한계가 빠르게 드러난다. 다양한 작업을 동시에 처리하면서 의사결정 오류가 증가하고, 메시지가 누적되면서 Context가 비대해진다. 특히 ReAct 구조에서는 메시지가 계속 쌓이기 때문에 Context 포화가 빠르게 발생하고, 결국 정확도와 추적 가능성이 모두 떨어진다.

 

이 문제를 해결하기 위해 멀티 에이전트 구조가 등장한다. 핵심은 하나의 거대한 Context를 유지하는 것이 아니라, 여러 개의 작은 Context로 나누어 관리하는 것이다. 각 에이전트는 특정 역할에 집중하고, 필요한 정보만 공유하면서 협업한다.

 

이때 중요한 설계 포인트는 “어떻게 협업할 것인가”이다. 즉, 아래 핵심 설계 요소를 어떻게 구현할 것인지에 따라서 멀티 에이전트의 디자인 패턴이 달라질 수 있다.

 

(1) 구조(Topology): 누가 흐름을 제어하는가?

(2) 입력(Input): 하위 작업자에게 맥락을 어떻게 전달하는가?

(3) 출력(Output): 작업자의 결과를 어디까지 공유하는가?

 

1. 누가 흐름을 제어하는가?

 

 

어떤 에이전트(또는 팀)이 작업을 할 것인가를 다음과 같은 방식으로 조정하여 협업할 수 있다.

▪ Round Robin : 에이전트들이 고정된 순서로 차례대로 발화하며 작업을 진행

▪ Supervisor: 현재 대화 흐름에 맞춰 가장 적합한 에이전트를 선택하여 다음 작업 진행

▪ Swarm : 모든 에이전트가 서로에게 작업을 위임(handoff)

 

2. 하위 작업자에게 맥락을 어떻게 전달하는가?

  Handoff (핸드오프) : 전체 대화 기록을 공유 및 작업 제어권 이전

  Agents-as-Tools(에이전트 호출) : 지휘자 에이전트가 하위 에이전트를 함수로 호출

 

3. 출력을 어디까지 공유하는가?

결과만 공유(Final Result Only): 필요한 결과/요약만 다음 에이전트에 전환

전체 컨텍스트 공유(Full Context Sharing): 모든 에이전트가 사고 과정을 포함한 대화 전체 히스토리에 접근

 

비교 항목 Handoff 방식(Network/Relay) Agents as Tool 방식(Supervisor/Subroutine)
핵심 철학 맥락의 공유 (Shared Context)모든 에이전트가 전체 대화 흐름을 파악하고 협력 맥락의 격리 (Context Isolation)필요한 정보만 선별하여 효율적으로 작업
데이터 전달 범위 전체 상태 (Full State)messages, artifacts 등 전체 전달 선별된 인자 (Filtered Arguments)필요한 값만 전달예: { "query": "...", "limit": 5 }
제어 흐름 단방향 이동 (Jump / Goto)제어권이 다음 에이전트로 넘어감 왕복 호출 (Call & Return)Supervisor가 호출 후 결과를 받아 다시 판단
토큰 효율성 낮음 (비용 증가) 전체 맥락을 계속 읽어야 함 높음 (비용 절감) 필요한 정보만 전달
장점 • 유연한 흐름 (동적 위임 가능)• 협업 자연스러움 (맥락 공유)• HITL(사람 개입) 용이 • 강한 통제력 (입력 완전 설계 가능)• 보안 강화 (민감 정보 차단)• 할루시네이션 감소
단점 • 정보 오염 가능 (불필요한 사고 과정 포함)• 무한 루프 위험 (에이전트 간 핑퐁) • Supervisor 병목• Schema 설계 필요 (구현 난이도 상승)
비유 릴레이 달리기→ 바통(전체 문맥)을 그대로 전달 팀장-팀원 구조→ 팀장이 필요한 일만 지시

 

 

멀티 에이전트는 다양한 구조를 가질 수 있지만, 실무에서는 주로 Supervisor 기반 구조나 Handoff 기반 구조가 사용된다. 특히 Handoff는 에이전트 간 제어권을 넘기는 핵심 메커니즘으로, 단순한 호출이 아니라 상태(state)를 함께 전달하는 것이 중요하다.


Handoff의 구현 예시

아래는 LangGraph의 문법을 활용해 Handoff를 Tool 형태로 구현한 예시다. 핵심은 다음 에이전트에게 단순히 명령을 전달하는 것이 아니라, 현재까지의 메시지 상태를 함께 넘기는 것이다.

from typing import Annotated
from langchain_core.messages import ToolMessage, HumanMessage, SystemMessage, AIMessage
from langchain_core.tools import InjectedToolCallId, tool
from langgraph.prebuilt import InjectedState
from langgraph.graph import StateGraph, START, MessagesState, END
from langgraph.types import Command

def create_handoff_tool(*, agent_name: str, description: str | None = None):
    name = f"transfer_to_{agent_name}"
    description = description or f"Transfer to {agent_name}"

    @tool(name, description=description)
    def handoff_tool(
        state: Annotated[dict, InjectedState],
        tool_call_id: Annotated[str, InjectedToolCallId],
    ) -> Command:
        messages = state.get("messages", [])

        tool_message = ToolMessage(
            content=f"Successfully transferred to {agent_name}",
            name=name,
            tool_call_id=tool_call_id,
        )

        return Command(
            graph=Command.PARENT,
            goto=agent_name,
            update={"messages": messages + [tool_message]},
        )

    return handoff_tool
 

이 코드에서 중요한 부분은 update={"messages": ...}이다. 이 부분이 없으면 다음 에이전트는 이전 맥락을 알 수 없기 때문에, 사실상 작업이 끊긴 상태로 시작하게 된다.


Supervisor + Worker 구조 (Handoff 기반)

아래는 Supervisor + Worker 구조를 가진 간단한 멀티 에이전트 예시다.

 
# ============================================================
# 2) 도메인 툴 (Worker가 호출)
# ============================================================
from langchain_core.tools import tool

@tool
def book_flight(from_airport: str, to_airport: str) -> str:
    """
    항공편을 예약합니다.

    Args:
        from_airport: 출발 공항의 IATA 코드 또는 이름 (예: "ICN", "인천")
        to_airport: 도착 공항의 IATA 코드 또는 이름 (예: "PUS", "부산")

    Returns:
        예약 완료 메시지 문자열
    """
    return f"{from_airport}에서 {to_airport}행 항공편 예약이 완료되었습니다."


@tool
def book_hotel(hotel_name: str) -> str:
    """
    호텔 숙박을 예약합니다.

    Args:
        hotel_name: 예약할 호텔 이름 (예: "해운대 파라다이스 호텔")

    Returns:
        예약 완료 메시지 문자열
    """
    return f"{hotel_name} 숙박 예약이 완료되었습니다."



# ============================================================
# 3) Worker Agents
# ============================================================
flight_agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[book_flight],
    system_prompt=(
        "당신은 항공편 예약 어시스턴트입니다.\n"
        "- 항공 관련 요청만 처리합니다.\n"
        "- 사용자가 출발/도착 공항을 이미 제공했다면 즉시 book_flight를 호출하세요.\n"
        "- book_flight에 필요한 정보는 from_airport, to_airport 뿐입니다.\n"
        "- 응답은 도구 실행 결과만 간결히 출력하세요."
    ),
    name="flight_agent",
)

hotel_agent = create_agent(
    model="openai:gpt-5-mini",
    tools=[book_hotel],
    system_prompt=(
        "당신은 호텔 예약 어시스턴트입니다.\n"
        "- 숙박 관련 요청만 처리합니다.\n"
        "- 사용자가 호텔 이름을 제공했다면 즉시 book_hotel 도구를 호출하세요.\n"
        "- book_hotel에 필요한 정보는 hotel_name 뿐입니다.\n"
        "- 응답은 도구 실행 결과만 간결히 출력하세요."
    ),
    name="hotel_agent",
)


# ============================================================
# 4) Supervisor Agent (+ handoff tools)
# ============================================================
handoff_to_flight = create_handoff_tool(
    agent_name="flight_agent",
    description="항공 작업을 flight_agent에게 이관합니다.")

handoff_to_hotel = create_handoff_tool(
    agent_name="hotel_agent",
    description="숙박 작업을 hotel_agent에게 이관합니다.")

supervisor = create_agent(
    model="openai:gpt-5-mini",
    tools=[handoff_to_flight, handoff_to_hotel],
    system_prompt=(
        "당신은 항공/호텔 예약 팀의 슈퍼바이저입니다.\n"
        "- 항공 요청은 transfer_to_flight_agent로, 호텔 요청은 transfer_to_hotel_agent로 이관하세요.\n"
        "- 한 번에 한 에이전트만 이관합니다.\n"
        "- 추가 정보 요청을 만들지 말고, 사용자가 준 정보만으로 진행하세요.\n"
        "- 모든 작업이 끝나면 최종 요약만 말하고 종료하세요."
    ),
    name="supervisor",
)

# ============================================================
# 5) Graph 구성 (Supervisor -> Worker -> Supervisor -> END)
# ============================================================
supervisor_with_handoff = (
    StateGraph(MessagesState)
    .add_node("supervisor", supervisor)
    .add_node("flight_agent", flight_agent)
    .add_node("hotel_agent", hotel_agent)
    .add_edge(START, "supervisor")
    .add_edge("flight_agent", "supervisor")
    .add_edge("hotel_agent", "supervisor")
    .add_edge("supervisor", END)  # supervisor가 tool call 없이 마무리하면 종료
    .compile()
)

 

이 구조에서 Supervisor는 직접 작업을 수행하지 않고, 적절한 Worker에게 작업을 위임하는 역할을 한다. 즉, Planner 역할을 수행한다.


실행 결과 예시

ICN에서 PUS행 항공편 예약이 완료되었습니다.
해운대 파라다이스 호텔 숙박 예약이 완료되었습니다.
 

이 흐름을 보면 Supervisor → Worker → Supervisor → Worker 형태로 제어권이 이동하는 것을 확인할 수 있다.


Agents-as-Tools 구조

반대로 Agents-as-Tools 방식에서는 에이전트를 함수처럼 호출한다.

@tool
def delegate_flight_agent(from_airport: str, to_airport: str, runtime) -> str:
    result = flight_agent.invoke(...)
    return result["messages"][-1].content

 

이 구조에서는 하위 에이전트가 전체 맥락을 알 필요가 없다.
Supervisor가 필요한 정보만 전달하고 결과만 받아온다.

즉, Handoff와 달리 Context를 공유하지 않고, 통제된 방식으로 호출한다.


Swarm 구조 (Supervisor 없는 협업)

Swarm 구조에서는 Supervisor가 존재하지 않는다.
각 에이전트가 서로에게 직접 작업을 위임한다.

from typing import Annotated
from langchain_core.messages import ToolMessage
from langchain_core.tools import InjectedToolCallId, tool
from langgraph.prebuilt import InjectedState
from langgraph.graph import StateGraph, START, MessagesState, END
from langgraph.types import Command
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent

def create_handoff_tool(*, agent_name: str, description: str | None = None):
    name = f"transfer_to_{agent_name}"
    description = description or f"Transfer to {agent_name}"

    @tool(name, description=description)
    def handoff_tool(
        state: Annotated[dict, InjectedState],
        tool_call_id: Annotated[str, InjectedToolCallId],
    ) -> Command:
        messages = state.get("messages", [])

        tool_message = ToolMessage(
            content=f"Successfully transferred to {agent_name}",
            name=name,
            tool_call_id=tool_call_id,
        )

        return Command(
            graph=Command.PARENT,
            goto=agent_name,
            update={"messages": messages + [tool_message]},
        )

    return handoff_tool


# Handoffs
transfer_to_hotel_agent = create_handoff_tool(
    agent_name="hotel_agent",
    description="Transfer user to the hotel-booking agent.",
)
transfer_to_flight_agent = create_handoff_tool(
    agent_name="flight_agent",
    description="Transfer user to the flight-booking agent.",
)


@tool
def book_flight(from_airport: str, to_airport: str) -> str:
    """항공편을 예약합니다."""
    return f"{from_airport}에서 {to_airport}행 항공편 예약이 완료되었습니다."

@tool
def book_hotel(hotel_name: str) -> str:
    """호텔 이름으로 예약할 수 있는 도구입니다."""
    return f"{hotel_name} 숙박 예약이 완료되었습니다."



# 워커 에이전트 정의 (ReAct)
flight_agent = create_agent(
    model=ChatOpenAI(model="gpt-5-mini").bind_tools([book_flight, transfer_to_hotel_agent], parallel_tool_calls=False),
    tools=[book_flight, transfer_to_hotel_agent],
    system_prompt=(
        "당신은 항공편 예약 어시스턴트입니다.\n"
        "- 항공 관련 요청만 처리하세요.\n"
        "- 출발지와 도착지 정보만 있으면 예약이 가능합니다.\n"
        "- 여러 개의 도구를 동시에 호출하지 마세요.\n"
        "- 본인이 해결할 수 없는 작업은 다른 에이전트에게 이관합니다.\n"
        "- 모든 작업이 완료되었으면 작업을 종료합니다."
    ),
    name="flight_agent",
)

hotel_agent = create_agent(
    model=ChatOpenAI(model="gpt-5-mini").bind_tools([book_hotel, transfer_to_flight_agent], parallel_tool_calls=False),
    tools=[book_hotel, transfer_to_flight_agent],
    system_prompt=(
        "당신은 호텔 예약 어시스턴트입니다.\n"
        "- 숙박 관련 요청만 처리하세요.\n"
        "- 호텔명 정보만 있으면 예약이 가능합니다.\n"
        "- 여러 개의 도구를 동시에 호출하지 마세요.\n"
        "- 본인이 해결할 수 없는 작업은 다른 에이전트에게 이관합니다.\n"
        "- 모든 작업이 완료되었으면 작업을 종료합니다."
    ),
    name="hotel_agent",
)


# Define multi-agent graph
swarm_team = (
    StateGraph(MessagesState)
    .add_node(flight_agent)
    .add_node(hotel_agent)
    .add_edge(START, "flight_agent")
    .compile()
)


result = swarm_team.invoke({"messages": [HumanMessage("인천(ICN)→부산(PUS) 항공편과 해운대 파라다이스 호텔을 예약해줘.")]})

for m in result['messages']:
    m.pretty_print()

 

이 구조는 매우 유연하지만, 잘못 설계하면 무한 루프에 빠질 위험이 있다.
따라서 종료 조건과 책임 분리가 매우 중요하다.

 


멀티 에이전트 시스템을 설계하는 과정은 단순히 에이전트를 여러 개 붙이는 문제가 아니다.
결국 모든 문제는 하나로 수렴한다.

“어떤 Context를, 어떤 방식으로, 누구에게 전달할 것인가”

앞에서 살펴본 것처럼
Handoff, Agents-as-Tools, Swarm, Supervisor 구조의 차이는 겉으로 보면 구조의 차이처럼 보이지만, 실제로는 Context를 전달하고 통제하는 방식의 차이에 가깝다.

단일 에이전트는 하나의 Context 안에서 모든 것을 해결하려 하지만, 멀티 에이전트는 Context를 분할하고, 역할을 나누고, 제어 흐름을 설계함으로써 복잡성을 관리한다.