콘텐츠로 이동

Orchestrator-Worker 프로덕션 시스템

단일 에이전트의 한계를 넘어서

섹션 제목: “단일 에이전트의 한계를 넘어서”

4-4 챕터에서 다룬 오케스트레이터-워커 패턴은 개념적으로 단순하다. 하지만 실제로 이 구조를 프로덕션에서 운영하는 것은 다른 문제다. Anthropic의 멀티에이전트 리서치 시스템은 이 구조를 실전에 적용한 사례 중 가장 구체적으로 공개된 시스템이다.

핵심 통계부터 살펴보자. 단일 Claude Opus 에이전트와 동일한 연구 작업을 멀티에이전트 시스템으로 수행했을 때, 내부 평가에서 성능이 90.2% 향상됐다. 이 시스템의 토큰 사용량은 단일 채팅 대비 약 15배에 달한다. 그리고 성능 분산의 80%가 토큰 사용량으로 설명된다. 즉, 더 많은 토큰을 쓸수록 더 좋은 결과가 나온다는 상관관계가 강하게 나타났다.

┌─────────────────────────────────────────────────────────────────┐
│ Anthropic 멀티에이전트 리서치 시스템 구조 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 오케스트레이터 (Opus 계열) │ │
│ │ • 연구 질문 분해 │ │
│ │ • 서브태스크 우선순위 결정 │ │
│ │ • 워커 결과 종합 및 품질 판단 │ │
│ │ • 추가 조사 필요 여부 결정 │ │
│ └─────────┬───────────────────────────────────────┘ │
│ │ 태스크 스펙 전달 │
│ ┌─────────▼──────────────────────────────────────┐ │
│ │ 워커 풀 (Sonnet 계열) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 워커 1 │ │ 워커 2 │ │ 워커 3 │ ... │ │
│ │ │(검색) │ │(분석) │ │(요약) │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ 각 워커는 독립 컨텍스트 윈도우 보유 │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

오케스트레이터는 Claude Opus 계열 모델이 담당한다. 전체 연구 목표를 이해하고 서브태스크로 분해하며, 워커가 반환한 결과를 종합해 전체적인 품질을 판단한다. 어느 부분이 아직 불충분한지, 추가 조사가 필요한지를 결정하는 메타 추론이 핵심 역할이다.

워커는 Claude Sonnet 계열 모델이 담당한다. 각 워커는 오케스트레이터로부터 구체적인 태스크 스펙을 받아 독립적으로 실행한다. 중요한 것은 각 워커가 별도의 컨텍스트 윈도우를 가진다는 점이다. 워커들은 서로의 컨텍스트를 공유하지 않는다. 이것이 단일 에이전트의 컨텍스트 한계를 우회하는 핵심 메커니즘이다.

오케스트레이터가 워커에게 전달하는 태스크 스펙(task spec)에는 네 가지 요소가 포함돼야 한다.

1. 명확한 목표(goal): 워커가 무엇을 달성해야 하는지 한 문장으로 명확히 기술한다. 모호한 목표는 워커의 루프가 방향을 잃게 만든다.

2. 사용 가능한 도구(tools available): 워커가 접근할 수 있는 도구 목록을 명시한다. 불필요한 도구를 주지 않는 것이 중요하다. 도구가 많을수록 워커가 혼란스러운 선택을 할 가능성이 높아진다.

3. 예상 출력 형식(expected output format): 오케스트레이터가 워커의 결과를 파싱하기 위한 스키마를 지정한다. 비정형 자연어보다 구조화된 JSON 출력이 오케스트레이터의 종합 작업을 훨씬 쉽게 만든다.

4. 종료 조건(termination condition): 워커 루프가 언제 끝나야 하는지 명시한다. “질문에 답하는 소스를 3개 이상 찾으면 종료”, “최대 10번 검색 후 종료” 같은 구체적인 조건이 있어야 한다.

개념 이해용 의사 코드이며 실제 API와 다를 수 있습니다.

from dataclasses import dataclass
@dataclass
class TaskSpec:
goal: str # 달성해야 할 목표
tools: list[str] # 사용 가능한 도구 이름 목록
output_schema: dict # 기대하는 출력 JSON 스키마
termination: str # 종료 조건 설명
context: str = "" # 필요한 배경 정보
def create_research_task(
question: str,
subtopic: str,
) -> TaskSpec:
return TaskSpec(
goal=f"'{subtopic}'에 관한 다음 질문에 답하기 위한 정보를 수집하라: {question}",
tools=["web_search", "read_url", "save_note"],
output_schema={
"findings": [{"source": "str", "summary": "str", "relevance": "high|medium|low"}],
"confidence": "high|medium|low",
"gaps": ["str"], # 아직 불확실한 부분
},
termination="관련성이 'high'인 소스 3개 이상 찾거나, 검색 10회 이후 중단",
context=f"전체 연구 맥락: 이 서브태스크는 '{subtopic}' 분석의 일부임",
)

멀티에이전트 시스템의 이론적 이점은 병렬 실행이다. 10개의 서브태스크를 10개의 워커가 동시에 처리하면 순차 실행 대비 10배 빠를 수 있다. 하지만 실제로는 **동기 병목(synchronous bottleneck)**이 자주 발생한다.

동기 병목은 다음 상황에서 발생한다.

  • 워커 B의 작업이 워커 A의 결과에 의존할 때
  • 오케스트레이터가 모든 워커의 완료를 기다린 뒤에야 다음 라운드를 시작할 때
  • 공유 자원(파일, 데이터베이스)에 동시 쓰기 경합이 생길 때

이를 완화하려면 **의존성 그래프(dependency graph)**를 명시적으로 관리해야 한다. 서로 독립적인 서브태스크는 병렬로 시작하고, 의존 관계가 있는 서브태스크는 순서를 지켜 시작한다. LLMCompiler(3-6 챕터)가 이 패턴의 정형화된 구현이다.

프로덕션에서 워커가 실패하는 것은 예외가 아닌 일반적인 상황이다. 워커 루프 안에서 예외가 발생하거나, 태스크 스펙과 다른 형식의 결과를 반환하거나, 최대 이터레이션에 도달해 종료될 수 있다.

오케스트레이터는 각 워커의 결과를 수집할 때 세 가지 상태를 처리해야 한다.

워커 상태 오케스트레이터 대응
성공 (유효한 출력) 결과를 종합 풀에 추가
부분 성공 (낮은 신뢰도) 결과를 낮은 가중치로 포함, 보완 태스크 생성
실패 (오류 또는 무효 출력) 태스크를 재설계해 재시도 또는 폐기

다음 챕터에서는 이 구조와 OpenAI Agents SDK의 핸드오프(handoff) 모델을 대비해, 두 협업 패턴이 어떻게 다른지 살펴본다.

참고 자료