서브에이전트로 개인 비서 만들기
서브에이전트로 개인 비서 만들기 (Build a personal assistant with subagents)
여러 분야의 전문성이 필요한 작업을 하나의 에이전트로 처리하려 하다 보면 금방 한계가 느껴져요. 예컨대 캘린더와 이메일 API에 동시에 접근하는 에이전트는 비슷비슷한 도구들 중에서 골라야 하고, API마다 정확한 형식을 이해해야 하며, 여러 도메인을 동시에 다뤄야 하죠. 이 튜토리얼에서는 슈퍼바이저 패턴(supervisor pattern) 을 직접 구현하면서 그 한계를 어떻게 해결하는지 체험해 볼 거예요. 중심 슈퍼바이저 에이전트가 전문화된 워커 에이전트들을 조율하는 멀티에이전트 구조입니다.
출처: LangChain 공식 문서 — Build a personal assistant with subagents
개요 (Overview)
슈퍼바이저 패턴은 작업이 서로 다른 유형의 전문 지식을 요구할 때 특히 빛을 발해요. 도메인 전반에 걸친 도구 선택을 관리하는 에이전트 하나를 만드는 대신, 전체 워크플로를 이해하는 슈퍼바이저가 조율하는 집중된 전문가들을 만드는 거죠.
이 튜토리얼에서 만들 개인 비서 시스템은 근본적으로 다른 책임을 가진 두 명의 전문가를 조율합니다.
- 캘린더 에이전트 (Calendar agent): 일정 잡기, 가능 시간 확인, 이벤트 관리를 담당해요.
- 이메일 에이전트 (Email agent): 커뮤니케이션 관리, 메시지 초안 작성, 알림 전송을 담당해요.
또한 사용자가 원하는 대로 (외부 발신 이메일 같은) 작업을 승인·수정·거부할 수 있게 휴먼-인-더-루프 검토(human-in-the-loop review)도 넣을 거예요.
langgraph-supervisor 패키지에서 마이그레이션 중이라면, interrupt·resume 흐름을 포함한 before-after 패턴이 있는 Migrate from langgraph-supervisor를 참고하세요.
왜 슈퍼바이저를 쓸까 (Why use a supervisor?)
멀티에이전트 아키텍처는 각자 자신의 프롬프트나 지시를 가진 워커들 사이에 도구를 분할할 수 있게 해줘요. 모든 캘린더·이메일 API에 직접 접근할 수 있는 에이전트 하나를 생각해 보세요. 비슷한 도구가 많고, 각 API의 정확한 형식을 이해해야 하며, 여러 도메인을 동시에 다뤄야 해요. 성능이 떨어지기 시작하면 관련 도구와 프롬프트를 논리적 그룹으로 나누는 게 도움이 될 수 있어요(반복적 개선을 관리하는 데도요).
다룰 개념 (Concepts)
설정 (Setup)
이 튜토리얼은 langchain 패키지가 필요해요.
pip install langchain
또는 conda라면:
conda install langchain -c conda-forge
자세한 내용은 설치 가이드를 참고하세요.
또한 LangSmith를 설정해서 에이전트 안에서 무슨 일이 벌어지는지 확인할 수 있어요. 다음 환경 변수를 설정하세요.
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."
채팅 모델 선택 (Components)
LangChain의 통합 모음에서 채팅 모델을 하나 고르면 되는데, 여기서는 OpenAI를 예로 들어볼게요.
pip install -U "langchain[openai]"
import os
from langchain.chat_models import init_chat_model
os.environ["OPENAI_API_KEY"] = "sk-..."
model = init_chat_model("gpt-5.5")
다른 제공자(Anthropic, Azure, Google Gemini, AWS Bedrock, HuggingFace, OpenRouter)의 설치 및 모델 클래스 예시는 원문 문서의 "Components" 섹션을 참고하세요.
1. 도구 정의하기 (Define tools)
구조화된 입력이 필요한 도구부터 정의해요. 실제 애플리케이션에서는 이 도구들이 실제 API(Google Calendar, SendGrid 등)를 호출하겠지만, 이 튜토리얼에서는 패턴을 보여주기 위해 스텁(stub)을 쓸게요.
from langchain.tools import tool
@tool
def create_calendar_event(
title: str,
start_time: str, # ISO format: "2024-01-15T14:00:00"
end_time: str, # ISO format: "2024-01-15T15:00:00"
attendees: list[str], # email addresses
location: str = ""
) -> str:
"""Create a calendar event. Requires exact ISO datetime format."""
# Stub: In practice, this would call Google Calendar API, Outlook API, etc.
return f"Event created: {title} from {start_time} to {end_time} with {len(attendees)} attendees"
@tool
def send_email(
to: list[str], # email addresses
subject: str,
body: str,
cc: list[str] = []
) -> str:
"""Send an email via email API. Requires properly formatted addresses."""
# Stub: In practice, this would call SendGrid, Gmail API, etc.
return f"Email sent to {', '.join(to)} - Subject: {subject}"
@tool
def get_available_time_slots(
attendees: list[str],
date: str, # ISO format: "2024-01-15"
duration_minutes: int
) -> list[str]:
"""Check calendar availability for given attendees on a specific date."""
# Stub: In practice, this would query calendar APIs
return ["09:00", "14:00", "16:00"]
2. 전문화된 서브에이전트 만들기 (Create specialized sub-agents)
다음으로 각 도메인을 담당할 전문화된 서브에이전트를 만들어요.
캘린더 에이전트 만들기 (Create a calendar agent)
캘린더 에이전트는 자연어 일정 요청을 이해하고 정밀한 API 호출로 번역해요. 날짜 파싱, 가능 시간 확인, 이벤트 생성을 처리하죠.
from datetime import date
from langchain.agents import create_agent
CALENDAR_AGENT_PROMPT = (
f"Today's date is {date.today().isoformat()}. "
"You are a calendar scheduling assistant. "
"Parse natural language scheduling requests (e.g., 'next Tuesday at 2pm') "
"into proper ISO datetime formats. "
"Use get_available_time_slots to check availability when needed. "
"If there is no suitable time slot, stop and confirm unavailability in your response. "
"Use create_calendar_event to schedule events. "
"Always confirm what was scheduled in your final response."
)
calendar_agent = create_agent(
model,
tools=[create_calendar_event, get_available_time_slots],
system_prompt=CALENDAR_AGENT_PROMPT,
)
캘린더 에이전트가 자연어 일정 요청을 어떻게 처리하는지 테스트해 볼게요.
query = "Schedule a team meeting next Tuesday at 2pm for 1 hour"
stream = calendar_agent.stream_events(
{"messages": [{"role": "user", "content": query}]},
version="v3",
)
for kind, item in stream.interleave("messages", "tool_calls"):
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
print(f"Tool result: {item.output}")
에이전트가 "next Tuesday at 2pm"을 ISO 형식("2024-01-16T14:00:00")으로 파싱하고, 끝나는 시간을 계산한 뒤 create_calendar_event를 호출하고 자연어 확인 메시지를 돌려주는 걸 볼 수 있어요.
이메일 에이전트 만들기 (Create an email agent)
이메일 에이전트는 메시지 작성과 전송을 담당해요. 수신자 정보 추출, 적절한 제목과 본문 작성, 이메일 커뮤니케이션 관리를 처리합니다.
EMAIL_AGENT_PROMPT = (
"You are an email assistant. "
"Compose professional emails based on natural language requests. "
"Extract recipient information and craft appropriate subject lines and body text. "
"Use send_email to send the message. "
"Always confirm what was sent in your final response."
)
email_agent = create_agent(
model,
tools=[send_email],
system_prompt=EMAIL_AGENT_PROMPT,
)
에이전트는 비공식적인 요청에서 수신자를 추론하고 전문적인 제목과 본문을 만들어 send_email을 호출한 뒤 확인 메시지를 돌려줘요. 각 서브에이전트는 도메인별 도구와 프롬프트를 가진 좁은 초점을 갖기 때문에, 특정 작업에서 뛰어난 성과를 낼 수 있어요.
3. 서브에이전트를 도구로 감싸기 (Wrap sub-agents as tools)
이제 각 서브에이전트를 슈퍼바이저가 호출할 수 있는 도구로 감쌉니다. 이게 계층적 시스템을 만드는 핵심 아키텍처 단계예요. 슈퍼바이저는 create_calendar_event 같은 저수준 도구가 아니라 schedule_event 같은 고수준 도구를 보게 됩니다.
@tool
def schedule_event(request: str) -> str:
"""Schedule calendar events using natural language.
Use this when the user wants to create, modify, or check calendar appointments.
Handles date/time parsing, availability checking, and event creation.
Input: Natural language scheduling request (e.g., 'meeting with design team
next Tuesday at 2pm')
"""
result = calendar_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
return result["messages"][-1].text
@tool
def manage_email(request: str) -> str:
"""Send emails using natural language.
Use this when the user wants to send notifications, reminders, or any email
communication. Handles recipient extraction, subject generation, and email
composition.
Input: Natural language email request (e.g., 'send them a reminder about
the meeting')
"""
result = email_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
return result["messages"][-1].text
도구 설명은 슈퍼바이저가 각 도구를 언제 쓸지 결정하는 데 도움을 주므로 명확하고 구체적으로 만드세요. 슈퍼바이저가 중간 추론이나 도구 호출을 볼 필요가 없으므로 서브에이전트의 최종 응답만 돌려줍니다.
슈퍼바이저가 받는 것을 제어하기 (Control what supervisor receives)
슈퍼바이저로 흘러가는 정보도 커스터마이즈할 수 있어요.
import json
@tool
def schedule_event(request: str) -> str:
"""Schedule calendar events using natural language."""
result = calendar_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
# Option 1: Return just the confirmation message
return result["messages"][-1].text
# Option 2: Return structured data
# return json.dumps({
# "status": "success",
# "event_id": "evt_123",
# "summary": result["messages"][-1].text
# })
중요: 서브에이전트 프롬프트에 최종 메시지가 관련 정보를 모두 담아야 한다는 점을 강조하세요. 흔한 실패 모드는 서브에이전트가 도구 호출을 수행하지만 결과를 최종 응답에 담지 않는 거예요.
4. 슈퍼바이저 에이전트 만들기 (Create the supervisor agent)
이제 서브에이전트를 조율하는 슈퍼바이저를 만들어요. 슈퍼바이저는 고수준 도구만 보고, 개별 API 수준이 아니라 도메인 수준에서 라우팅 결정을 내립니다.
SUPERVISOR_PROMPT = (
"You are a helpful personal assistant. "
"You can schedule calendar events and send emails. "
"Break down user requests into appropriate tool calls and coordinate the results. "
"When a request involves multiple actions, use multiple tools in sequence or in parallel as appropriate."
)
supervisor_agent = create_agent(
model,
tools=[schedule_event, manage_email],
system_prompt=SUPERVISOR_PROMPT,
)
5. 슈퍼바이저 사용하기 (Use the supervisor)
이제 여러 도메인을 가로지르는 조율이 필요한 복잡한 요청으로 전체 시스템을 테스트해 볼게요.
예시 1: 단순한 단일 도메인 요청 (Simple single-domain request)
query = "Schedule a team standup for tomorrow at 9am"
stream = supervisor_agent.stream_events(
{"messages": [{"role": "user", "content": query}]},
version="v3",
)
for kind, item in stream.interleave("messages", "tool_calls"):
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
print(f"Tool result: {item.output}")
슈퍼바이저는 이걸 캘린더 작업으로 판단하고 schedule_event를 호출하며, 캘린더 에이전트가 날짜 파싱과 이벤트 생성을 처리해요. 각 채팅 모델 호출의 프롬프트와 응답을 포함한 전체 정보 흐름은 LangSmith trace에서 확인할 수 있어요.
예시 2: 복잡한 다중 도메인 요청 (Complex multi-domain request)
query = (
"Schedule a meeting with the design team next Tuesday at 2pm for 1 hour, "
"and send them an email reminder about reviewing the new mockups."
)
stream = supervisor_agent.stream_events(
{"messages": [{"role": "user", "content": query}]},
version="v3",
)
for kind, item in stream.interleave("messages", "tool_calls"):
if kind == "messages":
for token in item.text:
print(token, end="", flush=True)
elif kind == "tool_calls":
print(f"\nTool call: {item.tool_name}({item.input})")
print(f"Tool result: {item.output}")
슈퍼바이저는 이 요청이 캘린더와 이메일 두 가지 작업을 모두 필요로 함을 인지하고, 미팅을 위해 schedule_event를, 알림을 위해 manage_email을 호출해요. 각 서브에이전트가 작업을 완료하면 슈퍼바이저가 두 결과를 일관된 응답으로 종합합니다.
슈퍼바이저는 기본적으로 서브에이전트에 작업을 순차적으로 배분해요. 각 도구 호출이 끝나야 다음 호출이 시작되죠. 그런데 위 trace에서처럼 많은 LLM이 한 응답에서 여러 도구 호출(schedule_event와 manage_email 동시)을 발행하면 런타임이 이를 병렬로 실행해요. 명시적인 병렬 배분도 설정할 수 있는데, 자세한 내용은 create_supervisor reference docs를 참고하세요.
완전한 동작 예시 (Complete working example)
모든 걸 하나의 실행 가능한 스크립트로 모으면 다음과 같아요.
"""
Personal Assistant Supervisor Example
This example demonstrates the tool calling pattern for multi-agent systems.
A supervisor agent coordinates specialized sub-agents (calendar and email)
that are wrapped as tools.
"""
from datetime import date
from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
# ============================================================================
# Step 1: Define low-level API tools (stubbed)
# ============================================================================
@tool
def create_calendar_event(
title: str,
start_time: str, # ISO format: "2024-01-15T14:00:00"
end_time: str, # ISO format: "2024-01-15T15:00:00"
attendees: list[str], # email addresses
location: str = ""
) -> str:
"""Create a calendar event. Requires exact ISO datetime format."""
return f"Event created: {title} from {start_time} to {end_time} with {len(attendees)} attendees"
@tool
def send_email(
to: list[str], # email addresses
subject: str,
body: str,
cc: list[str] = []
) -> str:
"""Send an email via email API. Requires properly formatted addresses."""
return f"Email sent to {', '.join(to)} - Subject: {subject}"
@tool
def get_available_time_slots(
attendees: list[str],
date: str, # ISO format: "2024-01-15"
duration_minutes: int
) -> list[str]:
"""Check calendar availability for given attendees on a specific date."""
return ["09:00", "14:00", "16:00"]
# ============================================================================
# Step 2: Create specialized sub-agents
# ============================================================================
model = init_chat_model("gpt-5.5") # for example
calendar_agent = create_agent(
model,
tools=[create_calendar_event, get_available_time_slots],
system_prompt=(
f"Today's date is {date.today().isoformat()}. "
"You are a calendar scheduling assistant. "
"Parse natural language scheduling requests (e.g., 'next Tuesday at 2pm') "
"into proper ISO datetime formats. "
"Use get_available_time_slots to check availability when needed. "
"If there is no suitable time slot, stop and confirm unavailability in your response. "
"Use create_calendar_event to schedule events. "
"Always confirm what was scheduled in your final response."
)
)
email_agent = create_agent(
model,
tools=[send_email],
system_prompt=(
"You are an email assistant. "
"Compose professional emails based on natural language requests. "
"Extract recipient information and craft appropriate subject lines and body text. "
"Use send_email to send the message. "
"Always confirm what was sent in your final response."
)
)
# ============================================================================
# Step 3: Wrap sub-agents as tools for the supervisor
# ============================================================================
@tool
def schedule_event(request: str) -> str:
"""Schedule calendar events using natural language.
Use this when the user wants to create, modify, or check calendar appointments.
Handles date/time parsing, availability checking, and event creation.
Input: Natural language scheduling request (e.g., 'meeting with design team
next Tuesday at 2pm')
"""
result = calendar_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
return result["messages"][-1].text
@tool
def manage_email(request: str) -> str:
"""Send emails using natural language.
Use this when the user wants to send notifications, reminders, or any email
communication. Handles recipient extraction, subject generation, and email
composition.
Input: Natural language email request (e.g., 'send them a reminder about
the meeting')
"""
result = email_agent.invoke({
"messages": [{"role": "user", "content": request}]
})
return result["messages"][-1].text
# ============================================================================
# Step 4: Create the supervisor agent
# ============================================================================
supervisor_agent = create_agent(
model,
tools=[schedule_event, manage_email],
system_prompt=(
"You are a helpful personal assistant. "
"You can schedule calendar events and send emails. "
"Break down user requests into appropriate tool calls and coordinate the results. "
"When a request involves multiple actions, use multiple tools in sequence or in parallel as appropriate."
)
)
핵심 포인트 (Key takeaways)
슈퍼바이저 패턴은 각 계층이 명확한 책임을 갖는 추상화의 계층을 만들어요. 슈퍼바이저 시스템을 설계할 때는 명확한 도메인 경계에서 시작해 각 서브에이전트에 집중된 도구와 프롬프트를 주고, 슈퍼바이저에게 명확한 도구 설명을 작성하며, 통합 전에 각 계층을 독립적으로 테스트하고, 필요에 따라 정보 흐름을 제어하세요.
슈퍼바이저 패턴을 언제 쓰나? 여러 개의 뚜렷한 도메인(캘린더, 이메일, CRM, 데이터베이스)이 있고, 각 도메인이 여러 도구나 복잡한 로직을 가지며, 중앙의 워크플로 제어를 원하고, 서브에이전트가 사용자와 직접 대화할 필요가 없을 때 슈퍼바이저 패턴을 쓰세요. 도구가 몇 개 없는 단순한 경우에는 단일 에이전트를 쓰세요. 에이전트가 사용자와 대화해야 한다면 핸드오프(handoffs)를 대신 사용하세요. 에이전트 간의 peer-to-peer 협업이 필요하면 다른 멀티에이전트 패턴을 고려해 보세요.
다음 단계 (Next steps)
핸드오프로 에이전트 간 대화를 배우고, 컨텍스트 공학으로 정보 흐름을 미세 조정하며, 멀티에이전트 개요로 서로 다른 패턴을 비교하고, LangSmith로 멀티에이전트 시스템을 디버깅·모니터링해 보세요.