
멀티 에이전트 조정: 에이전트의 안정성을 유지하는 메시지 버스 (Message-Bus) 패턴
요약
멀티 에이전트 시스템 설계 시 발생하는 핸드오프(Handoff) 방식과 공유 버스(Shared Bus) 패턴의 차이점을 분석합니다. 워크로드 특성에 따라 Anthropic의 오케스트레이터 방식과 Cognition의 컨텍스트 공유 방식의 장단점을 비교하며 안정적인 시스템 구조를 제안합니다.
핵심 포인트
- 워크로드 특성(병렬성 vs 의존성)에 따라 적합한 에이전트 구조가 다름
- 핸드오프 방식은 컨텍스트 누적 비용과 부모 정보 상실 위험이 있음
- 공유 버스 패턴은 관리자가 책임을 유지하며 타입화된 작업을 처리함
- 시스템 설계 단계에서의 통신 방식 선택이 실패 모드를 결정함
- 도서: Agents in Production — Building, Tracing, and Shipping Multi-Step AI You Can Trust
- 저자의 다른 저서: Observability for LLM Applications — The AI Engineer's Library (2권 시리즈)의 동반 도서
- 내 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구를 사용하는 개발자를 위한 IDE
- 자기소개: xgabriel.com | GitHub
2025년 6월, 두 엔지니어링 팀이 같은 주에 서로 상반된 조언을 게시했습니다.
Anthropic은 How we built our multi-agent research system을 공개했습니다. 이는 워커 에이전트 (worker agents)에게 하위 작업 (subtasks)을 배정하는 오케스트레이터 (orchestrator) 방식으로, 너비 우선 탐색 (breadth-first) 연구에서 단일 에이전트보다 90.2% 더 높은 성능을 보였습니다. 며칠 후, Devin을 만든 팀인 Cognition은 Don't Build Multi-Agents를 발표하며, 공유된 컨텍스트 (shared context)가 없는 병렬 하위 에이전트 (subagents)는 취약한 시스템을 만든다고 주장했습니다.
두 팀 모두 옳았습니다. 그들은 서로 다른 워크로드 (workloads)를 설명하고 있었기 때문입니다. Anthropic의 연구 에이전트는 매우 병렬적 (embarrassingly parallel)입니다. 네 명의 워커가 네 가지를 읽고 네 개의 작은 요약본을 가지고 돌아오는 방식입니다. 반면 Cognition의 목표는 코드 작성이며, 여기서는 모든 수정 사항이 다른 모든 수정 사항에 의존하며 컨텍스트 (context)를 조각낼 수 없습니다.
대부분의 사람들은 결정 자체가 아니라 배관 (plumbing, 시스템 구조)을 잘못 설계합니다. 조정이 필요한 두 에이전트가 생기면, 그들이 어떻게 대화할지를 선택해야 합니다. 그 선택은 모델이 문제를 일으키기 훨씬 전부터 실패 모드 (failure modes)를 결정합니다.
핸드오프 (Handoffs) vs 공유 버스 (shared bus)
에이전트들을 연결하는 데는 두 가지 방법이 있으며, 각각 실패하는 방식이 다릅니다.
**핸드오프 (handoff)**는 제어권을 전달합니다. 에이전트 A가 작업을 마치고 대화 전체를 에이전트 B에게 넘긴 뒤 물러납니다. 이는 데모에서는 잘 작동하는 것처럼 보입니다. 하지만 프로덕션 환경에서는 매 단계마다 트랜스크립트 (transcript)가 길어지며, 네 번째 에이전트에 도달하면 아무도 정리하지 않은 대화 내용을 다시 읽는 데 비용을 지불하게 됩니다. 또한 핸드오프는 부모 (parent) 정보를 잃어버립니다. A가 B에게 핸드오프를 하고 나면, 최종 답변을 원래의 작업과 대조하여 확인할 수 있는 주체가 아무도 남지 않게 됩니다.
**공유 버스 (shared bus)**는 관리자 (supervisor)가 책임을 유지합니다. 작업자 (worker)들은 서로 대화하지 않습니다. 이들은 작고 타입이 지정된 작업 (typed task)을 전달받아 업무를 수행하고, 결과물을 구성하는 관리자에게 작고 타입이 지정된 아티팩트 (artifact)를 반환합니다. 이것이 Anthropic 연구 시스템의 형태이며, 여러분이 지향해야 할 기본값입니다.
판단 기준은 단 하나입니다. 작업자가 방금 형제(sibling) 작업자가 생성한 내용을 볼 필요가 있는가? 만약 그렇다면, 여러분은 병렬 작업자 (parallel workers)를 가진 것이 아닙니다. 팀인 척하는 순차적 파이프라인 (sequential pipeline)을 가진 것이며, 이를 자유로운 핸드오프가 아닌 하나의 순서 있는 체인 (ordered chain)으로 실행해야 합니다.
다음은 순수 Anthropic SDK에서의 버스 형태입니다. 세 명의 작업자가 팬아웃 (fan-out) 되고, 관리자가 이를 병합 (merge) 합니다. 어떤 작업자도 다른 작업자에게 직접 접근하지 않습니다.
import asyncio
from anthropic import AsyncAnthropic
...
세 가지 요소가 그 자리를 차지합니다. asyncio.gather는 팬아웃 (fan-out) 역할을 수행하므로, 실제 소요 시간 (wall-clock time)은 세 명이 아닌 한 명의 작업자 시간과 같습니다. return_exceptions=True는 불안정한 작업자 하나가 전체 실행을 중단시키는 것을 방지합니다. 그리고 WORKERS 내의 그 어떤 것도 다른 작업자의 출력을 읽지 않습니다. 역할들은 오직 원래의 작업만을 공유합니다. 이 마지막 속성이 바로 이 패턴을 병렬화하기에 안전하게 만드는 핵심입니다.
모든 메시지에 봉투 (envelope)를 입히세요
작업자들이 서로 다른 머신에서 실행되거나 충돌 (crash) 후에도 생존해야 하는 순간, 공유 딕셔너리 (shared dictionary)는 더 이상 작동하지 않으며 메시지 버스 (message bus)가 필요해집니다. 그리고 이 버스를 통과하는 모든 메시지는 단순한 페이로드 (payload)가 아니라, 동일하고 고정된 봉투 (envelope)를 지니고 있어야 합니다.
{
"task_id": "t_8f2c",
"parent_task_id": "t_7a1b",
...
엔벨로프 (Envelope)는 메시지를 OpenTelemetry 트레이스 (trace)와 다시 연결하고, 잘못된 결과의 책임이 누구에게 있는지 알려주며, (사람들이 흔히 건너뛰는 부분이지만) 예산을 전달하는 역할을 합니다. cost_so_far_usd와 step_budget_remaining은 작업과 함께 이동합니다. 자식 에이전트가 예산을 초과한 것을 확인한 감독자 (supervisor)는 다음 메시지 전달을 거부할 수 있으며, 자식의 워커 (worker)들은 엔벨로프가 도착하지 않으면 종료됩니다. 공유된 엔벨로프가 없다면, 감독자는 1분 전에 보낸 작업을 취소할 방법이 없습니다.
한 곳에서 이를 강제하십시오. 워커 코드는 토픽 (topic)에 가공되지 않은 바이트 (raw bytes)를 직접 발행해서는 안 됩니다.
def publish(bus, topic, payload, *, ctx):
if ctx.steps_left <= 0:
raise StepBudgetExhausted(ctx.task_id)
...
이 두 개의 가드 절 (guard clauses)이 핵심입니다. 예산 강제 기능이 없는 버스 (bus)는 단일 에이전트의 폭주 루프 (runaway loop)가 전체 플릿 (fleet) 전체의 폭주 루프로 번지게 만드는 원인이 됩니다.
컨텍스트 덤프 (context dump)를 피하라
멀티 에이전트 시스템이 비용이 많이 들게 되는 가장 흔한 원인은 버그가 아니라 습관입니다. 바로 모든 에이전트에게 전체 대화 내용을 전달하는 것이 편하기 때문입니다.
모두가 모든 것을 읽는 피어 채팅 (Peer chat) 방식은 토큰 비용 측면에서 $O(n^2)$의 비용이 발생합니다. 더 심각한 것은 오류를 증폭시킨다는 점입니다. 한 에이전트의 실수가 다음 에이전트의 전제가 되고, 에이전트들이 유사한 데이터로 학습되었기 때문에 그룹 전체가 함께 경로를 이탈하게 됩니다. Galileo는 범위 설정이 제대로 되지 않은 멀티 에이전트 실행에서 이를 17배의 오류 증폭 효과 (error-amplification effect)로 측정했습니다.
해결책은 경계면에서의 스키마 (schema)입니다. 워커는 산문 (prose)이 아닌 타입이 지정된 아티팩트 (artifact)를 반환해야 합니다. 이것이 버스로 돌아가기 전에 검증하고, 규격에 맞지 않는 것은 거부하십시오.
from pydantic import BaseModel, ValidationError
class RiskReport(BaseModel):
...
5개의 리스크와 심각도를 반환해야 하는 워커는 4,000 토큰 분량의 대화 기록을 감독자의 컨텍스트 (context)에 몰래 끼워 넣을 수 없습니다. 타입 (type) 자체가 압축입니다. 만약 출력이 그것을 생성한 프롬프트 (prompt)보다 길다면, 그것은 병렬성 (parallelism)의 탈을 쓴 압축 문제 (compression problem)입니다.
TypeScript에서는 동일한 가드 (guard)가 경계면에서의 Zod 파싱 (parse)으로 구현됩니다:
import { z } from "zod";
const RiskReport = z.object({
...
무한 대화 루프(infinite chatter loop) 차단하기
사람들을 가장 두렵게 만드는 실패 사례는 두 에이전트가 영원히 대화를 나누는 것입니다. 에이전트 A는 B의 보고서를 기다리고, B는 A의 승인 없이는 작업을 마칠 수 없습니다. 어느 쪽도 자신이 기다리고 있는 대상이 차단(blocking)되고 있다는 사실을 인지하지 못합니다. 양쪽 모두 계속해서 토큰을 소모합니다. 이는 제어 불능의 재귀(runaway recursion)가 멀티 에이전트 버전으로 나타난 것이며, 완화 방법은 앞서 언급한 세 가지 습관과 동일합니다.
첫째, 모든 궤적(trajectory)에 단계 카운터(step counter)를 설정하고 버스(bus)에서 이를 확인하여, 데드락(deadlock) 비용이 커지기 전에 실행을 중단해야 합니다. 이것이 step_budget_remaining 필드가 제 역할을 하는 방식입니다.
둘째, 첫 번째 메시지를 보내기 전에 종료 조건(termination condition)을 작성해야 합니다. 어떤 피어(peer) 또는 토론(debate) 설정이든, 중단 규칙을 사전에 결정하십시오:
async def debate(task, max_rounds=3):
transcript = []
for _ in range(max_rounds):
...
"3라운드 진행 후 판사가 결정한다"라고 정해야 합니다. "합의될 때까지 계속한다"가 아닙니다. 모델들은 합의하기를 좋아합니다. 개방형 루프(open loop)에 있는 두 에이전트는 거의 항상 수렴(converge)하며, 그들이 수렴하는 결과는 거의 항상 같은 방향으로 틀린 결과입니다. 턴 제한(turn cap)은 단순한 안전망이 아니라 설계 그 자체입니다.
셋째, 모든 await에 타임아웃(timeout)을 설정하십시오. 작업 도중 중단된 에이전트가 형제 에이전트를 영원히 차단된 상태로 방치해서는 안 됩니다. 단계 예산(step budget)을 실제 시간 기준의 마감 시간(wall-clock deadline)과 결합하면, 데드락은 끝을 알 수 없는 청구서가 아니라 제한적이고 재시도 가능한 실패(bounded, retryable failure)가 됩니다.
반드시 지켜야 할 단 하나의 규칙
만약 워커(worker)가 형제 에이전트가 생성한 결과물을 확인해야 한다면, 병렬 팀을 구성하지 마십시오. 워커를 격리된 상태로 유지하고, 작고 타입이 지정된 작업(typed tasks)을 전달하며, 작고 타입이 지정된 산출물(typed artifacts)을 반환하게 하고, 예산(budget)을 봉투에 담아 전달하십시오. 그렇게 하면 관리자-워커(supervisor-worker) 구조를 저렴하고, 추적 가능하며, 디버깅 가능한 상태로 유지할 수 있습니다. 이를 생략하면 동일한 패턴이 청구서가 날아올 때까지 자기들끼리 논쟁하는 단체 채팅방으로 변질될 것입니다.
만약 이 내용의 전체 버전 — 네 가지 조정 형태(coordination shapes), Redis Streams, Kafka, NATS에 걸친 메시지 버스(message-bus) 선택 가이드, 그리고 두 에이전트가 동일한 행을 손상시키지 않도록 방지하는 분산 상태(distributed-state) 패턴 — 을 원하신다면, 그것이 바로 _Agents in Production_이 존재하는 이유입니다. 이 책의 동반서인 _Observability for LLM Applications_는 트레이싱(tracing)과 비용 산정(cost-accounting) 측면을 다룹니다. 즉, 공유된 trace_id를 어떻게 하면 위에서 아래로 읽을 수 있는 하나의 궤적(trajectory)으로 나타낼 수 있는지에 대해 설명합니다. 이 두 권은 _The AI Engineer's Library_를 구성합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기