계층형 패턴
를 재귀적으로 적용하면 이 된다. CEO 아래 매니저, 매니저 아래 워커 — 회사 조직도를 그대로 옮긴 구조.
슈퍼바이저의 재귀
슈퍼바이저는 한 명이 디스패치한다. 은 그 슈퍼바이저 밑에 또 슈퍼바이저가 있다. 트리 구조다.
계층형 = 슈퍼바이저 × 깊이.
각 내부 노드는 이자 동시에 위쪽의 자식이다. 잎 노드만 순수 워커다. 이 팀 안에 팀을 품은 모양.
트리 한 장
CEO 가 큰 목표를 받으면 — 자기가 직접 하지 않는다. 는 목표를 쪼개 매니저들에게 던지고, 매니저는 다시 잘게 쪼개 에게 넘긴다. 결과는 역방향으로 모아진다.
위에서 아래로 명령, 아래에서 위로 결과 — 이게 이다.
부모-자식 책임 분할
| 노드 | 무엇을 하는가 |
|---|---|
| 루트 | 목표를 받고 큰 도메인으로 쪼갠다 |
| 중간 () | 도메인 내 서브태스크 생성·라우팅 |
| 잎 () | 도구 호출·LLM 호출로 실제 작업 |
중요한 원칙 — 부모는 자식의 일을 직접 하지 않는다. 부모가 잎 일을 흉내내면 가 다시 시작된다. 의 분업이 무너지는 가장 흔한 실수다.
트리를 코드로
from dataclasses import dataclass, field
@dataclass
class Agent:
name: str
role: str
children: list["Agent"] = field(default_factory=list)
# 조직도 만들기
backend = Agent(name="Backend", role="백엔드")
frontend = Agent(name="Frontend", role="프론트엔드")
eng_mgr = Agent(name="EngMgr", role="엔지니어링 매니저",
children=[backend, frontend])
ceo = Agent(name="CEO", role="CEO", children=[eng_mgr])데이터 구조만 보면 그냥 트리다. 특별한 라이브러리가 필요 없다 — 의 본질은 안의 팀이고, 그 표현은 일반 트리로 충분하다. 가 자식 목록을 들고 있는 것만으로 시작이다.
재귀 실행
async def run(self, task: str) -> str:
if self.children:
subtasks = await self._plan(task)
results = await asyncio.gather(*[
c.run(sub) for c, sub in zip(self.children, subtasks)
])
return await self._merge(task, results)
# 잎: 실제 LLM 호출
r = await client.messages.create(
model="claude-sonnet-4-6", max_tokens=400,
system=f"너는 {self.role} 담당이다.",
messages=[{"role": "user", "content": task}],
)
return r.content[0].text같은 run 함수가 자기 자신을 호출한다. 이 의 재귀 적용이라는 말이 코드로 보인다. 는 자식의 run 만 부를 줄 알면 된다.
깊이의 비용
| 깊이 | LLM 호출 수 (각 노드 fan-out=3) | 누적 지연 |
|---|---|---|
| 1 (잎) | 1 | t |
| 2 | 1 + 3 = 4 | 2t |
| 3 | 1 + 3 + 9 = 13 | 3t |
| 4 | 1 + 3 + 9 + 27 = 40 | 4t |
깊이 4면 호출 수가 40번이다. 비용은 기하급수, 지연은 깊이에 선형. 은 강력하지만 그만큼 무겁다. 설계에서 깊이는 마지막 손잡이다. 을 평탄화할 수 있다면 그게 보통 정답.
에스컬레이션
자식이 실패하면 어떻게 되는가. 그냥 에러 던지고 끝내면 트리 전체가 무너진다. 은 “내가 못 푼 일을 위에 올린다”는 절차다.
이 신뢰도 낮은 답을 내면 가 받아 재계획한다. 도 못 풀면 사람에게 — 이게 와 합쳐지는 지점이다.
에스컬레이션 정책 코드
@dataclass
class EscalationPolicy:
max_retries: int = 2
confidence_floor: float = 0.6
escalate_keywords: tuple = ("법적", "환불", "보안 침해")
def should_escalate(self, reason: str, retries: int,
confidence: float) -> bool:
if retries >= self.max_retries: return True
if confidence < self.confidence_floor: return True
return any(k in reason for k in self.escalate_keywords)정책은 데이터로. 룰을 코드 곳곳에 흩어 놓지 말고 한 객체로 모은다. 사람 검토자가 한 파일만 보고 트리의 정책을 이해할 수 있어야 한다. 마다 다른 정책을 줄 수도 있다.
프레임워크 매핑
| 프레임워크 | 표현 방법 |
|---|---|
Process.hierarchical — 매니저 에이전트 자동 생성 | |
| 로 트리 표현 | |
| AutoGen | GroupChat 안의 GroupChat 중첩 |
이름은 다르지만 자료구조는 같다 — 노드와 자식 리스트. 의 본질이 자료구조에 있다는 증거.
언제 계층형을 쓰는가
- 작업이 명확히 도메인 별로 쪼개진다 (엔지니어링·디자인·마케팅).
- 도메인 안에서 또 잘게 쪼갤 일이 있다.
- 한 도메인 결과가 다른 도메인 결과와 합쳐져야 의미가 있다.
반대로 — 작업이 본질적으로 직렬이거나, 한 명이 끝까지 보는 게 자연스럽다면 이나 그냥 면 충분하다. 은 깊이를 살 만큼의 복잡도가 있을 때만.
계층형의 함정
- 추상화 비용 — 매니저 노드가 정작 아무 부가가치 없이 자식 결과를 그대로 패스스루.
- — 트리를 따라 정보가 위로 흐르며 압축·요약이 반복되면 디테일이 소실.
- 루프 — 부모가 자식에게 다시 던지는 무한 핑퐁.
- 의 깊이 = 디버깅 난이도 — 어느 레벨에서 잘못됐는지 찾기 어렵다. 마다 로그를 남기는 게 필수.
다음 챕터
을 봤다. 명령이 위에서 아래로 흐른다. 이제 같은 레벨끼리 서로 비평하고 합의하는 패턴 — 토론과 반성이다. 위계가 아닌 평행 검토로 정답률을 끌어올리는 길. 의 또 다른 정확도 손잡이를 다음 챕터에서 본다.