슈퍼바이저 패턴
협업의 가장 기본 토폴로지. 한 명의 가 위에서 지휘하고, 여러 가 아래에서 일한다.
왜 슈퍼바이저인가
단일 에이전트의 한계 — , , — 를 한꺼번에 무너뜨리는 가장 단순한 답이 분업이다. 분업을 강제하는 가장 단순한 구조가 슈퍼바이저다.
한 명이 지시하고, 여러 명이 일한다. 회사를 그대로 옮긴 셈이다.
는 위에서 을 책임지고, 는 좁은 도메인만 본다.
토폴로지 한 장 요약
위 다이어그램의 본질은 두 화살표 방향이다 — 으로 갈라지고, 으로 모인다. 가운데서 가 둘 다 관장한다.
주는 일과 받는 일이 한 노드에 모이는 게 이 패턴의 정체성이다.
슈퍼바이저의 3가지 책임
의 일은 세 개로 쪼개진다.
- — 어떤 가 이 일을 받아야 할지 고른다.
- — 고른 워커에게 실제로 일을 던진다. 보통 동시 호출.
- — 돌아온 결과들을 하나로 합친다.
이 세 책임을 분리해서 보면 슈퍼바이저는 그 자체로 작은 파이프라인이다.
라우팅 세 가지 방법
import re
KEYWORD_TABLE = {
"security": re.compile(r"(보안|cve|secret)", re.I),
"perf": re.compile(r"(성능|latency|qps)", re.I),
"cost": re.compile(r"(비용|cost|토큰)", re.I),
}
def route_by_keyword(text: str) -> str:
for role, rgx in KEYWORD_TABLE.items():
if rgx.search(text):
return role
return "general"키워드 은 빠르고 결정적이지만 표현력이 약하다. LLM 분류는 그 반대다. 는 라우터가 고른 결과를 그대로 받는다. 세 방법은 가 어떤 정확도와 비용을 받아들이느냐의 다른 표현이다.
LLM 분류 라우터
자연어 의도를 코드로 길게 풀지 않고, 모델한테 한 줄로 묻는다.
async def route_by_llm(text, client, model):
r = await client.messages.create(
model=model,
max_tokens=20,
system="질문을 보고 security|perf|cost|general 중 하나만 출력한다.",
messages=[{"role": "user", "content": text}],
)
label = r.content[0].text.strip().lower()
return label if label in {"security","perf","cost","general"} else "general"장점은 유연성. 단점은 한 번의 LLM 호출 비용이 에 끼어든다. 가 워커를 호출하기 전에 이미 모델 비용이 발생한다는 뜻이다. 시스템의 흔한 함정.
팬아웃 — 동시 디스패치
워커 셋에게 같은 질문을 동시에 던진다. 직렬로 부르면 N배 느려진다.
import asyncio
from anthropic import AsyncAnthropic
client = AsyncAnthropic()
async def worker(role, q):
r = await client.messages.create(
model="claude-sonnet-4-6", max_tokens=400,
system=f"너는 {role} 전문가다.",
messages=[{"role": "user", "content": q}],
)
return r.content[0].text
async def fan_out(q):
roles = ["보안", "성능", "비용"]
return await asyncio.gather(*[worker(r, q) for r in roles])의 핵심은 asyncio.gather 혹은 Promise.all. 이 없으면 슈퍼바이저는 그냥 시리얼 큐일 뿐이다. 가 셋이면 지연도 셋배.
집계 전략 1 — 첫번째 성공
가장 빨리 답한 워커의 결과만 쓰고 나머지는 버린다. 워커들이 본질적으로 같은 일을 다른 방식으로 하는 시나리오에 적합하다.
async def first_success(coros):
"""첫번째 성공: 가장 빠른 결과를 반환하고 나머지는 취소."""
tasks = [asyncio.create_task(c) for c in coros]
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
for p in pending:
p.cancel()
return done.pop().result()지연을 최소화하지만, 비용은 N배. 시스템에서 SLA가 빡빡할 때 쓴다. 중 하나만 살아남아도 작업이 끝난다.
집계 전략 2 — 다수결
세 워커에게 같은 질문을 던지고, 같은 답이 가장 많이 나온 것을 채택한다. 환각을 분산으로 누른다.
from collections import Counter
def majority_vote(results: list[str]) -> str:
counts = Counter(results)
return counts.most_common(1)[0][0]다수결 는 단답형 작업(분류·추출)에서 잘 먹힌다. 자유 텍스트에선 정확히 같은 문장이 거의 안 나오니 임베딩 클러스터링이 필요하다. 에서 환각 완화 기법으로 널리 쓰인다. 수가 홀수일 때 더 안정적.
집계 전략 3 — 요약 병합
서로 다른 관점의 답을 한 명의 편집자 LLM이 한 단락으로 합친다. 의견이 다른 게 정상인 작업에 쓴다.
async def summarize_merge(answers: list[str], client, model):
merged = "\n---\n".join(answers)
r = await client.messages.create(
model=model, max_tokens=500,
system="너는 여러 의견을 모순 없이 한 단락으로 합치는 편집자다.",
messages=[{"role": "user", "content": merged}],
)
return r.content[0].text가장 비싸지만 가장 풍부하다. 편집자가 곧 또 다른 가 된다 — 슈퍼바이저는 결국 자기 위에 또 다른 LLM을 부른다. 가 LLM이면 시스템의 깊이가 한 단계 늘어난다.
슈퍼바이저 전체 흐름
async def supervisor(question):
roles = ["보안", "성능", "비용"]
# 1) fan-out: 동시에 디스패치
results = await asyncio.gather(*[worker(r, question) for r in roles])
# 2) fan-in: 요약 병합
merged = "\n\n".join(f"[{r}] {a}" for r, a in zip(roles, results))
final = await client.messages.create(
model="claude-sonnet-4-6", max_tokens=600,
system="너는 의견을 한 단락으로 합치는 편집자다.",
messages=[{"role": "user", "content": merged}],
)
return final.content[0].text이 30줄이 슈퍼바이저의 본질이다 — , , . 프레임워크 없이도 충분하다.
프레임워크 위 슈퍼바이저
에는 슈퍼바이저 패턴이 로 들어 있다. create_supervisor 한 줄로 디스패처 노드를 만든다. 는 Process.hierarchical 모드로 를 자동 생성한다.
이름은 다르지만 그림은 같다 — 위에서 지휘하고 아래에서 일한다.
슈퍼바이저의 함정
- 슈퍼바이저 자체가 이 된다.
- 라우팅 실수는 워커 비용을 두 번 쓰게 만든다 — 정확도가 곧 비용 효율.
- 단계의 편집자 LLM이 워커보다 더 비싼 모델이면, 슈퍼바이저가 병목이 된다.
- 전략을 사후에 바꾸면 결과 품질이 통째로 흔들린다 — 첫 설계가 중요하다.
다음 챕터
슈퍼바이저가 위에서 지휘한다면, 은 옆에서 넘긴다. 같은 인데 토폴로지가 다르다. 위에 군림하는 감독자 대신, 평등한 동료 사이의 로 일이 흐른다. 다음 챕터에서 그 차이를 코드로 본다.