Alejandro Rioja.
AI Agents Operations

멀티 에이전트 오케스트레이션 패턴: 큐, 상태, 핸드오프

Alejandro Rioja
Alejandro Rioja
5 분 읽기
TL;DR

신뢰할 수 있는 멀티 에이전트 시스템은 영리한 프롬프트에 달린 게 아니라 지루한 분산 시스템 규율에 달려 있다. 에이전트 사이의 내구성 있는 큐, 모델 밖에 보관하는 상태, 재시도에도 견디는 멱등한 핸드오프. 모델은 워커, 큐는 등뼈다.

무료 뉴스레터

매주 수요일. 28,400명+ 구독자. 핵심만.

목차

2026년 6월 업데이트.

TL;DR: 신뢰할 수 있는 멀티 에이전트 시스템은 영리한 프롬프트로 얻는 게 아니라 지루한 분산 시스템 규율로 얻는다. 에이전트 사이에 내구성 있는 를 두고, 상태를 모델 밖에 보관하며, 재시도가 이중으로 동작하지 않도록 모든 핸드오프를 멱등하게 만들어라. 모델은 워커, 큐는 등뼈다. 이 세 가지를 제대로 잡으면 오케스트레이션은 더 이상 두렵지 않다.

운영자의 시각: 내 100개가 넘는 에이전트 대부분은 단일 단계다. 그렇지 않은 것들 — 분류한 뒤 보강하고 그다음 행동하는 파이프라인 — 은 “프롬프트 체인”으로 생각하기를 멈추고 “LLM 워커를 가진 작업 큐”로 생각하기 시작했을 때 비로소 신뢰할 수 있게 되었다. 이건 프롬프트 엔지니어링이 아니라 아키텍처다.

“멀티 에이전트”는 에이전트들이 서로 대화하는 것처럼 들린다. 실제로 신뢰할 수 있는 버전은 그 반대다. 에이전트는 전혀 직접 통신하지 않는다. 큐에 메시지를 떨어뜨리고 큐에서 작업을 집어 들며, 오케스트레이션은 그들 사이의 배관에 깃든다. 프로덕션에서 버티는 패턴들을 소개한다.

패턴 1: 모든 에이전트 사이에 내구성 있는 큐를 두라

첫 본능은 에이전트 A 안에서 에이전트 B를 직접 호출하는 것이다. 그러지 마라. 직접 호출은 둘을 결합한다. B가 느리면 A가 막히고, B가 실패하면 A의 작업이 사라지며, B를 확장하려 해도 A를 건드리지 않고는 할 수 없다.

대신 A는 자기 작업을 끝내고 B를 위해 메시지를 큐에 넣는다. B는 별도의 워커로, 자기 속도로 큐를 비운다.

typescript
// 에이전트 A가 끝나고 큐를 통해 핸드오프한다 — B를 직접 호출하지 않음
await env.ENRICH_QUEUE.send({
  traceId,
  type: "enrich",
  payload: classifierResult,
});
// A의 작업은 완료. B가 독립적으로 이것을 집어 든다.

Cloudflare에서는 바로 이 목적으로 Workers Queues를 쓴다 — 내가 사용하는 에이전트 스택 뒤에 있는 것과 같은 프리미티브다. 큐는 네 가지를 공짜로 준다. 버퍼링(B가 다운돼도 작업을 잃지 않음), 재시도(실패한 메시지는 재전달됨), 백프레셔(급증분이 크래시 대신 큐에 쌓임), 디커플링(A를 건드리지 않고 B를 확장하거나 재배포). 이들 하나하나가 그렇지 않으면 직접 만들고 틀려야 하는 것들이다.

패턴 2: 상태는 항상 모델 밖에 보관하라

가장 흔한 멀티 에이전트 버그는 모델이 단계 사이에 무언가를 기억한다고 가정하는 것이다. 기억하지 않는다. 모든 모델 호출은 상태가 없으며, 유일한 기억은 프롬프트에 넣은 것뿐이다. 그러므로 “이 작업이 파이프라인의 어디에 있는가”의 진실의 원천은 대화가 아니라 데이터베이스에 깃들어야 한다.

나는 모든 에이전트가 읽고 갱신하는 단일 작업 레코드를 유지한다.

typescript
interface JobState {
  traceId: string;
  stage: "classified" | "enriched" | "acted" | "done" | "failed";
  data: Record<string, unknown>;
  attempts: number;
  updatedAt: number;
}

각 에이전트는 같은 루프를 돈다. 작업 상태를 읽고, 자기 일을 하고, 새 상태를 쓰고, 다음 단계를 큐에 넣는다. 모델은 결코 상태를 쥐지 않는다 — 관련된 조각을 입력으로 받아 결과를 반환한다. 이것이 시스템을 재시작 가능하게 만든다. 워커가 작업 도중에 죽어도 상태 레코드는 여전히 상황이 어디까지였는지 정확히 말해주고, 재전달된 큐 메시지가 거기서부터 이어받는다. 디버깅도 다루기 쉬워진다. 상태 테이블이 모든 작업의 여정을 질의 가능한 기록으로 남기기 때문이다 — 에이전트가 실제로 작동하는지 어떻게 측정하는가와 같은 계측 마인드셋이다.

패턴 3: 모든 핸드오프를 멱등하게 만들라

큐는 최소 한 번 전달을 보장하지, 정확히 한 번이 아니다. 즉 메시지는 두 번 전달될 수 있다 — 네트워크 끊김, 재시도, 재배포. 에이전트의 액션이 멱등하지 않으면 이중 전달은 이중으로 동작한다. 확인 이메일 두 통, 예약 두 건, 결제 두 번. 이것은 가장 고약한 부류의 오케스트레이션 버그이며, 팀들이 프로덕션에서 발견하는 것이다.

해결책은 키를 써서 액션을 멱등하게 만드는 것이다.

typescript
async function handleEnrich(msg: QueueMessage, env: Env) {
  const job = await getJob(env, msg.traceId);
  if (job.stage !== "classified") {
    // 이미 이 단계를 지나 처리됨 — 중복 전달이다. 건너뛴다.
    return;
  }
  const result = await enrich(job.data);
  await advanceJob(env, msg.traceId, "enriched", result);
  await env.ACT_QUEUE.send({ traceId: msg.traceId, type: "act" });
}

단계 검사는 연산을 두 번 실행해도 안전하게 만든다. 두 번째 전달은 작업이 이미 진행됐음을 보고 아무 일도 하지 않는다. 외부 부작용(이메일 발송, 카드 결제)에 대해서는 다운스트림 API에 멱등성 키를 넘겨 그쪽에서도 중복을 제거하게 하라. 모든 메시지가 두 번 전달된다고 가정하고 그것이 무해하도록 설계하라 — 결국 그렇게 될 테니까.

패턴 4: 오케스트레이터 대 코레오그래피 — 의도적으로 선택하라

흐름을 배선하는 방법은 두 가지이고, 올바른 선택은 복잡도에 달려 있다.

코레오그래피(내 기본값): 각 에이전트는 다음 단계만 알고 그것을 큐에 넣는다. 흐름은 체인에서 창발한다. 단순하고 탈중앙적이며 확장하기 쉽다 — 큐를 삽입해 단계를 추가한다. 단점은 전체 흐름을 기술하는 단일한 곳이 없어서, 복잡한 파이프라인은 추론하기 어려워질 수 있다는 것이다.

오케스트레이션(중앙 코디네이터): 하나의 오케스트레이터가 흐름을 소유하고, 각 에이전트를 차례로 호출하며, 결과에 따라 다음을 결정한다. 전체 흐름이 읽기 쉬운 한 곳에 깃들고 분기 로직이 명시적이다. 대가는 그 자체로 내구성이 있어야 하는 중앙 컴포넌트다 — 오케스트레이터 자신의 상태가 외부화되지 않으면(패턴 2) 단일 장애점이 된다.

내 규칙: 분기가 복잡해질 때까지는 코레오그래피, 그다음에는 내구성 있는 오케스트레이터. 선형 3단계 파이프라인은 코레오그래피다. 조건부 라우팅, 병렬 팬아웃, 조인이 있는 흐름은 크래시 후 재개할 수 있도록 상태가 데이터베이스에 깃든 오케스트레이터를 원한다.

패턴 5: 조각을 잃지 않는 팬아웃, 팬인

한 작업이 N개의 병렬 하위 작업을 낳고(레코드 50개 보강, 문서 20개 요약) 계속하기 전에 그 모두를 기다려야 할 때, 조인이 필요하다. 비결은 작업 상태의 카운터다.

  1. 부모가 N개의 자식 메시지를 큐에 넣고 작업 레코드에 expected: N, completed: 0 을 쓴다.
  2. 각 자식이 자기 일을 하고 completed원자적으로 증가시킨다.
  3. completedexpected 와 같아지게 올린 자식이 다음 단계를 큐에 넣는다.

이 원자적 증가가 핵심이다 — 그것이 없으면 동시에 끝나는 두 자식이 둘 다 자신이 마지막이 아니라고 여겨 조인이 결코 발화하지 않는다. 데이터스토어가 원자적으로 증가시킬 수 있는 카운터, 또는 트랜잭션을 쓰라. 이 패턴은 파이프라인의 비싼 중간부를 병렬화하면서(흔히 Haiku로 저렴하게 처리되는 작업 — Haiku 대 Sonnet 비용 계산 참조) 끝에서 깔끔한 조인을 유지하게 해준다.

내가 생략하는 것

이 중 무엇을 하든 무거운 에이전트 프레임워크는 필요 없다. 큐, 상태 테이블, 멱등성 키는 모든 플랫폼에 이미 있는 프리미티브다. 큐가 공짜로 주는 기능을 얻으려고 정교한 멀티 에이전트 프레임워크에 손을 뻗었다가, 대체한 배관보다 디버깅하기 어려운 블랙박스를 떠안는 팀들을 봐왔다. 지루한 프리미티브부터 시작하라. 프레임워크에 손을 뻗는 것은 그것이 해결하는 구체적인 고통을 느꼈을 때만으로 하라.

요약: 에이전트는 상태 없는 워커, 큐는 내구성 있는 등뼈, 상태는 데이터베이스에 깃들고, 모든 핸드오프는 두 번 실행해도 안전하다. 이것이 게임의 전부다.

자주 묻는 질문

에이전트는 서로를 직접 호출해야 할까, 아니면 큐를 거쳐야 할까?

큐를 거쳐야 한다. 직접 호출은 에이전트를 결합한다 — 한쪽의 실패나 느림이 다른 쪽으로 전파되고, 독립적으로 확장하거나 재배포할 수 없다. 내구성 있는 큐는 버퍼링, 재시도, 백프레셔, 디커플링을 공짜로 준다.

멀티 에이전트 상태는 어디에 깃들어야 할까?

모델 밖, 데이터베이스 안에, 각 에이전트가 읽고 갱신하는 작업 레코드로. 모델 호출은 상태가 없으므로 파이프라인 진행의 진실의 원천은 외부여야 한다 — 그것이 크래시 후 시스템을 재시작 가능하게 만든다.

같은 작업에 대해 에이전트가 두 번 동작하는 것을 어떻게 막을까?

핸드오프를 멱등하게 만들라. 행동하기 전에 작업의 단계를 확인하고 이미 진행됐으면 아무것도 하지 말며, 외부 API에 멱등성 키를 넘겨라. 큐는 최소 한 번 전달하므로 모든 메시지가 두 번 도착할 수 있다고 가정하고 중복이 무해하도록 설계하라.

멀티 에이전트 프레임워크가 필요할까?

대개는 아니다. 내구성 있는 큐, 상태 테이블, 멱등성 키면 플랫폼이 이미 제공하는 프리미티브로 대부분의 프로덕션 요구를 충족한다. 프레임워크는 그것이 고유하게 해결하는 구체적인 문제에 부딪혔을 때만 도입하고, 기본값으로 도입하지 마라.

계속 읽기

AI 플레이북을 받아보세요

매주 수요일. 28,400명+ 구독자. 핵심만.

↵ 전체 결과 보기 esc esc 닫기