멀티 에이전트 DAG 오케스트레이션: 병렬 실행과 실패 격리 전략
단순 체인 구조를 벗어나 DAG(방향 비순환 그래프) 기반으로 에이전트를 병렬 실행할 때 발생하는 부분 실패 처리, 비용 폭발, 데드락 문제를 실제 패턴으로 해결한다.
왜 선형 체인이 아닌 DAG인가
단일 오케스트레이터가 서브에이전트를 순차 호출하면 전체 지연이 각 단계의 합이 된다. 3개 독립 에이전트를 직렬로 실행하면 평균 12~18초가 걸리지만, DAG로 병렬화하면 가장 긴 단계(보통 6~8초)로 수렴한다. 그러나 병렬화는 부분 실패 격리와 비용 상한 제어라는 새로운 복잡도를 가져온다.
핵심 패턴: Fan-out → Barrier → Fan-in
import asyncio
import anthropic
from dataclasses import dataclass
from typing import Any
@dataclass
class AgentResult:
agent_id: str
success: bool
output: Any
input_tokens: int
output_tokens: int
async def run_agent(client: anthropic.AsyncAnthropic, agent_id: str, prompt: str) -> AgentResult:
try:
response = await client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return AgentResult(
agent_id=agent_id,
success=True,
output=response.content[0].text,
input_tokens=response.usage.input_tokens,
output_tokens=response.usage.output_tokens,
)
except anthropic.APIError as e:
return AgentResult(agent_id=agent_id, success=False, output=str(e), input_tokens=0, output_tokens=0)
async def dag_orchestrate(tasks: dict[str, str], fail_fast: bool = False) -> list[AgentResult]:
client = anthropic.AsyncAnthropic()
# Fan-out: 모든 독립 노드를 동시에 실행
coros = [run_agent(client, aid, prompt) for aid, prompt in tasks.items()]
# Barrier: 전체 또는 first-failure 대기
if fail_fast:
results = await asyncio.gather(*coros, return_exceptions=False)
else:
results = await asyncio.gather(*coros, return_exceptions=True)
# Fan-in: 실패 분리 후 성공 결과만 집계
return [r if isinstance(r, AgentResult) else AgentResult(
agent_id="unknown", success=False, output=str(r), input_tokens=0, output_tokens=0
) for r in results]
fail_fast=False로 설정하면 하나의 에이전트 실패가 전체를 중단시키지 않는다. Fan-in 단계에서 실패 노드만 재시도 큐에 넣어 비용을 최소화할 수 있다.
트레이드오프와 실패 모드
비용 폭발 위험: 병렬 실행은 동시에 여러 API 호출을 발생시켜 단기 토큰 소비가 급등한다. 10개 에이전트를 동시에 실행하면 분당 토큰 한도(TPM)를 초과할 수 있다. asyncio.Semaphore(max_concurrent=5)로 동시 실행 수를 제한하고, 각 AgentResult의 토큰 합계를 누적하여 실행 전 예산 상한을 체크해야 한다. 일반적으로 병렬 에이전트 수는 4~6개가 비용 대비 속도 최적점이다.
데드락: 에이전트 A가 에이전트 B의 출력을 기다리고 B도 A를 기다리는 순환 의존이 생기면 asyncio는 영원히 대기한다. DAG를 구성할 때 위상 정렬(topological sort)로 순환 없음을 사전 검증하고, 개별 에이전트에 asyncio.wait_for(coro, timeout=30.0)으로 타임아웃을 강제해야 한다.
운영 체크리스트
- [ ] 각 에이전트 호출에 고유
trace_id부여 후 로그에 기록 - [ ] 전체 DAG 실행당 총 토큰 예산 상한 설정 (예: 50,000 토큰)
- [ ] 실패 노드 재시도 횟수 최대 2회, 지수 백오프 1s→4s
- [ ] Fan-in 결과에서 성공률 < 70%이면 오케스트레이터 레벨 알림 발송
- [ ] 위상 정렬 검증 로직을 DAG 빌드 타임에 실행 (런타임 아님)