워크플로 시리즈 (04): 멀티 에이전트 협업 — 오케스트레이터 경계, 동시성 제어 및 컨텍스트 격리
요약
멀티 에이전트 시스템 설계 시 오케스트레이터와 서브에이전트 간의 명확한 책임 경계 설정 방법을 다룹니다. 구조화된 데이터(JSON)를 통한 통신과 입력 완결성, 출력 계약의 엄격성을 통해 시스템의 안정성을 높이는 가이드를 제공합니다.
핵심 포인트
- 오케스트레이터는 결정, 배정, 수집 역할에 집중하고 비즈니스 로직은 서브에이전트에 위임해야 함
- 서브에이전트는 메시지 스트림 대신 구조화된 JSON 파일을 통해 결과를 보고해야 함
- 서브에이전트의 작업 프롬프트는 외부 컨텍스트 없이도 완결성을 갖춰야 함
- 출력 스키마를 엄격히 준수하고 실패 시에도 구조화된 에러를 출력하여 타임아웃과 구분해야 함
오케스트레이터 책임 경계 (Orchestrator Responsibility Boundaries)
오케스트레이터 (Orchestrator, 메인 에이전트)와 서브에이전트 (Subagents) 사이의 불분명한 구분은 멀티 에이전트 워크플로에서 가장 흔히 발생하는 설계 문제입니다.
오케스트레이터는 세 가지 역할을 수행합니다:
1. 결정 (Decide): 상태를 읽고 다음 단계를 결정함
2. 배정 (Dispatch): 서브에이전트를 생성하고 작업 프롬프트 (task prompts)를 전달함
3. 수집 (Collect): 서브에이전트의 출력 파일 (output files)을 읽고 상태를 업데이트함
오케스트레이터는 비즈니스 로직 (버그 분석, 코드 작성, 로그 쿼리)을 실행하거나, 가공되지 않은 파일 (raw files)을 읽거나, 비즈니스 데이터를 수정하지 않습니다. 그러한 작업은 서브에이전트의 몫입니다.
메인 에이전트는 오직 구조화된 결론 (structured conclusions) (JSON)만을 전달받습니다. 서브에이전트는 메시지 스트림 (message streams)이 아닌 output.json을 통해 보고합니다.
# ✅ 올바른 방식: 메인 에이전트가 구조화된 결론을 읽음
result = json.loads(Path("phase3/analysis_final.json").read_text())
if result["confidence"] >= 95:
...
이러한 경계 설정은 두 가지 이점을 제공합니다: 메인 에이전트의 컨텍스트 (context)가 관리 가능한 수준으로 유지되며 (상태와 결론만 포함하고 가공되지 않은 데이터는 제외), 서브에이전트의 비즈니스 로직을 메인 에이전트의 세션 히스토리 (session history) 없이 독립적으로 테스트할 수 있습니다.
서브에이전트 설계 원칙 (Subagent Design Principles)
원칙 1: 입력 완결성 (Input Completeness)
작업 프롬프트 (task prompt)에는 서브에이전트가 작업을 완료하는 데 필요한 모든 정보가 포함되어 있어야 합니다.
# ❌ 불완전한 작업 프롬프트
이 버그의 근본 원인을 분석하세요. 이전 분석 결과를 참조하십시오.
...
"이전 분석 결과를 참조하십시오"라는 문구는 서브에이전트가 메인 에이전트의 컨텍스트 히스토리에 접근할 것을 요구하지만, 격리된 세션 (isolated session) 내에는 해당 정보가 존재하지 않습니다. 각 서브에이전트는 오직 자신의 작업 프롬프트에 포함된 내용만을 알고 있습니다.
원칙 2: 출력 계약의 엄격성 (Output Contract Strictness)
서브에이전트는 선언된 JSON 스키마 (JSON Schema)에 따라 출력 파일을 작성해야 합니다. 메인 에이전트의 라우팅 로직 (routing logic)은 이 스키마에 의존하므로, 필드가 누락되거나 타입이 틀리면 결정 로직이 깨지게 됩니다.
# 서브에이전트 출력 스키마 (templates/에 정의됨)
OUTPUT_SCHEMA = {
"passed": bool, # 필수 — 메인 에이전트 라우팅이 이에 의존함
...
원칙 3: 실패 시 구조화된 에러 출력 (Structured Error Output on Failure)
실패 시, 서브에이전트 (subagent)는 반드시 passed=false가 포함된 출력 파일을 작성해야 합니다.
{
"passed": false,
"error": "Log file not found: /workspace/logs/crash_20260601.log",
...
출력 파일이 누락되면 메인 에이전트 (main Agent)는 이를 타임아웃 (timeout)으로 간주하게 됩니다. 구조화된 에러 출력 (Structured error output)을 사용하면 메인 에이전트가 "서브에이전트 실패"와 "서브에이전트 타임아웃"을 구분하여 서로 다르게 대응할 수 있습니다.
Fan-out / Fan-in 동시성 제어 (Concurrency Control)
Fan-out 설계
Fan-out은 하나의 트리거 지점이 N개의 동시 실행되는 서브에이전트 (subagents)를 생성하는 것을 의미합니다. 여기에는 두 가지 엄격한 제약 조건이 있습니다.
제약 조건 1: 각 서브에이전트는 서로 다른 출력 파일을 작성해야 함
# ✅ 올바른 예: 각 후보(candidate)가 자신의 파일에 작성함
candidates = ["candidate_a", "candidate_b", "candidate_c"]
for c in candidates:
...
제약 조건 2: 메인 에이전트는 모든 서브에이전트가 완료될 때까지 기다려야 함
Fan-out 이후, 메인 에이전트는 대기 상태로 진입하며 다음 단계로 진행하지 않습니다. 비동기 런타임 (async runtime)을 사용할 수 없는 경우에는 폴링 (polling) 방식이 유효합니다.
def wait_all_candidates(candidates: list[str], timeout: int = 300) -> dict:
results = {}
deadline = time.time() + timeout
...
Fan-in: 실패 전략
Fan-in 단계에서 일부 서브에이전트가 실패할 경우, 두 가지 전략을 사용할 수 있습니다.
fail-fast (어떠한 실패라도 발생 시 즉시 중단)
# 용도: 모든 분기 결과가 필수적일 때; 하나라도 실패하면 전체 배치(batch)가 무의미해짐
phase_parallel_analysis:
fan_in_strategy: fail-fast
...
사용 시점: 세 개의 서브에이전트가 각각 서로 다른 소스에서 데이터를 가져오는 경우. 소스 중 하나라도 누락되면 추가 분석이 불가능합니다.
collect-all (실패를 포함하여 모든 것을 수집)
# 용도: 부분적인 성공만으로도 충분할 때; 통과한 결과 중 최적의 것을 선택
phase_4_fix:
fan_in_strategy: collect-all
...
사용 시점: 세 개의 코드 수정 후보(code-fix candidates)가 동시에 실행되는 경우. 하나라도 테스트를 통과하면 충분합니다. 실패한 후보들은 폐기됩니다. 세 개 모두 실패하는 경우에만 인간 검토 단계(human gate)가 트리거됩니다.
선택 원칙
모든 분기 결과가 필요함 → fail-fast
부분적 성공으로 충분함 → collect-all (코드 수정, 후보 생성)
품질을 위해 여러 결과를 비교함 → collect-all (최적안 선택)
버그 수정 (Bug fix) 워크플로의 Phase 4 수정 단계에서는 collect-all 방식을 사용합니다. 세 개의 후보가 동시에 실행되며, 통과한 후보 중 테스트 커버리지 (test coverage)가 가장 높은 것이 선택됩니다. 인간 검토 단계 (human gate)는 세 후보가 모두 실패할 때만 트리거됩니다.
컨텍스트 격리 (Context Isolation)
서브에이전트 (Subagent)는 메인 에이전트 (Main Agent)의 대화 기록에 접근할 수 없는 **격리된 세션 (isolated sessions)**에서 실행되어야 합니다.
메인 에이전트의 컨텍스트 (context)에는 전체 워크플로 이력, 즉 모든 파일 내용, 모든 서브에이전트의 원시 출력 (raw outputs), 모든 중간 결정 사항이 포함되어 있습니다. 하나의 패치 (patch)를 작성하는 서브에이전트에게 이를 그대로 전달하면, 컨텍스트가 수천 토큰에서 수만 토큰으로 급격히 팽창하여 토큰 비용이 두 배로 증가하고, 무관한 이력이 서브에이전트의 집중력을 저하시키게 됩니다.
정보는 두 방향으로 흐릅니다:
메인 에이전트 (Main Agent)
│
│ 작업 프롬프트 (task prompt) (서브에이전트에게 필요한 필드만 포함)
...
서브에이전트는 자신의 작업 프롬프트에 무엇이 들어있는지와 합의된 출력 경로를 알고 있습니다. 하지만 메인 에이전트가 무엇을 했는지, 워크플로가 얼마나 진행되었는지, 또는 다른 서브에이전트들이 무엇을 생성했는지는 알지 못합니다.
만약 서브에이전트가 작업을 완료하기 위해 "배경 지식을 이해"해야 한다면, 해당 작업 프롬프트는 불완전한 것입니다. 그 배경 지식을 명시적으로 포함시키십시오. 서브에이전트가 볼 수 없는 이력에 접근할 수 있을 것이라고 기대해서는 안 됩니다.
설계 체크리스트 (Design Checklist)
오케스트레이터 (Orchestrator)의 책임
- 메인 에이전트는 구조화된 JSON 출력만 읽음 — 원시 로그 (raw logs)나 긴 텍스트는 제외
- 메인 에이전트는 비즈니스 로직 (분석, 코드 작성, 쿼리)을 실행하지 않음
- 라우팅 (Routing) 결정은 대화 기록이 아닌 상태 파일 (state file)과 서브에이전트 출력에 의존함
서브에이전트 설계
- 태스크 프롬프트 (Task prompt)는 서브에이전트가 필요로 하는 모든 필드를 포함함 (암시적인 컨텍스트 의존성 없음)
- 출력 스키마 (Output schema)는
templates/에 선언되어 있으며passed필드를 포함함 - 실패 시에도 서브에이전트는 출력 파일에
{"passed": false, "error": "..."}를 작성함
동시성 제어 (Concurrency control)
- 팬아웃 (Fan-out) 시 각 서브에이전트는 고유한 출력 파일 경로에 기록함
- 팬인 (Fan-in) 전략은 페일 패스트 (fail-fast) 또는 컬렉트 올 (collect-all)로 명시적으로 라벨링됨
- 컬렉트 올 (collect-all)은 정의된
selection_criteria및on_all_failed동작을 가짐
컨텍스트 격리 (Context isolation)
- 서브에이전트는 메인 에이전트 (Agent)의 히스토리에 접근할 수 없는 격리된 세션에서 실행됨
- 서브에이전트가 필요로 하는 모든 배경 정보는 태스크 프롬프트 (task prompt)에 명시적으로 포함됨
요약 (Summary)
- 오케스트레이터 (Orchestrator)는 결정하고 배정할 뿐임: JSON 결론을 읽고, 서브에이전트를 생성하며, JSON 결론을 수집함 — 핵심 목표는 "똑똑해지는 것"이 아니라 컨텍스트 (context)를 관리 가능한 수준으로 유지하는 것임
- 팬인 (Fan-in) 전략이 워크플로의 회복 탄력성을 결정함: 솔루션 공간 문제 (예: 코드 수정)에는 컬렉트 올 (collect-all)을 사용하고, 전부 아니면 전무인 문제 (예: 데이터 수집)에는 페일 패스트 (fail-fast)를 사용함 — 이를 잘못 설정하면 단일 실패로 인해 차단되거나 불가능한 작업에 시간을 낭비하게 됨
- 컨텍스트 격리 (Context isolation)는 품질 보증임: 추가적인 컨텍스트는 도움이 아니라 노이즈임; 만약 서브에이전트가 업무 수행을 위해 배경 지식이 필요하다면, 그 배경 지식은 태스크 프롬프트 (task prompt)에 명시적으로 포함되어야 함
실제 엔터프라이즈급 워크플로에서 검증된 AI 에이전트 및 기술의 큐레이션 마켓플레이스인 PrimeSkills를 확인해 보세요. 군더더기 없이 실제로 작동하는 것들만 제공합니다.
제 홈페이지에서 더 유용한 지식과 흥미로운 제품들을 찾아보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기