API Key 없이 Claude Agent 서버 만들기! #9 - 역할 분담하는 멀티 에이전트 만들기

조현상·2026년 4월 2일

ClaudeCode

목록 보기
17/17

들어가며

지금까지의 모든 예시에서 Claude는 혼자 일했습니다. 프롬프트를 받고, 도구를 쓰고, 결과를 돌려주는 단일 에이전트였죠.

하지만 복잡한 작업에서는 하나의 에이전트가 모든 것을 하기 어렵습니다:

  • 코드를 분석하는 에이전트, 테스트를 작성하는 에이전트, 문서를 생성하는 에이전트가 따로 있으면?
  • 각 에이전트에 다른 모델, 다른 도구, 다른 시스템 프롬프트를 줄 수 있다면?
  • 메인 에이전트가 상황을 판단해서 적절한 전문가 에이전트를 호출한다면?

AgentDefinition으로 이 모든 것이 가능합니다.


단일 에이전트 vs 멀티 에이전트

단일 에이전트:
  사용자 → [Claude] → 결과
                ↕
            모든 도구

멀티 에이전트:
  사용자 → [메인 Claude] → 판단 후 위임
              │
              ├── [코드 리뷰어] → Read, Grep으로 분석
              ├── [테스트 작성자] → Write, Bash로 테스트 생성/실행
              └── [문서 생성기] → Write로 문서 작성

메인 에이전트가 오케스트레이터 역할을 하고, 서브 에이전트가 전문가 역할을 합니다. Claude가 자체적으로 판단해서 어떤 서브 에이전트를 호출할지 결정합니다.


AgentDefinition 구조

@dataclass
class AgentDefinition:
    description: str                          # 에이전트 설명 (필수)
    prompt: str                               # 시스템 프롬프트 (필수)
    tools: list[str] | None = None            # 사용 가능한 도구
    disallowedTools: list[str] | None = None  # 사용 금지 도구
    model: str | None = None                  # 모델 (sonnet/opus/haiku/inherit 또는 전체 ID)
    skills: list[str] | None = None           # 스킬 목록
    memory: str | None = None                 # 메모리 범위 (user/project/local)
    mcpServers: list | None = None            # MCP 서버 목록
    initialPrompt: str | None = None          # 초기 프롬프트
    maxTurns: int | None = None               # 최대 턴 수
    background: bool | None = None            # 백그라운드 실행 여부
    effort: str | int | None = None           # 사고 깊이 (low/medium/high/max)
    permissionMode: str | None = None         # 권한 모드

필수 필드: description + prompt

AgentDefinition(
    description="코드를 분석하고 버그를 찾는 전문가",  # 메인 에이전트가 이걸 보고 언제 호출할지 판단
    prompt="너는 코드 리뷰 전문가야. 버그, 보안 취약점, 성능 이슈를 찾아서 보고해.",
)
  • description: 메인 에이전트가 이 에이전트를 언제 호출할지 판단하는 기준
  • prompt: 서브 에이전트의 시스템 프롬프트 — 역할과 행동 지침

선택 필드 상세

tools / disallowedTools — 도구 범위 제한

AgentDefinition(
    description="코드 리뷰어",
    prompt="코드를 분석하고 이슈를 보고해.",
    tools=["Read", "Glob", "Grep"],           # 이 도구만 사용 가능
    disallowedTools=["Write", "Edit", "Bash"], # 또는 이 도구를 금지
)

tools를 설정하면 해당 도구만 사용 가능합니다. disallowedTools를 설정하면 나머지 도구는 전부 사용 가능하되 지정한 것만 차단됩니다.

읽기 전용 에이전트를 만들려면 tools=["Read", "Glob", "Grep"]만 주면 됩니다.

model — 에이전트별 모델 지정

# 모델 별칭 사용
AgentDefinition(
    description="빠른 코드 생성기",
    prompt="...",
    model="sonnet",   # claude-sonnet 사용
)

# 전체 모델 ID
AgentDefinition(
    description="깊은 분석가",
    prompt="...",
    model="claude-opus-4-6",
)

# 메인 에이전트와 같은 모델
AgentDefinition(
    description="...",
    prompt="...",
    model="inherit",
)

지원하는 별칭: "sonnet", "opus", "haiku", "inherit"

비용 최적화 패턴: 분석은 opus로, 단순 코드 생성은 sonnet으로, 간단한 확인은 haiku로.

maxTurns — 실행 범위 제한

AgentDefinition(
    description="빠른 확인용",
    prompt="...",
    maxTurns=5,  # 5턴 이내로 끝내야 함
)

서브 에이전트가 무한 루프에 빠지는 것을 방지합니다. 프로덕션 환경에서는 반드시 설정하는 것을 권장합니다.

effort — 사고 깊이

AgentDefinition(
    description="보안 분석가",
    prompt="...",
    effort="max",   # 최대한 깊게 생각
)

AgentDefinition(
    description="포맷 변환기",
    prompt="...",
    effort="low",   # 빠르게 처리
)

"low", "medium", "high", "max" 또는 정수값.

background — 백그라운드 실행

AgentDefinition(
    description="로그 모니터",
    prompt="...",
    background=True,  # 메인 에이전트와 병렬로 실행
)

background=True면 메인 에이전트가 서브 에이전트 완료를 기다리지 않고 다음 작업을 진행합니다.

permissionMode — 에이전트별 권한

AgentDefinition(
    description="코드 수정자",
    prompt="...",
    permissionMode="bypassPermissions",  # 도구 자동 승인
)

AgentDefinition(
    description="코드 분석자",
    prompt="...",
    permissionMode="plan",  # 읽기만 가능
)

mcpServers — 에이전트별 MCP 서버

AgentDefinition(
    description="DB 분석가",
    prompt="...",
    mcpServers=["postgres"],  # 이름으로 참조 (ClaudeAgentOptions.mcp_servers에 정의된 서버)
)

memory — 메모리 범위

AgentDefinition(
    description="...",
    prompt="...",
    memory="project",  # 프로젝트 메모리만 참조
)

"user" (사용자 전역), "project" (프로젝트), "local" (로컬).


기본 사용법

from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition

options = ClaudeAgentOptions(
    permission_mode="bypassPermissions",
    max_turns=20,
    agents={
        "code-reviewer": AgentDefinition(
            description="코드를 분석하고 버그, 보안 취약점, 성능 이슈를 찾습니다.",
            prompt="너는 시니어 코드 리뷰어야. 코드를 분석하고 이슈를 심각도별로 분류해서 보고해.",
            tools=["Read", "Glob", "Grep"],
            model="opus",
            maxTurns=10,
            effort="high",
        ),
        "test-writer": AgentDefinition(
            description="주어진 코드에 대한 단위 테스트를 작성합니다.",
            prompt="너는 테스트 엔지니어야. 주어진 코드에 대해 pytest 기반 테스트를 작성하고 실행해.",
            tools=["Read", "Write", "Bash", "Glob"],
            model="sonnet",
            maxTurns=10,
        ),
    },
)

async for message in query(
    prompt="main.py를 리뷰하고 테스트도 작성해줘.",
    options=options,
):
    # 메인 에이전트가 code-reviewer와 test-writer를 적절히 호출
    ...

메인 에이전트는 description을 보고 자동으로 어떤 서브 에이전트를 호출할지 결정합니다. "리뷰해줘"라고 하면 code-reviewer를, "테스트 작성해줘"라고 하면 test-writer를 호출합니다.


서브 에이전트 진행 추적: Task 메시지

서브 에이전트가 실행되면 3종류의 Task 메시지가 스트림에 나타납니다. 3편(메시지 타입)에서 다뤘던 타입들입니다.

실행 흐름

메인 에이전트: "code-reviewer에게 위임합니다"
    │
    ▼
TaskStartedMessage
    task_id: "task-abc"
    description: "코드 리뷰"
    │
    ├── TaskProgressMessage (반복)
    │     usage: {total_tokens: 3000, tool_uses: 2, duration_ms: 5000}
    │     last_tool_name: "Read"
    │
    ├── TaskProgressMessage
    │     usage: {total_tokens: 8000, tool_uses: 5, duration_ms: 12000}
    │     last_tool_name: "Grep"
    │
    ▼
TaskNotificationMessage
    status: "completed"  (또는 "failed", "stopped")
    summary: "3개 이슈 발견: ..."
    output_file: "/path/to/output"
    │
    ▼
메인 에이전트: "리뷰 결과를 바탕으로..."

메시지 처리 코드

from claude_agent_sdk import (
    AssistantMessage,
    ResultMessage,
    TaskStartedMessage,
    TaskProgressMessage,
    TaskNotificationMessage,
    TextBlock,
)

active_tasks = {}

async for message in query(prompt="분석하고 테스트도 작성해줘", options=options):

    if isinstance(message, TaskStartedMessage):
        active_tasks[message.task_id] = message.description
        print(f"[태스크 시작] {message.description} (id={message.task_id})")

    elif isinstance(message, TaskProgressMessage):
        usage = message.usage
        print(f"[진행 중] 토큰 {usage['total_tokens']}, "
              f"도구 {usage['tool_uses']}회, "
              f"마지막 도구: {message.last_tool_name}")

    elif isinstance(message, TaskNotificationMessage):
        desc = active_tasks.pop(message.task_id, "알 수 없음")
        print(f"[태스크 {message.status}] {desc}")
        print(f"  요약: {message.summary}")
        if message.usage:
            print(f"  사용량: 토큰 {message.usage['total_tokens']}, "
                  f"도구 {message.usage['tool_uses']}회")

    elif isinstance(message, AssistantMessage):
        for block in message.content:
            if isinstance(block, TextBlock):
                print(f"Claude: {block.text}")

    elif isinstance(message, ResultMessage):
        print(f"\n완료: {message.num_turns}턴, ${message.total_cost_usd or 0:.4f}")

stop_task()로 서브 에이전트 중단

ClaudeSDKClient에서 실행 중인 서브 에이전트를 중단할 수 있습니다.

async with ClaudeSDKClient(options=options) as client:
    await client.query("전체 코드를 분석해줘")

    async for msg in client.receive_messages():
        if isinstance(msg, TaskProgressMessage):
            # 토큰을 너무 많이 쓰고 있으면 중단
            if msg.usage["total_tokens"] > 50000:
                print(f"토큰 초과 — 태스크 중단: {msg.task_id}")
                await client.stop_task(msg.task_id)
                # TaskNotificationMessage(status="stopped")가 올 때까지 대기

        elif isinstance(msg, TaskNotificationMessage):
            print(f"태스크 {msg.status}: {msg.summary}")

        elif isinstance(msg, ResultMessage):
            break

SandboxSettings — Bash 명령 격리

서브 에이전트가 Bash 도구를 사용할 때, 샌드박스로 파일시스템과 네트워크 접근을 격리할 수 있습니다.

options = ClaudeAgentOptions(
    permission_mode="bypassPermissions",
    sandbox={
        "enabled": True,
        "autoAllowBashIfSandboxed": True,   # 샌드박스 안이면 Bash 자동 승인
        "excludedCommands": ["git", "docker"],  # 이 명령은 샌드박스 밖에서 실행
    },
    agents={
        "code-runner": AgentDefinition(
            description="코드를 실행하고 결과를 확인합니다.",
            prompt="...",
            tools=["Bash", "Read", "Write"],
        ),
    },
)

SandboxSettings 필드

필드타입기본값설명
enabledboolFalse샌드박스 활성화 (macOS/Linux)
autoAllowBashIfSandboxedboolTrue샌드박스 안이면 Bash 자동 승인
excludedCommandslist[str][]샌드박스 밖에서 실행할 명령
allowUnsandboxedCommandsboolTruedangerouslyDisableSandbox 허용
networkdict네트워크 설정
ignoreViolationsdict무시할 위반
enableWeakerNestedSandboxboolFalseDocker 내 약한 샌드박스 (Linux)

네트워크 설정

sandbox={
    "enabled": True,
    "network": {
        "allowUnixSockets": ["/var/run/docker.sock"],  # Docker 소켓 허용
        "allowLocalBinding": True,   # localhost 바인딩 허용 (macOS)
    },
}

중요: 파일시스템/네트워크 제한은 샌드박스 설정이 아니라 권한 규칙(Read deny, Edit allow/deny, WebFetch allow/deny)으로 설정합니다. 샌드박스는 Bash 명령의 격리 실행 환경을 제공하는 것입니다.


실전 패턴

패턴 1: 코드 리뷰 + 테스트 + 문서 파이프라인

options = ClaudeAgentOptions(
    permission_mode="bypassPermissions",
    max_turns=30,
    agents={
        "reviewer": AgentDefinition(
            description="코드를 분석하고 버그, 보안 취약점, 코드 스멜을 찾습니다.",
            prompt=(
                "너는 시니어 코드 리뷰어야.\n"
                "1. 버그와 보안 취약점을 찾아\n"
                "2. 심각도(high/medium/low)로 분류해\n"
                "3. 수정 방안을 제시해"
            ),
            tools=["Read", "Glob", "Grep"],
            model="opus",
            maxTurns=10,
            effort="high",
        ),
        "tester": AgentDefinition(
            description="주어진 코드에 대한 pytest 단위 테스트를 작성하고 실행합니다.",
            prompt=(
                "너는 테스트 엔지니어야.\n"
                "1. 핵심 함수에 대한 pytest 테스트를 작성해\n"
                "2. 엣지 케이스를 포함해\n"
                "3. 테스트를 실행하고 결과를 보고해"
            ),
            tools=["Read", "Write", "Bash", "Glob"],
            model="sonnet",
            maxTurns=10,
        ),
        "documenter": AgentDefinition(
            description="코드의 API 문서와 사용 예시를 생성합니다.",
            prompt=(
                "너는 기술 문서 작성자야.\n"
                "1. 공개 함수/클래스의 docstring을 확인해\n"
                "2. API 레퍼런스 문서를 마크다운으로 작성해\n"
                "3. 사용 예시를 포함해"
            ),
            tools=["Read", "Write", "Glob"],
            model="sonnet",
            maxTurns=8,
            effort="medium",
        ),
    },
)

패턴 2: 비용 최적화 — 모델별 역할 분배

agents={
    # 복잡한 판단이 필요한 작업 → 강력한 모델
    "architect": AgentDefinition(
        description="시스템 아키텍처를 분석하고 개선 방안을 제시합니다.",
        prompt="...",
        model="opus",
        effort="max",
        maxTurns=5,       # 비싼 모델이니 턴 제한
    ),
    # 단순 코드 생성 → 빠르고 저렴한 모델
    "coder": AgentDefinition(
        description="구체적인 코드를 작성합니다.",
        prompt="...",
        model="sonnet",
        maxTurns=15,
    ),
    # 간단한 확인 작업 → 가장 저렴한 모델
    "checker": AgentDefinition(
        description="파일이 존재하는지, 문법 에러가 없는지 확인합니다.",
        prompt="...",
        model="haiku",
        maxTurns=3,
        effort="low",
    ),
}

패턴 3: 읽기 전용 분석자 + 쓰기 가능 실행자

agents={
    "analyst": AgentDefinition(
        description="코드를 분석하고 변경 계획을 세웁니다. 파일을 수정하지 않습니다.",
        prompt="코드를 분석해서 변경 계획을 세워. 직접 수정하지는 마.",
        tools=["Read", "Glob", "Grep"],      # 읽기 도구만
        model="opus",
    ),
    "executor": AgentDefinition(
        description="주어진 변경 계획을 실행하여 코드를 수정합니다.",
        prompt="분석 결과를 바탕으로 코드를 수정해. 변경 전후를 보고해.",
        tools=["Read", "Write", "Edit", "Bash"],  # 쓰기 도구 포함
        model="sonnet",
    ),
}

패턴 4: 백그라운드 모니터링 에이전트

agents={
    "monitor": AgentDefinition(
        description="백그라운드에서 빌드 상태와 테스트 결과를 모니터링합니다.",
        prompt="빌드와 테스트 상태를 주기적으로 확인하고 이상이 있으면 보고해.",
        tools=["Bash", "Read"],
        background=True,      # 메인 에이전트와 병렬 실행
        maxTurns=5,
        model="haiku",
    ),
    "developer": AgentDefinition(
        description="기능을 구현합니다.",
        prompt="...",
        tools=["Read", "Write", "Edit", "Bash"],
        model="sonnet",
    ),
}

내부 동작 원리

에이전트가 CLI에 전달되는 과정

ClaudeAgentOptions.agents
       │
       ▼
dataclass → dict 변환 (None 필드 제거)
       │
       ▼
initialize 제어 요청에 포함
  {"subtype": "initialize", "agents": {...}, "hooks": ...}
       │
       ▼
CLI가 에이전트 정의를 수신하고 등록
       │
       ▼
메인 에이전트가 판단하여 서브 에이전트 호출
       │
       ▼
Task 메시지로 진행 상황 보고

에이전트 정의는 CLI 인자가 아니라 제어 프로토콜로 전달됩니다. SDK가 내부적으로 스트리밍 모드를 사용하므로, query()ClaudeSDKClient 모두에서 동작합니다.

description이 중요한 이유

메인 에이전트는 description을 보고 어떤 서브 에이전트를 호출할지 판단합니다. 구체적이고 명확한 description이 정확한 위임으로 이어집니다.

# ❌ 모호한 description → 메인 에이전트가 언제 호출할지 판단 어려움
AgentDefinition(description="코드 관련 작업", prompt="...")

# ✅ 구체적인 description → 정확한 위임
AgentDefinition(description="Python 코드의 보안 취약점(SQL injection, XSS, 인증 우회)을 분석합니다", prompt="...")

AgentDefinition 필드 요약

필드타입필수설명
descriptionstrO에이전트 설명 (메인 에이전트의 판단 기준)
promptstrO시스템 프롬프트
toolslist[str]사용 가능한 도구 화이트리스트
disallowedToolslist[str]사용 금지 도구 블랙리스트
modelstr모델 (sonnet/opus/haiku/inherit 또는 ID)
maxTurnsint최대 턴 수
effortstr \| int사고 깊이 (low/medium/high/max)
backgroundbool백그라운드 실행
permissionModestr권한 모드
mcpServerslistMCP 서버 목록
memorystr메모리 범위 (user/project/local)
skillslist[str]스킬 목록
initialPromptstr초기 프롬프트

마치며

멀티 에이전트의 핵심은 역할 분리입니다.

기억할 3가지:

  1. description이 가장 중요하다 — 메인 에이전트가 이걸 보고 위임을 결정
  2. maxTurns를 반드시 설정하라 — 서브 에이전트의 무한 루프 방지
  3. 모델을 에이전트별로 다르게 하라 — 복잡한 판단은 opus, 단순 실행은 sonnet/haiku

전체 소스 코드는 GitHub에서 확인할 수 있습니다:
👉 GitHub 레포지토리 링크

profile
꿈꾸는 개발자

0개의 댓글