멀티 에이전트 아키텍처 설계 — 언제 나누고 언제 나누지 말아야 하는가
에이전트를 하나 만들어 돌려 보면 곧 같은 질문에 도달한다. 여러 개로 나누면 더 잘하지 않을까. 2026년 현재 멀티 에이전트는 주요 오케스트레이션 프레임워크 문서의 기본 예제로 자리 잡았고, 그래프 기반으로 노드와 엣지, 공유 상태를 정의하는 방식이 사실상 표준 어휘가 됐다. 문제는 나누는 순간 얻는 것과 잃는 것이 동시에 생긴다는 점이다. 이 글은 최신 소식을 나열하는 대신, 지금 설계 앞에 앉은 개발자가 내려야 할 판단을 정리한다.
나누는 이유는 사실 세 가지뿐이다
멀티 에이전트를 정당화하는 이유는 생각보다 적다. 아래 세 가지 중 하나에도 해당하지 않으면 단일 에이전트가 거의 항상 낫다.
- 컨텍스트 격리 — 한 작업의 중간 산출물이 다른 작업의 판단을 오염시킬 때다. 대량의 검색 결과를 읽는 조사 단계와 실제 코드를 고치는 단계가 같은 창을 공유하면, 조사 과정의 잡음이 구현 판단을 흔든다.
- 병렬성 — 서로 의존하지 않는 작업이 여러 건 있을 때다. 열 개 모듈을 각각 훑어야 한다면 순차 실행은 그냥 시간 낭비다.
- 역할별 권한 제한 — 검증 담당에게는 쓰기 권한을 주고 싶지 않은 경우다. 도구 목록 자체를 역할별로 다르게 주는 것은 프롬프트로 타이르는 것보다 훨씬 강한 통제다.
반대로 "정확도가 오를 것 같아서"는 이유가 되지 않는다. 나누면 단계 사이에 정보 손실 지점이 생기고, 그 손실이 정확도를 깎는 경우가 오히려 흔하다.
오케스트레이션 3패턴
수퍼바이저 — 조정자가 배분하고 통합한다
가장 널리 쓰이는 구성이다. 조정자 역할의 에이전트가 요청을 받아 어떤 하위 에이전트를 부를지 결정하고, 결과를 모아 최종 답을 만든다. 분기가 많고 어떤 단계가 필요한지 미리 알 수 없는 작업에 맞는다. 대신 조정자가 매 턴 판단하므로 호출 횟수가 늘고, 조정자의 판단이 틀리면 전체가 틀린다.
순차 — 앞의 결과가 뒤의 입력이 된다
단계가 고정된 작업에 쓴다. 요구 정리, 코드 생성, 테스트 검증처럼 순서가 바뀔 이유가 없는 파이프라인이다. 구현이 단순하고 디버깅도 쉽다. 단계 사이를 함수 호출처럼 다룰 수 있으면 굳이 에이전트로 만들 필요조차 없다는 점도 기억할 만하다.
병렬 — 흩뿌리고 한 번에 모은다
독립적인 다건 처리에 쓴다. 지연시간을 줄이는 데는 가장 확실하지만, 동시 실행 수를 정하지 않으면 비용과 레이트 리밋 양쪽에서 사고가 난다. 상한선을 코드로 걸어 두는 것이 기본이다.
실무에서는 이 셋이 섞인다. 수퍼바이저 아래에 병렬 조사 그룹을 두고, 그 결과를 순차 파이프라인으로 넘기는 식의 혼합 구조가 오히려 일반적이다.
컨텍스트 분리가 설계의 절반이다
멀티 에이전트 설계에서 가장 자주 대충 넘어가는 부분이 단계 사이에 무엇을 넘길 것인가다. 자연어 요약으로 넘기면 구현이 편하지만, 요약되는 과정에서 나중에 필요한 세부가 사라진다. 넘기는 내용을 구조로 고정하면 이 손실이 크게 줄어든다.
# 핸드오프를 자연어가 아니라 구조로 고정한다
from dataclasses import dataclass, field
@dataclass
class Handoff:
task_id: str
goal: str # 다음 단계가 달성할 목표 한 줄
facts: list = field(default_factory=list) # 검증된 사실만 (근거 경로 포함)
open_questions: list = field(default_factory=list)
files_touched: list = field(default_factory=list)
budget_left: int = 0 # 남은 호출 예산
def to_prompt(h: Handoff) -> str:
lines = [f"목표: {h.goal}"]
lines += [f"- 확인된 사실: {f}" for f in h.facts]
lines += [f"- 미해결: {q}" for q in h.open_questions]
lines.append(f"- 남은 예산: {h.budget_left}회")
return "\n".join(lines)
핵심은 facts와 open_questions를 분리하는 것이다. 앞 단계가 확인한 것과 추측한 것을 섞어 넘기면, 뒤 단계는 추측을 사실로 받아들이고 그 위에 작업을 쌓는다. 멀티 에이전트에서 발생하는 오답의 상당수가 여기서 시작된다.
어디서 무너지는가, 그리고 어떻게 찾는가
단일 에이전트의 실패는 대체로 눈에 보인다. 예외가 나거나 답이 이상하다. 멀티 에이전트의 실패는 다르다. 모든 단계가 정상 종료했는데 결론만 틀린 형태로 나타난다. 그래서 로그에 "성공"만 남기면 아무것도 알 수 없다.
디버깅의 출발점은 단계별 트레이스다. 어떤 에이전트가, 어떤 입력으로, 어떤 도구를 고려했고 실제로 무엇을 호출했으며, 몇 토큰을 썼고 얼마나 걸렸는지를 하나의 계층 트레이스로 이어 붙여야 재현이 가능하다. 이 영역에서는 오픈텔레메트리의 생성형 AI 시맨틱 규약이 공통 어휘로 정리되는 중이며, 공식 명세 기준으로 에이전트·워크플로·도구·모델 단위의 스팬과 토큰·지연 지표가 정의돼 있다. 다만 이 규약은 아직 안정화 이전 단계로 표기돼 있어 버전 간 이름이 바뀔 수 있다는 점은 감안해야 한다.
규약을 그대로 도입하지 않더라도, 최소한 아래 필드는 직접 남겨 두는 편이 낫다.
| 필드 | 왜 필요한가 |
| trace_id / parent_span | 어느 단계가 어느 단계를 불렀는지 복원 |
| agent_role | 역할 중복과 무한 왕복을 발견 |
| tool_name / arguments | 잘못된 인자로 부른 지점 특정 |
| retry_count | 재시도 폭주 조기 감지 |
| tokens_in / tokens_out | 어느 단계가 비용을 먹는지 분리 |
| stop_reason | 왜 끝났는지 — 완료인지 예산 소진인지 |
stop_reason이 특히 중요하다. 종료 조건을 명시하지 않은 구성에서는 에이전트가 "할 만큼 했다"고 스스로 판단하며 끝내는데, 이게 정상 완료인지 포기인지 구분되지 않으면 품질 문제를 영원히 못 찾는다.
재시도와 종료 조건은 코드로 못 박는다
에이전트는 자신을 제한하지 않는다. 실패한 도구 호출을 계속 다시 부르고, 이미 읽은 파일을 또 읽는다. 프롬프트에 "세 번까지만 시도하라"고 쓰는 것으로는 부족하다. 루프 바깥에 하드 리밋을 둬야 한다.
MAX_STEPS = 12
MAX_TOOL_RETRY = 2
MAX_TOKENS = 200_000
def run_agent(agent, state):
steps, used = 0, 0
while steps < MAX_STEPS and used < MAX_TOKENS:
step = agent.next(state)
used += step.tokens
steps += 1
if step.failed and step.retry >= MAX_TOOL_RETRY:
return state.fail("tool_retry_exceeded") # 조용히 재시도하지 않는다
if step.done:
return state.ok("completed")
return state.fail("budget_exhausted") # 예산 소진도 명시적 실패다
예산 소진을 실패로 기록하는 것이 요령이다. 이걸 성공으로 처리하면 미완성 결과가 파이프라인 뒤로 흘러가고, 다음 단계는 그것을 완성된 입력으로 취급한다.
쓰지 말아야 할 때
- 단계가 서로 강하게 얽혀 있을 때. 매 단계가 이전 전체 맥락을 필요로 하면 분리 자체가 손해다.
- 결정 규칙이 이미 명확할 때. 조건문으로 쓸 수 있는 분기를 조정자 에이전트에게 판단시키는 것은 비싸고 불안정하다.
- 지연시간이 사용자 경험을 좌우할 때. 단계마다 모델 호출이 쌓이면 체감 속도가 확연히 떨어진다.
- 실패했을 때 원인을 설명해야 할 때. 관측 체계를 먼저 갖추지 않은 상태에서 나누면 사고 시 아무 말도 못 한다.
비용도 무시할 수 없다. 공개된 비교 사례들에서 멀티 에이전트 구성은 동일 작업 기준 단일 구성 대비 토큰 사용량이 수십 퍼센트에서 몇 배까지 늘어난다고 보고된다. 수치의 폭이 큰 이유는 구성 방식에 따라 편차가 워낙 크기 때문인데, 방향만큼은 일관된다. 나누면 늘어난다.
도입 순서 — 실무에서 권하는 네 단계
- 단일 에이전트로 먼저 구현하고 실제 로그를 남긴다. 어디서 실패하는지 데이터가 없으면 분리 지점을 고를 수 없다.
- 병목이 확인된 지점 하나만 분리한다. 처음부터 다섯 개 역할을 설계하지 않는다.
- 핸드오프를 구조체로 고정하고, 각 단계에 독립적인 예산과 종료 조건을 준다.
- 분리 전후를 같은 입력 세트로 비교한다. 품질이 그대로인데 비용만 늘었다면 되돌리는 것이 맞다.
도구 연결 방식도 함께 정리해 두면 좋다. 에이전트가 외부 시스템에 붙는 경로는 모델 컨텍스트 프로토콜을 중심으로 빠르게 표준화되는 중이고, 2026년에는 주요 플랫폼들이 관련 클라이언트나 서버 배포 경로를 공식 문서에서 다루고 있다. 다만 도구를 너무 많이 노출하면 모델이 고르지 못하고 컨텍스트만 잡아먹는 문제가 함께 지적된다. 역할별로 필요한 도구만 주는 설계가 여기서도 유효하다.
정리
멀티 에이전트는 기능이 아니라 비용을 지불하고 사는 구조다. 지불할 만한 이유는 컨텍스트 격리, 병렬성, 권한 분리 셋이다. 그 외의 이유로 나누면 느려지고 비싸지고 디버깅이 어려워질 뿐이다. 기본값은 단일이고, 분리는 실측으로 정당화된 뒤에 하는 것이 맞다.
함께 보면 좋은 글: Claude Code 서브에이전트 실전 — 조사·구현·검증을 나눠 맡기는 법
함께 보면 좋은 글: MCP 서버 직접 만들기
※ 본문의 프레임워크 동작 방식과 명세 상태는 2026년 8월 기준이며 변경될 수 있습니다.
댓글
댓글 쓰기