LLM 에이전트 오케스트레이션 패턴 — 단일 에이전트부터 멀티 에이전트까지

AI

LLM 에이전트오케스트레이션멀티 에이전트ReActAI 아키텍처

이 글은 누구를 위한 것인가

  • LLM 에이전트를 만들었지만 복잡한 태스크에서 실패율이 높은 팀
  • 단일 에이전트로는 한계가 있어 멀티 에이전트 전환을 고민하는 엔지니어
  • 에이전트 패턴의 이름은 들었지만 어떤 상황에서 쓰는지 기준이 없는 개발자

에이전트란 무엇인가

LLM 에이전트는 단순히 프롬프트에 응답하는 것을 넘어, 도구를 사용하고 결과를 관찰하며 목표를 달성하기 위해 반복적으로 행동하는 시스템이다.

목표 입력
  │
  ▼
[LLM] 다음 행동 결정
  │
  ├─ 도구 호출 (웹 검색, DB 조회, API 호출, 코드 실행)
  │       │
  │       ▼
  │    관찰 결과
  │       │
  └───────┘ (반복)
  │
  ▼
목표 달성 → 최종 응답

1. ReAct: 가장 보편적인 패턴

Reasoning + Acting의 줄임말. LLM이 생각(Thought) → 행동(Action) → 관찰(Observation)을 반복하며 문제를 해결한다.

패턴 구조

사용자: "2026년 1분기 우리 회사 매출을 작년 동기 대비로 분석해줘"

Thought: DB에서 2026년 Q1과 2025년 Q1 매출 데이터를 조회해야 한다.
Action: query_database("SELECT quarter, revenue FROM sales WHERE year IN (2025, 2026) AND quarter = 'Q1'")
Observation: [{"year": 2025, "quarter": "Q1", "revenue": 1250000}, {"year": 2026, "quarter": "Q1", "revenue": 1480000}]

Thought: 데이터를 얻었다. 증가율 계산: (1480000 - 1250000) / 1250000 = 18.4% 증가.
Action: final_answer("2026년 1분기 매출은 14.8억원으로, 작년 동기(12.5억원) 대비 18.4% 증가했습니다.")

적합한 경우

  • 태스크 수행에 필요한 도구 수가 3~5개 이하
  • 단선적인 문제 해결 (탐색 → 분석 → 응답)
  • 실시간 응답이 필요한 경우

한계

  • 복잡한 멀티스텝 태스크에서 중간 단계 실수 누적
  • 루프(같은 행동 반복)가 발생할 수 있음
  • 병렬 처리 불가 (순차적 실행)

2. Plan-and-Execute: 계획 후 실행

먼저 전체 실행 계획을 수립하고, 단계별로 실행한다. 복잡한 멀티스텝 태스크에 적합하다.

[Planner LLM]
입력: 복잡한 사용자 요청
출력: 실행 단계 목록
  1. 현재 재고 데이터 조회
  2. 최근 30일 판매 추이 분석
  3. 재고 부족 예상 품목 식별
  4. 자동 발주 권고안 생성
  5. 요약 리포트 작성

[Executor Agent]
각 단계를 순서대로 실행
단계 실패 시 Planner에게 재계획 요청

ReAct vs Plan-and-Execute

ReActPlan-and-Execute
복잡도단순~중간복잡
계획 수정즉각적배치
토큰 비용낮음높음 (계획 단계 추가)
실패 복구어려움단계별 재시도 가능
병렬 실행불가독립 단계 병렬화 가능

3. 멀티 에이전트: 역할 분리

단일 에이전트가 너무 많은 역할을 맡으면 성능이 저하된다. 전문화된 에이전트를 역할별로 분리하고 오케스트레이터가 조율한다.

오케스트레이터-서브에이전트 패턴

[오케스트레이터]
사용자 요청을 분석하고 적절한 서브에이전트에 위임

       ┌────────────┬────────────┬────────────┐
       ▼            ▼            ▼            ▼
[검색 에이전트] [분석 에이전트] [코드 에이전트] [리포트 에이전트]
 웹/DB 검색    데이터 분석    코드 작성/실행  문서 작성

에이전트 통신 설계

에이전트 간 메시지 포맷을 구조화한다:

from dataclasses import dataclass
from typing import Any

@dataclass
class AgentMessage:
    sender: str
    receiver: str
    task: str
    context: dict[str, Any]
    priority: int = 1

@dataclass
class AgentResult:
    agent_id: str
    task: str
    success: bool
    result: Any
    error: str | None = None
    tokens_used: int = 0

체크 앤 밸런스: 에이전트 간 검증

중요한 출력은 다른 에이전트가 검증한다:

async def generate_with_review(task: str) -> str:
    # 1단계: 생성 에이전트
    draft = await generator_agent.run(task)

    # 2단계: 검토 에이전트 (독립적으로 검증)
    review = await reviewer_agent.run(
        f"다음 결과를 검토하고 오류나 개선점을 찾으세요:\n{draft}"
    )

    if review.has_issues:
        # 3단계: 수정
        return await generator_agent.run(
            f"원본: {draft}\n검토 의견: {review.feedback}\n수정된 버전을 작성하세요."
        )

    return draft

4. 신뢰성과 안전 설계

루프 방지

에이전트가 같은 행동을 반복하는 무한루프를 막아야 한다.

class AgentRunner:
    def __init__(self, max_steps: int = 20, max_tokens: int = 50000):
        self.max_steps = max_steps
        self.max_tokens = max_tokens

    async def run(self, task: str) -> str:
        steps = 0
        total_tokens = 0
        action_history = []

        while steps < self.max_steps and total_tokens < self.max_tokens:
            action = await self.llm.decide_action(task, action_history)

            # 동일 액션 반복 탐지
            if action_history.count(action) >= 3:
                return "태스크를 완료할 수 없습니다. 다른 방법을 시도해주세요."

            result = await self.execute_action(action)
            action_history.append(action)
            steps += 1
            total_tokens += result.tokens_used

            if action.is_final:
                return action.final_answer

        return "최대 처리 한도에 도달했습니다."

인간 검토 게이트

돌이킬 수 없는 작업 전에 인간 확인을 요청한다.

REQUIRES_HUMAN_APPROVAL = [
    'send_email',
    'delete_record',
    'execute_payment',
    'post_to_social',
]

async def execute_action(action: Action) -> Result:
    if action.tool_name in REQUIRES_HUMAN_APPROVAL:
        approval = await request_human_approval(
            f"다음 작업을 수행합니다: {action.description}\n승인하시겠습니까?"
        )
        if not approval:
            return Result(success=False, error="사용자가 거부했습니다.")

    return await tool_registry[action.tool_name](action.params)

5. 비용과 지연시간 관리

멀티스텝 에이전트는 단일 LLM 호출보다 훨씬 많은 토큰과 시간을 소비한다.

병렬 실행

독립적인 태스크는 병렬로 처리한다.

async def parallel_research(topics: list[str]) -> list[str]:
    # 각 주제 검색을 병렬로 실행
    results = await asyncio.gather(*[
        search_agent.run(topic) for topic in topics
    ])
    return results

에이전트 메모리: 컨텍스트 압축

긴 대화 히스토리를 그대로 유지하면 토큰 비용이 폭발한다. 중간 요약으로 컨텍스트를 압축한다.

async def compress_history(history: list[Message], keep_last: int = 5) -> list[Message]:
    if len(history) <= keep_last:
        return history

    old_messages = history[:-keep_last]
    summary = await summarize_agent.run(
        f"다음 대화 내용을 핵심만 요약하세요:\n{format_messages(old_messages)}"
    )

    return [Message(role='system', content=f"이전 대화 요약: {summary}")] + history[-keep_last:]

맺으며

에이전트 설계에서 가장 흔한 실수는 처음부터 복잡한 멀티 에이전트 시스템을 만드는 것이다. ReAct 패턴의 단일 에이전트로 시작하고, 실제 실패 케이스가 누적된 후 복잡성을 추가하는 것이 훨씬 효율적이다.

"에이전트가 몇 번이나 LLM을 호출하는가" 를 항상 추적해야 한다. 에이전트 실행 당 평균 호출 수가 늘어날수록 비용과 지연시간이 급증한다. 에이전트 설계의 목표는 최소한의 LLM 호출로 목표를 달성하는 것이다.