콘텐츠로 이동

Multi-Agent 오케스트레이션

언제 멀티 에이전트가 필요한가?

섹션 제목: “언제 멀티 에이전트가 필요한가?”

단일 에이전트는 컨텍스트 윈도우 한계, 도구 다양성 한계, 단일 실행 스레드 한계를 가집니다. 다음 상황에서 멀티 에이전트가 필요합니다:

  • 태스크가 단일 컨텍스트 윈도우를 초과하는 경우
  • 전문화된 에이전트가 각 도메인을 더 잘 처리하는 경우
  • 독립적 태스크를 병렬로 실행해야 하는 경우
  • 검증을 별도 에이전트가 독립적으로 수행해야 하는 경우

패턴 1: Supervisor (중앙 집중 조율)

섹션 제목: “패턴 1: Supervisor (중앙 집중 조율)”

Supervisor 패턴에서는 중앙 에이전트(Supervisor)가 하위 에이전트들에게 태스크를 배분하고 결과를 통합합니다.

┌─────────────┐
│ Supervisor │ ← 사용자 요청
└──────┬──────┘
┌───────────┼───────────┐
↓ ↓ ↓
┌───────┐ ┌───────┐ ┌───────┐
│Agent A│ │Agent B│ │Agent C│
│(코드) │ │(테스트)│ │(문서) │
└───────┘ └───────┘ └───────┘
└───────────┴───────────┘
↓
결과 통합 및 반환
class SupervisorAgent:
def run(self, goal: str) -> str:
# 태스크 분해
subtasks = self.llm.decompose(goal)
results = {}
for subtask in subtasks:
# 적합한 하위 에이전트 선택
agent = self.route_to_agent(subtask)
results[subtask.id] = agent.run(subtask.description)
# 결과 통합
return self.llm.synthesize(goal, results)
def route_to_agent(self, subtask: Subtask) -> Agent:
routing = {
"code": self.code_agent,
"test": self.test_agent,
"docs": self.docs_agent,
}
return routing[subtask.type]

적합한 상황:

  • 태스크 유형이 명확히 구분되는 경우
  • 중앙 조율이 필요한 복잡한 워크플로우
  • 결과를 통합해야 하는 경우

주의사항: Supervisor가 단일 장애점(SPOF)이 됩니다. Supervisor 자체의 품질이 전체 시스템을 결정합니다.

패턴 2: Peer-to-Peer (직접 핸드오프)

섹션 제목: “패턴 2: Peer-to-Peer (직접 핸드오프)”

P2P 패턴에서는 에이전트들이 서로 직접 핸드오프합니다. 중앙 조율자가 없고 각 에이전트가 다음 에이전트를 결정합니다.

Agent A → (핸드오프) → Agent B → (핸드오프) → Agent C
↑ │
└─────────────── (피드백 루프) ─────────────────┘
@dataclass
class HandoffMessage:
from_agent: str
to_agent: str
content: str
metadata: dict
class PeerAgent:
def run(self, message: HandoffMessage) -> HandoffMessage | None:
result = self.process(message.content)
# 다음 에이전트 결정
next_agent = self.determine_next(result)
if next_agent is None:
return None # 파이프라인 종료
return HandoffMessage(
from_agent=self.name,
to_agent=next_agent,
content=result,
metadata={"previous": message.metadata},
)

적합한 상황:

  • 선형 파이프라인 (A → B → C)
  • 각 단계의 책임이 명확한 경우
  • 에이전트 추가/제거가 자주 필요한 경우

Hierarchical 패턴은 트리 구조로 에이전트를 조직합니다. 상위 에이전트가 하위 에이전트를 관리하고, 하위 에이전트는 다시 더 하위 에이전트를 가질 수 있습니다.

┌──────────────┐
│ Coordinator │ Level 0
└──────┬───────┘
┌─────────┴─────────┐
↓ ↓
┌──────────┐ ┌──────────┐
│ Manager A│ │ Manager B│ Level 1
└────┬─────┘ └────┬─────┘
┌────┴────┐ ┌────┴────┐
↓ ↓ ↓ ↓
Worker1 Worker2 Worker3 Worker4 Level 2

적합한 상황:

  • 대규모 복잡 태스크 (대형 코드베이스 전체 리팩토링)
  • 도메인별 전문화가 필요한 경우
  • 병렬 실행과 조율이 동시에 필요한 경우

| 기준 | Supervisor | Peer-to-Peer | Hierarchical | |---|---|---|---| | 복잡도 | 중간 | 낮음 | 높음 | | 확장성 | 제한적 | 높음 | 매우 높음 | | 디버깅 용이성 | 중간 | 높음 | 낮음 | | 병렬 실행 | 가능 | 제한적 | 매우 용이 | | 단일 장애점 | 있음 | 없음 | 있음 (각 레벨) | | 적합 규모 | 중소형 | 소형 | 대형 |

MCP가 호스트 애플리케이션과 도구·데이터의 연결을 표준화한다면, A2A(Agent-to-Agent) 는 독립적으로 배포된 에이전트가 능력을 알리고, 태스크를 위임하고, 상태와 산출물을 교환하는 계약을 다룬다. 내부 함수 호출을 A2A로 바꿀 필요는 없다. 서로 다른 팀·런타임·신뢰 경계를 넘는 에이전트를 연결할 때 후보가 된다.

사용자 → 오케스트레이터 ── A2A 태스크 위임 ──► 외부 전문 에이전트
│ │
└──── MCP 도구 호출 ────────────────┘

Agent Skills는 에이전트가 특정 업무를 일관되게 수행하도록 만드는 SKILL.md 중심의 재사용 묶음이다. 지침뿐 아니라 스크립트·참조 자료·템플릿을 포함할 수 있다. A2A가 “누구에게 어떤 태스크를 맡길지”의 통신 계약이라면, Skills는 “그 에이전트가 어떤 절차와 자원을 사용할지”의 작업 계약이다. 둘 다 권한·입력·출력·버전을 명시해야 안전하게 재사용할 수 있다.

필요 이상으로 에이전트를 추가하지 마세요. 에이전트가 늘어날수록 오케스트레이션 복잡도가 기하급수적으로 증가합니다.

에이전트 간 통신은 구조화된 형태로 정의하세요.

@dataclass
class AgentMessage:
task_id: str
content: str
context: dict
expected_output_format: str # 출력 형식 명시

각 에이전트는 가능한 한 독립적으로 작동해야 합니다. 공유 상태를 최소화하고, 각 에이전트가 자체 완결적인 컨텍스트를 갖도록 설계하세요.

# 나쁜 패턴: 공유 전역 상태
shared_state = {}
# 좋은 패턴: 메시지 패싱
def agent_b(message_from_a: AgentMessage) -> AgentMessage:
# message_from_a에만 의존, 전역 상태 없음
result = process(message_from_a.content)
return AgentMessage(task_id=message_from_a.task_id, content=result)

Multi-Agent 오케스트레이션은 Supervisor(중앙 조율), P2P(직접 핸드오프), Hierarchical(계층 구조) 세 가지 핵심 패턴으로 구성됩니다. 패턴 선택은 태스크 규모, 병렬화 필요성, 디버깅 용이성을 기준으로 합니다. 조직·런타임 경계를 넘는 위임은 A2A, 반복 가능한 업무 절차는 Agent Skills를 검토할 수 있습니다. 최소 에이전트 원칙과 명확한 인터페이스가 안정적인 시스템의 기반입니다.

Section 02 학습을 완료했습니다. 다음 Section 03에서는 세 축 중 첫 번째인 Context Engineering을 심화 학습합니다.