왜 멀티 에이전트인가

에이전트 하나가 벽에 부딪혔다면, 더 똑똑한 선택은 더 큰 에이전트가 아니라 더 많은 에이전트입니다.

유형: Learn 언어: TypeScript 선수 조건: Phase 14(에이전트 엔지니어링) 예상 시간: 약 60분

학습 목표

  • 단일 에이전트 한계(single-agent ceiling), 즉 컨텍스트 초과(context overflow), 섞인 전문성(mixed expertise), 순차 병목(sequential bottleneck)을 식별하고, 언제 여러 에이전트로 나누는 것이 올바른 선택인지 설명합니다.
  • 오케스트레이션 패턴(orchestration pattern)인 파이프라인(pipeline), 병렬 팬아웃(parallel fan-out), 감독자(supervisor), 계층형(hierarchical)을 비교하고 주어진 과제 구조에 맞는 패턴을 선택합니다.
  • 명확한 역할 경계(role boundary), 공유 상태(shared state), 통신 계약(communication contract)을 가진 멀티 에이전트 시스템(multi-agent system)을 설계합니다.
  • 멀티 에이전트 복잡성, 즉 지연 시간(latency), 비용(cost), 디버깅 난이도와 단일 에이전트의 단순성 사이의 트레이드오프(tradeoff)를 분석합니다.

문제

Phase 14에서 단일 에이전트(single agent)를 만들었습니다. 잘 작동합니다. 파일을 읽고, 명령을 실행하고, API를 호출하고, 결과를 추론할 수 있습니다. 그런데 이제 실제 코드베이스를 맡겨봅니다. 파일 200개, 언어 세 가지, 인프라에 의존하는 테스트, 코드를 쓰기 전에 외부 API를 조사해야 한다는 요구사항까지 있습니다.

에이전트는 막힙니다. LLM이 멍청해서가 아닙니다. 과제가 에이전트 루프 하나가 감당할 수 있는 범위를 넘어섰기 때문입니다. 컨텍스트 창(context window)은 파일 내용으로 가득 찹니다. 에이전트는 도구 호출 40번 전에 읽은 내용을 잊습니다. 연구자, 코더, 리뷰어를 한꺼번에 하려다가 세 역할 모두를 어설프게 수행합니다.

이것이 단일 에이전트 한계입니다. 다음 조건이 생길 때마다 이 한계에 부딪힙니다.

  • 한 창에 들어가지 않는 컨텍스트 - 파일 50개만 읽어도 20만 토큰을 넘어갑니다.
  • 단계마다 다른 전문성 - 조사는 코드 생성과 다른 프롬프팅(prompting)을 필요로 합니다.
  • 병렬로 할 수 있는 작업 - 파일 세 개를 동시에 읽을 수 있는데 왜 순차적으로 읽어야 할까요?

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.'단일 에이전트 한계(single-agent ceiling)'란 무엇인가요?

2.강력한 범용 에이전트 하나 대신 여러 전문 에이전트를 쓰는 이유는 무엇인가요?

0/2 답변 완료

개념

단일 에이전트 한계

단일 에이전트는 루프 하나, 컨텍스트 창 하나, 시스템 프롬프트(system prompt) 하나입니다. 그림으로 보면 이렇습니다.

┌─────────────────────────────────────────┐
│            단일 에이전트                │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │         컨텍스트 창               │  │
│  │                                   │  │
│  │  조사 메모                       │  │
│  │  + 코드 파일                     │  │
│  │  + 테스트 출력                   │  │
│  │  + 리뷰 피드백                   │  │
│  │  + API 문서                      │  │
│  │  + ...                           │  │
│  │                                   │  │
│  │  ██████████████████████ 가득 참 █ │  │
│  └───────────────────────────────────┘  │
│                                         │
│  시스템 프롬프트 하나가 조사 + 코딩 +  │
│  리뷰 + 테스트를 모두 커버하려고 함    │
│                                         │
│  결과: 모든 일을 그럭저럭만 수행       │
└─────────────────────────────────────────┘

세 가지가 깨집니다.

  1. 컨텍스트 포화(context saturation) - 도구 결과가 계속 쌓입니다. 30턴쯤 되면 에이전트는 파일 내용, 명령 출력, 이전 추론으로 15만 토큰을 이미 소비했습니다. 5턴에서 중요했던 세부사항은 사라집니다.

  2. 역할 혼란(role confusion) - "너는 연구자, 코더, 리뷰어, 테스터다"라고 말하는 시스템 프롬프트는 절반만 조사하고, 절반만 코딩하며, 리뷰는 끝내지 못하는 에이전트를 만듭니다.

  3. 순차 병목(sequential bottleneck) - 에이전트는 파일 A를 읽고, 그다음 파일 B를 읽고, 그다음 파일 C를 읽습니다. LLM 호출도 세 번 순차로 일어나고, 도구 실행도 세 번 순차로 일어납니다. 병렬성이 없습니다.

멀티 에이전트 해법

작업을 나눕니다. 각 에이전트에게 하나의 일, 하나의 컨텍스트 창, 그 일에 맞춘 하나의 시스템 프롬프트를 줍니다.

┌──────────────────────────────────────────────────────────┐
│                    오케스트레이터                        │
│                                                          │
│  "사용자 관리를 위한 REST API를 만든다"                  │
│                                                          │
│         ┌──────────┬──────────┬──────────┐               │
│         │          │          │          │               │
│         ▼          ▼          ▼          ▼               │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐  │
│   │ 연구자   │ │  코더    │ │ 리뷰어   │ │ 테스터   │  │
│   │          │ │          │ │          │ │          │  │
│   │ 문서를   │ │ 조사와   │ │ 코드     │ │ 테스트를 │  │
│   │ 읽고,    │ │ 명세에   │ │ 품질을   │ │ 실행하고 │  │
│   │ 패턴을   │ │ 맞춰     │ │ 확인하고 │ │ 결과를   │  │
│   │ 찾음     │ │ 코드 작성│ │ 버그 탐색│ │ 보고함   │  │
│   └─────┬────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘  │
│         │           │            │             │         │
│         └───────────┴────────────┴─────────────┘         │
│                          │                               │
│                     결과 병합                            │
└──────────────────────────────────────────────────────────┘

각 에이전트는 다음을 갖습니다.

  • 집중된 시스템 프롬프트: "너는 코드 리뷰어다. 네 유일한 일은 버그를 찾는 것이다."
  • 자기만의 컨텍스트 창: 다른 에이전트의 작업으로 오염되지 않습니다.
  • 명확한 입출력 계약: 조사 메모를 받고 코드를 출력합니다.

실제로 이렇게 하는 시스템

Claude Code 서브에이전트(subagents) - Claude Code가 Task로 서브에이전트를 만들면 범위가 제한된 일을 맡는 자식 에이전트(child agent)가 생성됩니다. 부모는 컨텍스트를 깨끗하게 유지합니다. 자식은 집중된 작업을 수행하고 요약을 반환합니다.

Devin - 계획자 에이전트(planner agent), 코더 에이전트(coder agent), 브라우저 에이전트(browser agent)를 실행합니다. 계획자는 작업을 단계로 나눕니다. 코더는 코드를 씁니다. 브라우저는 문서를 조사합니다. 각자 별도 컨텍스트를 갖습니다.

멀티 에이전트 코딩 팀(SWE-bench) - SWE-bench에서 상위권을 차지하는 시스템은 코드베이스를 읽는 연구자, 수정안을 설계하는 계획자, 구현하는 코더를 사용합니다. 단일 에이전트 시스템은 더 낮은 점수를 받습니다.

ChatGPT Deep Research - 여러 검색 에이전트를 병렬로 띄워 각자 다른 관점을 탐색하게 한 뒤, 결과를 종합합니다.

스펙트럼

멀티 에이전트는 이분법이 아닙니다. 하나의 스펙트럼입니다.

단순 ───────────────────────────────────────────── 복잡

 단일        서브         파이프라인    팀          스웜
 에이전트    에이전트

 ┌───┐       ┌───┐        ┌───┐───┐    ┌───┐───┐    ┌─┐┌─┐┌─┐
 │ A │       │ A │        │ A │ B │    │ A │ B │    │ ││ ││ │
 └───┘       └─┬─┘        └───┘─┬─┘    └─┬─┘─┬─┘    └┬┘└┬┘└┬┘
               │                │        │   │       ┌┴──┴──┴┐
             ┌─┴─┐          ┌───┘───┐    │   │       │shared │
             │ a │          │ C │ D │  ┌─┴───┴─┐    │ state │
             └───┘          └───┘───┘  │  msg   │    └───────┘
                                       │  bus   │
 1 loop      부모 +       단계별       │       │    N개의 동료,
 1 context   자식 작업    실행         └───────┘    창발 행동
                                       명시적
                                       역할

단일 에이전트(Single agent) - 루프 하나, 프롬프트 하나입니다. 단순한 과제에 좋습니다.

서브에이전트(Subagents) - 부모가 집중된 하위 과제(subtask)를 위해 자식을 띄웁니다. 부모는 계획을 유지하고, 자식은 보고합니다. Claude Code가 이 방식을 사용합니다.

파이프라인(Pipeline) - 에이전트가 순서대로 실행됩니다. 에이전트 A의 출력이 에이전트 B의 입력이 됩니다. 조사 -> 코드 -> 리뷰 -> 테스트처럼 단계가 있는 워크플로(workflow)에 좋습니다.

팀(Team) - 에이전트가 공유 메시지 버스(message bus)를 두고 병렬 실행됩니다. 각자 역할이 있고, 오케스트레이터가 조정합니다. 서로 다른 기술이 동시에 필요할 때 좋습니다.

스웜(Swarm) - 동일하거나 거의 동일한 에이전트가 공유 상태를 사용합니다. 고정된 오케스트레이터가 없습니다. 에이전트는 큐(queue)에서 일을 가져갑니다. 처리량이 높은 병렬 과제에 좋습니다.

네 가지 멀티 에이전트 패턴

패턴 1: 파이프라인

입력 ──▶ Agent A ──▶ Agent B ──▶ Agent C ──▶ 출력
          (조사)     (코드)      (리뷰)

각 에이전트가 데이터를 변환해 다음 단계로 넘깁니다. 이해하기 쉽습니다. 한 단계가 실패하면 나머지가 막힙니다.

패턴 2: 팬아웃 / 팬인(Fan-out / Fan-in)

                ┌──▶ Agent A ──┐
                │              │
입력 ──▶ 분할 ──├──▶ Agent B ──├──▶ 병합 ──▶ 출력
                │              │
                └──▶ Agent C ──┘

작업을 병렬 에이전트에 나누고 결과를 다시 병합합니다. 독립 하위 과제로 분해되는 작업에 좋습니다.

패턴 3: 오케스트레이터-워커(Orchestrator-Worker)

                    ┌──────────┐
                    │  Orch.   │
                    └──┬───┬───┘
                  task │   │ task
                 ┌─────┘   └─────┐
                 ▼               ▼
           ┌──────────┐   ┌──────────┐
           │ Worker A │   │ Worker B │
           └──────────┘   └──────────┘

똑똑한 오케스트레이터가 할 일을 결정하고, 워커(worker)에게 위임하고, 결과를 종합합니다. 오케스트레이터 자체도 워커를 띄우는 도구를 가진 에이전트입니다.

패턴 4: 피어 스웜(Peer Swarm)

         ┌───┐ ◄──── msg ────▶ ┌───┐
         │ A │                  │ B │
         └─┬─┘                  └─┬─┘
           │                      │
      msg  │    ┌───────────┐     │ msg
           └───▶│  Shared   │◄────┘
                │  State    │
           ┌───▶│  / Queue  │◄────┐
           │    └───────────┘     │
      msg  │                      │ msg
         ┌─┴─┐                  ┌─┴─┐
         │ C │ ◄──── msg ────▶ │ D │
         └───┘                  └───┘

중앙 오케스트레이터가 없습니다. 에이전트가 피어 투 피어(peer-to-peer)로 소통합니다. 결정은 상호작용에서 창발(emerge)합니다. 디버깅은 어렵지만 많은 에이전트로 확장할 수 있습니다.

멀티 에이전트를 쓰지 말아야 할 때

멀티 에이전트는 복잡성을 더합니다. 에이전트 사이의 모든 메시지는 잠재적 실패 지점입니다. 디버깅은 "대화 하나를 읽기"에서 "에이전트 다섯 개 사이의 메시지를 추적하기"로 바뀝니다.

단일 에이전트를 유지해야 할 때:

  • 과제가 하나의 컨텍스트 창에 들어갑니다. 작업 데이터가 대략 10만 토큰 이하입니다.
  • 단계마다 다른 시스템 프롬프트가 필요하지 않습니다.
  • 순차 실행으로도 충분히 빠릅니다.
  • 과제가 단순해서 나누는 비용이 가치보다 큽니다.

복잡성 비용:

  • 모든 에이전트 경계는 손실 압축(lossy compression) 단계입니다. 에이전트 A의 전체 컨텍스트는 에이전트 B에게 가는 메시지로 요약됩니다.
  • 조정 로직(coordination logic), 즉 누가 무엇을 언제 어떤 순서로 할지 정하는 로직 자체가 버그의 원천입니다.
  • 지연 시간이 늘어납니다. N개의 에이전트는 최소 N번의 순차 LLM 호출을 의미하며, 서로 오가며 대화해야 하면 더 늘어납니다.
  • 비용이 곱해집니다. 각 에이전트가 독립적으로 토큰을 씁니다.

경험칙은 이렇습니다. 과제가 도구 호출 20번 미만으로 끝나고 10만 토큰 안에 들어가면 단일 에이전트로 유지합니다.

만들어보기

Step 1: 과부하된 단일 에이전트

아래는 모든 일을 혼자 하려는 단일 에이전트입니다. 거대한 시스템 프롬프트 하나와 조사, 코드, 리뷰를 모두 담는 컨텍스트 창 하나를 가집니다.

type AgentResult = {
  content: string;
  tokensUsed: number;
  toolCalls: number;
};

async function singleAgentApproach(task: string): Promise<AgentResult> {
  const systemPrompt = `당신은 풀스택 개발자입니다. 반드시 다음을 수행합니다.
1. 요구사항을 조사합니다
2. 코드를 작성합니다
3. 버그를 찾기 위해 코드를 리뷰합니다
4. 테스트를 작성합니다
이 모든 일을 하나의 대화 안에서 수행합니다.`;

  const contextWindow: string[] = [];
  let totalTokens = 0;
  let totalToolCalls = 0;

  const research = await fakeLLMCall(systemPrompt, `조사: ${task}`);
  contextWindow.push(research.output);
  totalTokens += research.tokens;
  totalToolCalls += research.calls;

  const code = await fakeLLMCall(
    systemPrompt,
    `다음 조사 결과를 바탕으로:\n${contextWindow.join("\n")}\n\n이 과제를 위한 코드를 작성하세요: ${task}`
  );
  contextWindow.push(code.output);
  totalTokens += code.tokens;
  totalToolCalls += code.calls;

  const review = await fakeLLMCall(
    systemPrompt,
    `이전 모든 컨텍스트:\n${contextWindow.join("\n")}\n\n코드를 리뷰하세요.`
  );
  contextWindow.push(review.output);
  totalTokens += review.tokens;
  totalToolCalls += review.calls;

  return {
    content: contextWindow.join("\n---\n"),
    tokensUsed: totalTokens,
    toolCalls: totalToolCalls,
  };
}

이 접근의 문제는 다음과 같습니다.

  • 컨텍스트 창이 단계마다 커집니다. 리뷰 단계가 되면 조사 메모와 코드와 이전 추론이 모두 들어 있습니다.
  • 시스템 프롬프트가 너무 일반적입니다. 각 단계에 맞게 조정할 수 없습니다.
  • 아무것도 병렬로 실행되지 않습니다.

Step 2: 전문 에이전트

이제 나눕니다. 각 에이전트가 하나의 일을 맡습니다.

type SpecialistAgent = {
  name: string;
  systemPrompt: string;
  run: (input: string) => Promise<AgentResult>;
};

function createSpecialist(name: string, systemPrompt: string): SpecialistAgent {
  return {
    name,
    systemPrompt,
    run: async (input: string) => {
      const result = await fakeLLMCall(systemPrompt, input);
      return {
        content: result.output,
        tokensUsed: result.tokens,
        toolCalls: result.calls,
      };
    },
  };
}

const researcher = createSpecialist(
  "researcher",
  "당신은 기술 조사자입니다. 문서를 읽고, 패턴을 찾고, 발견한 내용을 요약합니다. 구현에 필요한 사실만 출력하세요."
);

const coder = createSpecialist(
  "coder",
  "당신은 시니어 TypeScript 개발자입니다. 요구사항과 조사 메모를 바탕으로 깔끔하고 테스트된 코드를 작성하세요. 다른 내용은 쓰지 마세요."
);

const reviewer = createSpecialist(
  "reviewer",
  "당신은 코드 리뷰어입니다. 버그, 보안 문제, 논리 오류를 찾으세요. 구체적으로 말하고 줄 번호를 인용하세요."
);

각 전문 에이전트는 집중된 프롬프트를 갖습니다. 각자는 필요한 입력만 담긴 깨끗한 컨텍스트 창을 받습니다.

Step 3: 메시지로 조정하기

명시적인 메시지 전달(message passing)로 전문 에이전트를 연결합니다.

type AgentMessage = {
  from: string;
  to: string;
  content: string;
  timestamp: number;
};

async function multiAgentApproach(task: string): Promise<AgentResult> {
  const messages: AgentMessage[] = [];
  let totalTokens = 0;
  let totalToolCalls = 0;

  const researchResult = await researcher.run(task);
  messages.push({
    from: "researcher",
    to: "coder",
    content: researchResult.content,
    timestamp: Date.now(),
  });
  totalTokens += researchResult.tokensUsed;
  totalToolCalls += researchResult.toolCalls;

  const coderInput = messages
    .filter((m) => m.to === "coder")
    .map((m) => `[From ${m.from}]: ${m.content}`)
    .join("\n");

  const codeResult = await coder.run(coderInput);
  messages.push({
    from: "coder",
    to: "reviewer",
    content: codeResult.content,
    timestamp: Date.now(),
  });
  totalTokens += codeResult.tokensUsed;
  totalToolCalls += codeResult.toolCalls;

  const reviewerInput = messages
    .filter((m) => m.to === "reviewer")
    .map((m) => `[From ${m.from}]: ${m.content}`)
    .join("\n");

  const reviewResult = await reviewer.run(reviewerInput);
  messages.push({
    from: "reviewer",
    to: "orchestrator",
    content: reviewResult.content,
    timestamp: Date.now(),
  });
  totalTokens += reviewResult.tokensUsed;
  totalToolCalls += reviewResult.toolCalls;

  return {
    content: messages.map((m) => `[${m.from} -> ${m.to}]: ${m.content}`).join("\n\n"),
    tokensUsed: totalTokens,
    toolCalls: totalToolCalls,
  };
}

각 에이전트는 자기에게 주소가 지정된 메시지만 받습니다. 컨텍스트 오염이 없습니다. 연구자가 문서 읽기에 쓴 5만 토큰은 리뷰어의 컨텍스트에 들어가지 않습니다.

Step 4: 비교하기

async function compare() {
  const task = "Express.js API용 레이트 리미터 미들웨어를 만들기";

  console.log("=== 단일 에이전트 ===");
  const single = await singleAgentApproach(task);
  console.log(`사용 토큰: ${single.tokensUsed}`);
  console.log(`도구 호출: ${single.toolCalls}`);

  console.log("\n=== 멀티 에이전트 ===");
  const multi = await multiAgentApproach(task);
  console.log(`사용 토큰: ${multi.tokensUsed}`);
  console.log(`도구 호출: ${multi.toolCalls}`);
}

멀티 에이전트 버전은 총 토큰을 더 많이 사용합니다. 에이전트 세 개와 별도 LLM 호출 세 번이 있기 때문입니다. 하지만 각 에이전트의 컨텍스트는 깨끗하게 유지됩니다. 시스템 프롬프트가 전문화되어 있기 때문에 각 단계의 품질이 좋아집니다.

사용해보기

이 강의는 언제 멀티 에이전트로 넘어갈지 결정하는 재사용 가능한 프롬프트를 만듭니다. outputs/prompt-multi-agent-decision.md를 확인하세요.

연습문제

  1. 네 번째 전문가를 추가하세요. 코더에게서 코드와 리뷰어에게서 리뷰 피드백을 받아 테스트를 작성하는 "테스터(tester)" 에이전트를 만듭니다.
  2. 리뷰어가 코더에게 피드백을 다시 보내 수정 루프(revision loop)를 만들 수 있게 파이프라인을 수정하세요. 최대 2라운드로 제한합니다.
  3. 순차 파이프라인을 팬아웃으로 바꾸세요. 연구자와 "요구사항 분석가(requirements analyzer)" 에이전트를 병렬로 실행하고, 두 출력을 병합한 뒤 코더에게 넘깁니다.

핵심 용어

용어흔한 설명실제 의미
스웜(Swarm)"AI 에이전트의 집단 지성"공유 상태를 가지며 고정 리더가 없는 피어 에이전트 집합입니다. 행동은 지역 상호작용에서 창발합니다.
오케스트레이터(Orchestrator)"보스 에이전트"다른 에이전트를 만들고 관리하는 도구를 가진 에이전트입니다. 계획하고 위임하지만 실제 작업은 하지 않을 수 있습니다.
코디네이터(Coordinator)"교통정리 담당"규칙에 따라 에이전트 사이의 메시지를 라우팅하는 비에이전트 컴포넌트입니다. LLM이 아니라 코드인 경우가 많습니다.
합의(Consensus)"에이전트들이 동의함"여러 에이전트가 진행 전에 합의해야 하는 프로토콜입니다. 충돌하는 출력을 해결해야 할 때 사용합니다.
창발 행동(Emergent behavior)"에이전트들이 스스로 알아냈음"명시적으로 프로그래밍되지 않았지만 에이전트 상호작용에서 생기는 시스템 수준 패턴입니다. 유용할 수도 있고 해로울 수도 있습니다.
팬아웃 / 팬인(Fan-out / fan-in)"에이전트용 맵리듀스"작업을 병렬 에이전트로 나누고(fan-out), 결과를 결합하는(fan-in) 방식입니다.
메시지 전달(Message passing)"에이전트들이 서로 말함"에이전트 사이의 통신 메커니즘입니다. 공유 컨텍스트 창 대신 구조화된 데이터를 한 에이전트에서 다른 에이전트로 보냅니다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

single vs multi
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

prompt-multi-agent-decision

Decide whether a task needs a multi-agent system or a single agent

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.파이프라인 방식의 순차 멀티 에이전트 패턴이 병렬 팬아웃보다 더 적합한 경우는 언제인가요?

2.멀티 에이전트 시스템에서 감독자(supervisor) 패턴은 무엇인가요?

3.단일 에이전트와 비교했을 때 멀티 에이전트 시스템의 주요 트레이드오프는 무엇인가요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다