
AI 상담원은 왜 하나인데, 업무는 여러 Task로 나눌까?: LiveKit Supervisor Pattern으로 설계한 병원 Voice AI
환자가 병원에 전화해 예약을 요청합니다.
AI는 먼저 환자를 확인하고, 필요한 경우 생년월일을 키패드로 입력받습니다. 보험을 선택하고, 실제 가능한 진료 항목과 의료진, 날짜와 시간을 찾아 최종 확인까지 받아야 합니다. 그런데 예약을 진행하던 환자가 갑자기 이렇게 말할 수도 있습니다.
“잠깐만요. 예약 말고 주차장 위치부터 알려주세요.”
또는 이렇게 요청할 수도 있습니다.
“그냥 상담원 연결해 주세요.”
한 통화 안에서 예약, 병원 정보, 키패드 입력, 상담원 연결이 오갑니다. 모든 일을 하나의 거대한 Agent에게 맡기면 프롬프트와 Tool이 계속 늘어나고, 지금 누가 대화를 책임지는지 불분명해집니다. 반대로 업무마다 별도 Agent를 만들면 Agent가 바뀔 때마다 대화 Context와 제어권을 넘겨야 합니다.
와이즈에이아이 인바운드 에이전트는 이 문제를 하나의 SingleAgent가 통화 전체의 맥락을 유지하고, 특정 업무만 짧게 Task에 위임하는 구조로 다룹니다.
LiveKit에서는 이를 Supervisor Pattern이라고 설명합니다. 오래 살아 있는 Agent가 통화를 계속 관리하고, AgentTask는 한정된 목표를 수행하는 동안에만 제어권을 가져간 뒤 typed result를 반환합니다. 이 글에서는 이 패턴이 실제 병원 전화 서비스에서 어떻게 동작하는지, 그리고 공식 예제보다 더 복잡한 운영 환경에서 어떤 경계를 추가했는지 살펴보겠습니다.
Version note: 이 글은 2026년 7월 31일과 현재 프로젝트의
livekit-agents>=1.6.8구성을 기준으로 작성했습니다. LiveKit의AgentTask와TaskGroupAPI는 빠르게 변하고 있으므로 새로 적용할 때는 현재 문서를 다시 확인해야 합니다.
목차
- 1. 한 명의 상담원처럼 들리되, 모든 일을 직접 하지는 않는다
- 2. Agent, Tool, Task는 서로 다른 책임을 가진다
- 3. 현재 구조의 Supervisor는 SingleAgent다
- 4. 예약 Tool 안에서 제어권은 어떻게 이동하는가
- 5. typed result는 자연어보다 안전한 복귀 경로다
- 6. Context는 전부 넘기는 것이 아니라 필요한 만큼 전달한다
- 7. 왜 업무마다 별도의 Agent를 만들지 않았을까
- 8. 왜 현재 예약 흐름은 TaskGroup이 아닌 하나의 Task인가
- 9. AgentTask의 어려움은 대화보다 수명주기에 있다
- 10. Task 경계는 관찰과 테스트의 경계이기도 하다
- 11. Supervisor Pattern을 선택하는 기준
- 마치며
1. 한 명의 상담원처럼 들리되, 모든 일을 직접 하지는 않는다
환자 입장에서 통화 상대는 처음부터 끝까지 한 명의 AI 상담원입니다. 예약 단계에 들어갔다고 목소리나 성격이 바뀌고, 예약을 취소했다고 다시 처음부터 자신을 소개하면 자연스럽지 않습니다.
하지만 시스템 내부의 업무까지 하나로 뭉쳐야 하는 것은 아닙니다.
병원 전화에서 필요한 업무는 성격이 서로 다릅니다.
- 병원 FAQ는 등록된 정보 안에서 짧게 답해야 합니다.
- 예약은 여러 차례 질문하면서 실제 후보를 조회하고 검증해야 합니다.
- 생년월일은 음성보다 DTMF 입력이 적합할 수 있습니다.
- 상담원 연결은 SIP Participant와 오디오 Track까지 제어해야 합니다.
- 연결에 실패하면 상담 메모나 희망 연락 시간을 별도로 수집해야 합니다.
이 기능을 모두 하나의 프롬프트와 Tool 목록에 넣으면 Agent는 매 순간 너무 많은 선택지를 갖게 됩니다. 예약 일시를 고르는 중에도 통화 종료 Tool이나 상담원 전환 Tool이 같은 판단 공간에 들어오고, 각 업무의 상세 규칙이 전체 시스템 프롬프트를 밀어 올립니다.
Supervisor Pattern은 외부의 일관성과 내부의 분리를 함께 가져갑니다.
핵심은 “여러 모듈을 호출한다”가 아닙니다.
통화의 장기적인 책임자는 그대로 두고, 한정된 목표에 대해서만 대화 제어권을 빌려준다.
LiveKit의 Supervisor Pattern과 Workflows 문서도 Agent를 장기 제어자, Task를 typed result를 반환하는 단기 작업 단위로 구분합니다. LiveKit Blog의 Supervisor Pattern 글은 이 구조를 전화 접수 담당자가 전문 업무를 잠시 위임하고 다시 통화를 이어가는 모습에 비유합니다.
2. Agent, Tool, Task는 서로 다른 책임을 가진다
Voice AI 코드에서는 Agent, Tool, AgentTask, Handoff가 한 흐름 안에 등장합니다. 이름이 비슷해 보여도 제어권과 수명주기가 다릅니다.
| 구성 요소 | 주된 책임 | 제어권 | 대표 사례 |
|---|---|---|---|
Agent | 장기 대화, 의도 판단, 전체 라우팅 | 활성 상태 동안 계속 유지 | SingleAgent |
| Tool | 외부 조회·상태 변경 또는 Task 진입 | 호출되는 동안 실행 | 예약 생성, 예약 조회, 통화 종료 |
AgentTask[T] | 한정된 다중 턴 목표를 수행하고 T 반환 | 완료할 때까지 잠시 제어 | DTMF 수집, 보험 선택, 예약 대화 |
| Agent Handoff | 다른 Agent에게 장기 제어권 이전 | 기존 Agent가 물러남 | 별도 전문 Agent로 역할 전환 |
| Human Transfer | AI에서 사람에게 통화 연결 | 미디어와 SIP 상태까지 변경 | SingleRoomWarmTransferTask |
Tool과 Task의 차이는 특히 중요합니다.
Tool은 “병원 API를 한 번 호출한다”처럼 짧은 동작일 수도 있지만, 현재 예약 Tool처럼 여러 Task와 API 호출을 조율하는 업무 진입점일 수도 있습니다. AgentTask는 사용자와 여러 번 대화해야 하는 목표에 적합합니다. 자신의 instructions와 Tool을 가질 수 있고, 목표를 달성하면 complete(result)로 결과를 반환합니다.
공식 Tasks and task groups 문서는 환자 접수, 예약 정보 수집, 동의 확인처럼 여러 턴에 걸친 구조화된 대화를 대표적인 Task 사용 사례로 제시합니다.
여기서 한 가지 주의할 점이 있습니다.
현재 프로젝트에서 이름에 Task가 붙는 모든 클래스가 LiveKit AgentTask인 것은 아닙니다. 예를 들어 환자 조회를 조율하는 VerifyPatientIdentityTask는 일반 Python 업무 객체이며 run()으로 실행됩니다. 반면 GetBirthdateDtmfTask와 DynamicBookingTask는 실제 AgentTask입니다.
이 구분은 코드 스타일 문제가 아닙니다. LiveKit AgentTask가 되는 순간 해당 객체는 대화의 active activity가 되고, 생성·완료·취소가 AgentSession의 수명주기 규칙을 따르게 됩니다. 사용자와 독립적인 백엔드 조율까지 무조건 AgentTask로 만들 필요는 없습니다.
3. 현재 구조의 Supervisor는 SingleAgent다
현재 인바운드 런타임에서 SingleAgent는 통화의 중심입니다.
SingleAgent는 환자의 발화와 병원별 설정을 바탕으로 다음 행동을 고릅니다.
- 등록된 병원 FAQ 안에서 답하기
- 예약 생성·조회·변경·취소 Tool 실행
- DTMF로 이미 정해진 업무 경로 실행
- 상담원 연결 또는 상담 메모 흐름으로 이동
- 명시적인 요청에 따라 통화 종료
그렇다고 SingleAgent가 예약의 진료과·의사·일시 선택 규칙까지 모두 직접 들고 있지는 않습니다. 예약 생성 의도가 확정되면 create_new_appointment Tool이 업무 경계를 넘겨받고, 그 안에서 환자 확인과 전문 Task들이 실행됩니다.
이 구조에서 SingleAgent의 역할은 모든 대화를 직접 수행하는 것이 아니라 올바른 업무 소유자를 선택하고, 그 결과를 다음 통화 경로로 연결하는 것입니다.
예약 Task가 끝났다고 항상 통화가 끝나는 것도 아닙니다. 환자가 예약을 취소했거나 다른 병원 정보를 물었다면 Task만 종료하고 SingleAgent가 다시 대화를 이어갑니다. 사람 상담원을 요청했다면 반환된 이탈 사유를 바탕으로 전환 흐름을 시작할 수 있습니다.
Supervisor가 필요한 이유가 바로 여기에 있습니다. 전문 Task는 자신의 목표가 끝난 뒤 전체 통화의 다음 행동까지 결정하지 않습니다.
4. 예약 Tool 안에서 제어권은 어떻게 이동하는가
신규 예약 요청을 예로 들면 제어권은 다음 순서로 움직입니다.
LiveKit AgentTask는 활성 Agent의 on_enter, on_exit, 또는 Tool 함수 안에서 await해야 합니다. 현재 예약 흐름은 세 번째 방식을 사용합니다. SingleAgent가 Tool을 선택하고, Tool 본문이 DynamicBookingTask를 직접 await합니다.
핵심 구조만 단순화하면 다음과 같습니다.
@function_tool()
async def create_new_appointment(ctx: RunContext_T) -> str | None:
# 환자 확인, 필요한 DTMF·보험 Task, 기준 정보 로딩
# ...
chat_ctx = ctx.session.current_agent.chat_ctx.copy(
exclude_instructions=True,
exclude_function_call=True,
exclude_handoff=True,
).truncate(max_items=6)
result = await DynamicBookingTask(
call_session_data=ctx.userdata,
reference=ctx.userdata.dynamic_booking_reference,
chat_ctx=chat_ctx,
)
if result.outcome == "exited":
return _dynamic_booking_exit_tool_result(
result,
ctx.userdata.call_config_data.language,
)
if result.reservation is None:
raise ToolError("Completed booking task ended without a reservation")
# 검증된 예약 요청을 병합하고 terminal 흐름을 처리한다.
Task가 활성화된 동안에는 예약 대화가 자신의 instructions와 제한된 Tool 집합을 사용합니다. Task가 complete()를 호출하면 기다리던 Tool이 결과를 받고, 다시 상위 흐름이 이어집니다.
이것은 새 프로세스나 새 통화를 시작하는 동작이 아닙니다. 같은 AgentSession과 음성 파이프라인 안에서 active activity만 잠시 바뀝니다.
5. typed result는 자연어보다 안전한 복귀 경로다
Task가 끝나면서 다음과 같은 문장만 돌려준다고 가정해 보겠습니다.
“환자가 예약을 완료했습니다.”
이 문장을 받은 상위 Agent는 다시 해석해야 합니다. 실제 후보가 있는지, 환자가 다른 문의로 빠진 것은 아닌지, 상담원 연결을 요청한 것은 아닌지 자연어에서 추측하게 됩니다.
현재 DynamicBookingTask는 DynamicBookingResult를 반환합니다.
@dataclass(frozen=True)
class DynamicBookingResult:
outcome: BookingOutcome
reservation: StagedBooking | None = None
exit_reason_code: DynamicBookingExitReasonCode | None = None
detail: str = ""
이 결과에는 불변식이 있습니다.
completed라면 완전한 예약 후보가 있어야 합니다.exited라면 예약 후보 대신 이탈 사유 코드가 있어야 합니다.- 상담원 요청, 예약 취소, 다른 문의를 서로 다른 상태로 구분합니다.
- 성공과 이탈 데이터가 한 결과에 동시에 들어갈 수 없습니다.
typed result의 장점은 타입 안정성만이 아닙니다. Task가 책임질 수 있는 범위를 결과 계약으로 제한한다는 것이 더 중요합니다.
DynamicBookingTask는 예약 대화를 끝낼 수 있지만 전체 통화를 임의로 종료하지 않습니다. 상담원 연결을 직접 시작하지도 않습니다. 자신이 발견한 결과를 명확한 상태로 반환하고, 전체 통화 정책을 아는 상위 흐름이 다음 행동을 결정합니다.
6. Context는 전부 넘기는 것이 아니라 필요한 만큼 전달한다
전문 Task가 자연스럽게 대화를 이어가려면 이전 Context가 필요합니다. 환자가 이미 “오후가 좋아요”라고 말했는데 예약 Task가 다시 처음부터 묻는다면 내부 구조가 그대로 드러납니다.
하지만 상위 Agent의 Context를 그대로 복사하는 것도 위험합니다.
SingleAgent의 전체 system instructions가 Task의 역할과 충돌할 수 있습니다.- 이전 Tool Call과 Handoff 기록이 새 Task의 다음 행동을 왜곡할 수 있습니다.
- 통화가 길어질수록 불필요한 Context가 LLM 비용과 판단 복잡도를 키웁니다.
- 오래된 후보나 완료된 업무의 흔적이 현재 Task의 상태처럼 보일 수 있습니다.
그래서 현재 예약 흐름은 상위 대화 Context를 복사하되 다음 항목을 제외합니다.
- 상위 Agent instructions
- 이전 function call
- 빈 메시지
- handoff와 config update 기록
그리고 최근 항목 수를 제한한 뒤 Task에 전달합니다. 환자의 최근 의도는 보존하면서, Task가 자신의 instructions와 현재 업무 상태를 우선하도록 만든 것입니다.
공식 문서도 Task에 Context를 전달할 때 exclude_instructions=True로 부모 Agent의 system prompt를 제외할 수 있다고 설명합니다. 다만 어느 기록을 얼마나 남길지는 애플리케이션의 업무 경계에 따라 달라집니다.
Context preservation의 목표는 모든 것을 기억하게 하는 것이 아니라, 반복 질문을 막는 데 필요한 근거만 안전하게 이어주는 것이다.
7. 왜 업무마다 별도의 Agent를 만들지 않았을까
예약 Agent, 정보 Agent, 전환 Agent를 각각 만들고 Handoff로 연결하는 구조도 가능합니다. 실제로 LiveKit은 Agent Handoff를 공식적으로 지원합니다.
하지만 Agent Handoff와 Supervisor Pattern은 해결하는 문제가 다릅니다.
| 질문 | Supervisor + Task | Agent Handoff |
|---|---|---|
| 통화의 장기 책임자는 누구인가 | 기존 Agent가 유지 | 새 Agent로 이전 |
| 전문 업무가 끝난 뒤 | 결과를 받고 기존 Agent가 재개 | 명시적으로 다시 Handoff해야 함 |
| 대화 Context | 같은 세션에서 범위를 정해 전달 | 복사·요약·초기화 정책 필요 |
| 적합한 상황 | 확인, 정보 수집, 예약처럼 끝이 있는 업무 | 역할·권한·모델이 장기적으로 달라지는 구간 |
| 주요 비용 | Context 누적, Task 수명주기 관리 | Handoff 지연, Context 이전과 역할 경계 관리 |
현재 병원 인바운드 서비스에서는 예약, 정보 문의, 상담원 요청이 한 통화 안에서 다시 섞일 수 있습니다. 환자가 예약을 중단하고 주차 정보를 물은 뒤 다시 예약으로 돌아오는 흐름도 가능합니다.
이때 예약 Agent로 완전히 Handoff하면 다음 문제가 생깁니다.
- 예약이 끝나거나 중단될 때 원래 접수 Agent로 돌아오는 경로가 필요합니다.
- 어느 Agent가 통화 종료와 상담원 연결 정책을 최종 소유하는지 정해야 합니다.
- 병원별
flow_config와 통화 후 분석 이벤트를 Agent 전환마다 일관되게 유지해야 합니다. - 환자가 주제를 바꿀 때 Context를 어디까지 넘길지 계속 판단해야 합니다.
반대로 예약은 시작과 종료가 있는 업무입니다. 예약 후보를 찾거나, 요청을 남기거나, 중단 사유를 반환하면 제어권을 돌려줄 수 있습니다. 따라서 별도 Agent보다 AgentTask가 더 자연스러운 경계가 됩니다.
다만 모든 시스템에 Supervisor Pattern이 정답은 아닙니다. 결제처럼 완전히 다른 권한이 필요한 구간, 서로 다른 모델이나 목소리를 써야 하는 역할, 이후 원래 Agent가 다시 관여할 필요가 없는 전문 영역이라면 Handoff가 더 명확할 수 있습니다.
8. 왜 현재 예약 흐름은 TaskGroup이 아닌 하나의 Task인가
LiveKit은 여러 Task를 순서대로 실행하고 이전 단계로 돌아갈 수 있는 TaskGroup도 제공합니다. 이름만 보면 환자 확인 → 보험 선택 → 진료과 → 의료진 → 일정 선택을 각각 Task로 만들고 묶는 방식이 자연스러워 보입니다.
그러나 현재 신규 예약의 핵심 대화는 하나의 DynamicBookingTask가 소유합니다.
이유는 실제 예약 대화가 고정된 일방향 단계가 아니기 때문입니다.
“오후 3시로 할게요.”
“아니요, 김 원장님으로 바꿔 주세요.”
“그분이 안 되면 날짜를 다음 주로 바꿀게요.”
이런 변경은 단순히 이전 단계 하나로 돌아가는 일이 아닙니다. 진료 항목·의료진·날짜·시간 조건이 서로 영향을 주고, 기존 예약 후보와 동의 대상도 함께 폐기해야 합니다.
초기 설계에서는 확인 단계를 별도 중첩 Task로 나누는 방안도 검토했습니다. 하지만 Task가 중첩되면 다음 경계가 추가됩니다.
- 사용자 발화를 부모와 자식 중 누가 소비했는가?
- 조건 변경 시 어느 Task로 돌아가야 하는가?
- 이전 Context를 합칠 것인가, 다시 재생할 것인가?
- 후보와 확인 상태를 어느 Task가 폐기하는가?
- 자식 Task가 취소될 때 부모 Task는 무엇을 반환하는가?
이 복잡성은 새로운 예약 기능을 만들지 않습니다. 같은 예약 여정 안에 수명주기 경계만 하나 더 추가합니다.
그래서 현재 구조는 다음 원칙을 사용합니다.
- 환자 확인과 DTMF처럼 독립적으로 완료되는 작업은 별도 단위로 둡니다.
- 보험 선택처럼 결과가 명확한 수집 단계는 별도
AgentTask로 둡니다. - 진료 항목·의료진·일시 탐색과 최종 확인은 하나의
DynamicBookingTask가 소유합니다. - Task 내부에서는 후보 상태와 확인 권한을 코드로 직렬화합니다.
- 전체 예약 여정이 끝났을 때만 하나의
DynamicBookingResult를 반환합니다.
또한 2026년 8월 기준 LiveKit 공식 문서는 TaskGroup을 experimental feature로 표시하고 있습니다. 현재 구현이 TaskGroup을 쓰지 않는 이유가 이것 하나뿐인 것은 아니지만, 운영 서비스에서는 API 안정성과 실제 대화 모델을 함께 고려해야 합니다.
Task를 많이 나누는 것이 좋은 분리는 아니다. 사용자가 하나의 목표로 느끼는 여정에는 하나의 명확한 대화 소유자가 필요하다.
9. AgentTask의 어려움은 대화보다 수명주기에 있다
AgentTask의 기본 형태는 단순합니다. 생성하고, 대화하고, complete(result)를 호출하면 됩니다. 실제 운영에서 어려운 부분은 발화·끼어들기·Task 완료·통화 종료가 동시에 움직인다는 것입니다.
Task를 어디에서 await하는가
Python LiveKit Agents에서 AgentTask는 활성 Agent의 on_enter, on_exit, 또는 Tool 함수 본문에서 await해야 합니다. 현재 예약 Task는 Tool 안에서 직접 await합니다. 별도 background task로 띄우거나 존재하지 않는 task.run()을 호출하지 않습니다.
직접 await해야 상위 Tool은 Task가 활성화된 동안 기다리고, 완료 결과를 정확히 한 번 받아 후속 경로를 실행할 수 있습니다.
Task 완료와 통화 종료를 분리한다
complete()는 현재 Task의 목표가 끝났다는 뜻입니다. 전체 통화가 끝났다는 뜻은 아닙니다.
예약 완료 뒤에는 상위 Tool이 세션 데이터와 최종 결과를 검증해야 합니다. 예약 중 다른 문의로 이탈했다면 SingleAgent가 통화를 계속해야 합니다. 상담원 요청이면 전환 정책을 적용해야 합니다.
Task가 직접 session.shutdown()까지 호출하면 재사용 가능한 예약 로직이 특정 통화 종료 정책에 결합되고, Task activity 복원과 세션 종료가 경쟁할 수 있습니다. 현재 구조는 Task가 상태를 반환하고, 상위 소유자가 terminal 여부를 결정하도록 분리합니다.
발화를 끝까지 기다리는 코드도 경계가 필요하다
현재 예약 Task 설계에서는 AgentTask의 on_exit가 아닌 async 메서드에서 generate_reply()나 wait_for_playout()을 직접 await하지 않도록 경계를 두었습니다. 프로젝트에는 이 패턴이 다른 Task에 다시 들어오는지 탐지하기 위한 AST 회귀 테스트도 있습니다.
이 규칙은 LiveKit 전체에 적용되는 일반 법칙이 아닙니다. 이 프로젝트에서 Task activity와 발화 완료를 서로 기다리는 순환 대기를 경험한 뒤 추가한 내부 guardrail입니다. Task 안에서는 필요한 발화를 시작하되, terminal 발화의 재생 완료와 세션 종료는 가능한 한 상위 Agent나 별도의 수명주기 소유자가 담당합니다.
중단은 결과 계약으로 복구해야 한다
환자가 예약 도중 “상담원 연결해 주세요”라고 말하면 예약 Task가 전체 세션을 강제로 바꾸지 않습니다. staff_connection이라는 이탈 사유를 반환하고, 상위 흐름이 상담원 연결 가능 여부와 병원 정책을 확인합니다.
환자가 “예약은 그만할게요”라고 하면 cancel을 반환하고 통화는 계속할 수 있습니다. 다른 문의로 전환하면 unrelated를 반환하고, 상위 Agent가 이미 유지하고 있던 Context에서 대화를 재개합니다.
이런 복구 경로가 없으면 Task 취소가 곧 세션 취소가 되거나, 중단된 Tool 안에서 새 Task를 시작하는 race condition으로 이어질 수 있습니다.
10. Task 경계는 관찰과 테스트의 경계이기도 하다
Supervisor Pattern의 장점은 코드를 나누는 데 그치지 않습니다. 통화가 어느 업무에 들어갔고 어떤 결과로 돌아왔는지 관찰할 수 있는 경계를 만듭니다.
현재 예약 흐름은 다음과 같은 이벤트를 남깁니다.
- 예약 생성 업무 시작
- 환자 확인 성공·실패·상담원 이관
- 보험 선택 결과
- 예약 Task 진입 당시의 병원 설정과 후보 범위
- 일정 검색과 후보 스테이징
- 최종 동의와 예약 요청 기록
- Task 완료 또는 구조화된 이탈 사유
통화 후에는 이러한 Agent event와 대화 기록을 함께 분석합니다. 단순히 “예약이라는 단어가 나왔는가?”가 아니라, 실제 예약 Task가 시작되었는지, 어느 단계까지 갔는지, 무엇 때문에 상위 Agent로 돌아왔는지를 구분할 수 있습니다.
테스트도 경계에 따라 나뉩니다.
| 검증 대상 | 적합한 테스트 |
|---|---|
| Result 불변식, 날짜 검증, 상태 전이 | 단위 테스트 |
| Task가 올바른 Tool을 선택하는가 | LiveKit text-based eval |
| 환자가 조건을 바꿀 때 후보가 폐기되는가 | 대화 시나리오 테스트 |
| Task 수명주기와 충돌하는 await 패턴이 생겼는가 | AST 회귀 테스트 |
| 예약 중 상담원 요청이 상위 경로로 복귀하는가 | 통합·환자 시뮬레이션 |
특히 Task의 happy path만 테스트해서는 부족합니다.
- Task 시작 직후 환자가 끊는 경우
- 발화와
complete()가 거의 동시에 끝나는 경우 - 확인 중 조건을 바꾸는 경우
- 환자 조회 API가 실패하는 경우
- Task가 이탈 결과를 반환한 직후 다른 Tool이 실행되는 경우
- Task 완료 후 session shutdown이 drain되는 경우
실시간 음성 시스템에서는 정상 결과보다 이 경계에서 더 많은 문제가 발생합니다.
11. Supervisor Pattern을 선택하는 기준
새로운 Voice AI 업무를 추가할 때 모든 기능을 Task로 만들 필요는 없습니다. 다음 질문으로 경계를 정할 수 있습니다.
Tool이면 충분한 경우
- 한 번의 외부 조회나 상태 변경으로 끝납니다.
- 추가 사용자 대화가 필요하지 않습니다.
- 결과를 바로 현재 Agent의 추론에 돌려주면 됩니다.
AgentTask가 적합한 경우
- 여러 턴에 걸쳐 하나의 목표를 완성해야 합니다.
- 해당 구간만의 instructions와 제한된 Tool 집합이 필요합니다.
- 성공·거절·이탈을 typed result로 반환할 수 있습니다.
- 업무가 끝난 뒤 원래 Agent가 다시 통화를 이어가야 합니다.
TaskGroup을 검토할 경우
- 단계가 명확한 순서로 진행됩니다.
- 사용자가 이전 단계로 돌아가 값을 수정해야 합니다.
- 각 단계가 독립적인 결과와 재사용 가치를 가집니다.
- 사용하는 LiveKit 버전의 experimental API를 감당할 수 있습니다.
Agent Handoff가 적합한 경우
- 새 역할이 이후 대화의 장기적인 책임자가 됩니다.
- 서로 다른 권한, 모델, 목소리 또는 Context 격리가 필요합니다.
- 기존 Agent가 다시 관여할 이유가 없습니다.
사람에게 연결해야 하는 경우
- AI가 처리할 수 없는 판단이나 권한이 필요합니다.
- 환자가 명시적으로 사람을 요청합니다.
- 업무 실패를 자연어로 감추지 말고 사람에게 이관해야 합니다.
결국 기준은 클래스의 크기가 아닙니다.
이 작업이 끝났을 때 누가 다시 대화를 책임져야 하는가?
답이 기존 Agent라면 Task가 유력합니다. 답이 새로운 전문 역할이라면 Handoff를 검토할 수 있습니다. 답이 사람이라면 AI 내부의 Agent 전환이 아니라 실제 통화 전환 경계를 설계해야 합니다.
마치며
복잡한 Voice AI를 만든다고 해서 Agent를 계속 늘릴 필요는 없습니다.
현재 와이즈에이아이 인바운드 에이전트는 하나의 SingleAgent가 통화 전체의 Context와 병원별 정책을 유지합니다. 예약, DTMF 입력, 보험 선택, 상담원 연결, 상담 메모처럼 목표가 분명한 업무는 전문 Task에 잠시 위임합니다. Task는 자신의 목표에 집중하고, 완료되면 자연어 설명이 아니라 typed result로 제어권을 돌려줍니다.
이 구조는 환자에게는 한 명의 상담원처럼 들리면서도, 내부적으로는 업무별 instructions와 Tool, 상태, 테스트 경계를 분리합니다.
중요한 것은 AgentTask라는 클래스를 사용하는 것 자체가 아닙니다.
- 장기 대화와 단기 업무의 소유자를 구분하고
- Context를 필요한 만큼만 전달하며
- Task 완료와 통화 종료를 분리하고
- 이탈과 실패를 구조화된 결과로 돌려주고
- 실제 대화의 수정·중단·끼어들기 경로까지 검증하는 것
이 경계가 명확할수록 Agent는 덜 거대해지고, Task는 더 재사용 가능해지며, 통화는 중간에 업무가 바뀌어도 한 사람과 이야기하는 것처럼 이어집니다.
Supervisor Pattern은 여러 Agent를 지휘하는 거대한 AI를 만드는 기법이 아닙니다.
하나의 대화 책임자가, 필요한 순간에만 전문 업무를 맡기고 결과를 회수하는 제어 구조다.
병원 전화처럼 여러 업무가 한 통화 안에서 오가는 서비스에서는 이 단순한 원칙이 대화의 자연스러움과 시스템의 예측 가능성을 함께 지켜줍니다.