AI에서 사람으로 안전하게 연결하기: Single-Room Warm Transfer


1. “상담원에게 연결해 주세요”

환자에게 상담원 연결은 간단한 기능처럼 보인다. AI에게 연결을 요청하고, 잠시 기다린 뒤, 사람과 대화를 시작하면 된다.

그러나 Voice AI 내부에서 일어나는 일은 그렇게 단순하지 않다.

  • 환자가 기다리는 동안에는 대기음악만 들려야 한다.
  • 상담원이 전화를 받자마자 환자의 음성이 들리면 안 된다.
  • AI가 정리한 환자 정보와 문의 내용은 상담원에게만 전달해야 한다.
  • 상담원이 수락한 뒤에만 환자와 상담원의 음성을 연결해야 한다.
  • 상담원이 받지 않거나 거절하면 환자를 AI 상담으로 복구해야 한다.
  • 여러 환자가 동시에 연결을 요청하면 상담원과 SIP trunk를 두고 경쟁하지 않게 해야 한다.
  • 연결에 성공해도 실제 상담이 끝날 때까지 자원 점유를 유지해야 한다.

사용자에게는 버튼 하나지만, 시스템에는 환자, AI, 사람 상담원이라는 세 Participant와 계속 변하는 오디오 경로가 존재한다.

이 글에서는 먼저 LiveKit이 제공하는 공식 WarmTransferTask가 어떤 문제를 풀어 주는지 살펴본다. 이어서 직접 운영하는(self-hosted) LiveKit 환경에서 왜 이를 그대로 쓸 수 없었는지, 그리고 하나의 통화방에서 음성 연결을 제어하는 SingleRoomWarmTransferTask를 어떻게 만들었는지 설명한다.

TL;DR

  • Warm Transfer는 AI가 사람 상담원을 먼저 호출하고, 통화 맥락을 전달한 뒤, 수락 시 환자와 연결하는 방식이다.
  • LiveKit은 별도의 상담원 Room, 대기음악, SIP 호출, 컨텍스트 전달과 통화 병합을 자동화하는 WarmTransferTask를 제공한다.
  • 우리가 구현을 시작했을 당시 공식 Task는 LiveKit Cloud 사용을 전제로 했고, self-hosted 환경에는 그대로 적용할 수 없었다.
  • 그래서 환자, AI, 상담원을 하나의 Room에 두고 Track Subscription으로 오디오를 격리·전환하는 방식을 구현했다.
  • 실제 운영에서는 연결 한 건뿐 아니라 실패 복구, DTMF, 음질, FIFO 대기열과 상담 점유 수명주기까지 함께 설계해야 했다.

먼저 알아두면 좋은 말

이 글에는 통화 시스템 용어가 몇 번 등장한다. 아래 네 가지만 알고 읽으면 충분하다.

용어쉽게 말하면
Room한 통화가 진행되는 가상의 통화방
Participant통화방에 들어온 참여자. 여기서는 환자, AI, 상담원
Track참여자가 보내는 각각의 음성 길. 누가 들을 수 있는지도 따로 정할 수 있다.
DTMF전화 키패드의 1, 2처럼 버튼을 눌러 보내는 신호

self-hosted는 이 통화 시스템을 외부 클라우드에만 맡기지 않고, 서비스 팀이 직접 운영하는 방식을 뜻한다.


2. Cold Transfer와 Warm Transfer는 무엇이 다른가

전화 상담에서 사람에게 연결하는 방법은 크게 Cold Transfer와 Warm Transfer로 나뉜다.

Cold Transfer: 전화를 바로 넘긴다

Cold Transfer는 현재 전화를 다른 전화번호나 SIP endpoint로 곧바로 전달한다. LiveKit에서는 TransferSIPParticipant API가 SIP REFER를 통해 통신사에 전환을 요청하고, 성공하면 환자는 기존 LiveKit Room을 떠난다.

구조는 단순하지만, 연결 뒤의 상황을 AI가 챙기기 어렵다. 상담원이 받지 않거나 잘못된 부서로 연결되면 환자가 그 불편을 그대로 겪게 된다.

Warm Transfer: 사람에게 필요한 맥락까지 넘긴다

Warm Transfer에서는 AI가 환자를 바로 넘기지 않는다.

  1. 환자를 대기 상태로 전환한다.
  2. AI가 사람 상담원에게 별도로 전화를 건다.
  3. 상담원이 받으면 지금까지의 통화 내용을 브리핑한다.
  4. 상담원이 연결을 수락하는지 확인한다.
  5. 수락했을 때만 환자와 상담원을 연결한다.
  6. 연결에 실패하면 환자에게 돌아와 다음 방법을 안내한다.
구분Cold TransferWarm Transfer
AI의 역할전달 요청 후 종료상담원을 먼저 호출하고 맥락 전달
상담원 부재환자가 실패를 직접 경험AI가 실패를 처리하고 환자에게 복귀
통화 내용 전달별도 구현 필요연결 전에 상담원에게 브리핑
구현 복잡도비교적 낮음오디오 격리와 상태 관리 필요
적합한 상황대표번호·부서로 단순 전달의료·금융·고객지원처럼 맥락이 중요한 상담

LiveKit 공식 문서도 Cold Transfer를 SIP REFER 기반의 직접 전달, Warm Transfer를 AI가 컨텍스트와 실패 처리를 보조하는 연결로 구분한다. 참고: Transfers overview


3. LiveKit의 공식 WarmTransferTask

LiveKit Agents에는 Warm Transfer의 일반적인 수명주기를 자동화하는 prebuilt task가 있다.

from livekit.agents.beta.workflows import WarmTransferTask

result = await WarmTransferTask(
    sip_call_to=human_agent_phone,
    sip_trunk_id=outbound_trunk_id,
    chat_ctx=self.chat_ctx,
    ringing_timeout=30.0,
)

코드는 짧아 보이지만, 실제로는 여러 일을 순서대로 처리한다.

  • 상담원과 비공개로 대화하기 위한 별도 Room 생성
  • outbound SIP trunk를 이용한 상담원 호출
  • 연결 중 환자에게 대기음악 재생
  • 환자의 오디오 입출력 중지
  • 기존 대화 내용과 컨텍스트를 상담원에게 전달
  • 상담원이 수락하면 원래 환자 Room으로 이동
  • 거절과 음성사서함 감지, timeout 처리

상담원 측에서는 기본적으로 다음과 같은 Tool을 사용할 수 있다.

  • connect_to_caller: 환자와의 연결 수락
  • decline_transfer: 이유와 함께 연결 거절
  • voicemail_detected: 음성사서함 연결 처리

대기음악, 브리핑용 추가 instruction, DTMF 내선번호, ringing timeout, 사용자 정의 Tool도 설정할 수 있다. Python에서는 beta.workflows에 있으며, 현재 Node.js에서는 stable workflows namespace로 제공된다.

공식 Task가 채택한 핵심 아이디어는 Room을 프라이버시 경계로 사용하는 것이다. 환자와 상담원이 처음부터 같은 Room에 있지 않기 때문에, 브리핑이 환자에게 들리거나 환자 음성이 상담원에게 미리 전달되는 것을 구조적으로 막을 수 있다.

일반적인 Warm Transfer라면 이 Task를 먼저 검토하는 것이 합리적이다. LiveKit 역시 대부분의 사용 사례에서는 prebuilt task 사용을 권장한다. 참고: WarmTransferTask, Agent-assisted warm transfer


4. 문제는 우리가 LiveKit을 self-hosting하고 있었다는 것

우리가 Warm Transfer를 구현하기 시작했을 당시, 공식 WarmTransferTask 흐름은 LiveKit Cloud 사용을 전제로 했다. 공식 예제도 LiveKit Cloud 계정과 그 환경의 SIP 설정을 먼저 요구했다.

하지만 우리의 인바운드 Voice AI는 LiveKit Server, SIP, Agent worker와 관련 인프라를 직접 운영하는 self-hosted 환경이었다. 공식 Task를 가져와 호출하는 것만으로는 필요한 Warm Transfer 수명주기를 실행할 수 없었다.

그렇다고 Warm Transfer 자체를 포기할 필요는 없었다. 직접 운영하는 LiveKit에서도 통화방(Room), 참여자, SIP 전화 연결, 음성 구독 권한과 서버 API 같은 기본 기능은 쓸 수 있다. 따라서 선택지는 “기능을 포기한다”가 아니라 공식 Task가 조합하던 기능을 우리 환경에 맞게 다시 엮는 것이었다.

이 과정에서 요구사항도 더 구체적으로 정리됐다.

  • 별도 Consultation Room 없이 전체 통화를 하나의 Room에서 유지할 것
  • 통화 녹음과 대화 이벤트를 동일한 Room 수명주기에서 수집할 것
  • 상담원 수락을 LLM 판단이 아닌 DTMF 12로 결정적으로 처리할 것
  • 병원별 SIP trunk의 실제 사용 가능 상태를 외부 시스템과 함께 관리할 것
  • 여러 환자의 동시 연결 요청을 FIFO로 처리할 것
  • 실패 유형에 따라 재시도, AI 복구, 메모 남기기를 구분할 것
  • 연결 성공 후에도 실제 사람 상담이 끝날 때까지 자원 점유를 유지할 것

결과적으로 우리는 단순히 공식 Task를 복제하지 않았다. Self-hosted 환경과 병원 콜센터의 운영 정책을 함께 담은 SingleRoomWarmTransferTask를 만들었다.

Version note: LiveKit은 빠르게 발전하고 있다. 현재 공식 self-hosting 문서는 Agents framework와 SIP 자체를 self-hosted 환경에서도 지원한다고 설명한다. 이 글의 Cloud 제약은 우리가 이 기능을 설계하고 도입하던 시점의 상황이다. 새로 구현한다면 현재 사용 중인 LiveKit Agents 버전과 WarmTransferTask의 self-hosted 호환성을 다시 확인하는 것이 좋다.

참고: 공식 Warm Transfer 예제, LiveKit self-hosting overview


5. 하나의 Room에서 Warm Transfer를 수행한다

LiveKit의 공식 구조는 환자 통화방과 상담원 상담방을 나누고, 수락 뒤 상담원을 원래 통화방으로 이동시킨다. 우리의 구조는 환자, AI, 상담원이 처음부터 끝까지 하나의 LiveKit Room(통화방)에 함께 존재한다.

통화방이 하나이면 브리핑만을 위한 별도 공간은 없다. 대신 단계마다 누가 어떤 음성을 들을 수 있는지를 바꿔, 논리적으로 안전한 분리 공간을 만든다.

단일 Room을 선택한 이유는 다음과 같다.

  • Room 생성과 MoveParticipant 과정이 필요 없다.
  • 환자와 AI, 상담원의 전체 통화 기록을 같은 Room에서 수집할 수 있다.
  • 기존 AgentSession과 통화 컨텍스트를 그대로 유지할 수 있다.
  • 참가자의 연결과 종료 상태를 한곳에서 관찰할 수 있다.
  • 서비스가 원하는 오디오 전환 순서를 세밀하게 제어할 수 있다.

대신 Room이 제공하던 격리 책임을 애플리케이션이 직접 맡아야 한다. Subscription 하나를 잘못 열면 브리핑 중 환자 음성이 상담원에게 들리거나, 상담원의 인사말이 대기 중인 환자에게 먼저 들릴 수 있다.


6. 핵심은 “누가 무엇을 듣는가”다

Warm Transfer에서는 Participant의 존재 여부보다 오디오 경로가 더 중요하다.

단계환자가 듣는 것상담원이 듣는 것AI가 듣는 것
기존 AI 상담AI 음성참여하지 않음환자 음성
상담원 호출대기음악연결음 또는 무음입력 처리 중지
상담원 브리핑대기음악AI 브리핑입력 처리 중지
연결 전환대기음악 → 상담원브리핑 → 환자양쪽 구독 해제
연결 완료상담원 음성환자 음성상태만 모니터링
연결 실패AI 음성연결 종료환자 음성 처리 재개

이를 구현하기 위해 두 가지 제어 계층을 사용한다.

6.1 AI가 발행한 Track의 구독 권한

local_participant.set_track_subscription_permissions()는 AI가 발행한 Track을 누가 들을 수 있는지 제어한다.

브리핑 단계에는 AI가 두 종류의 오디오를 발행한다.

  • AI voice track: 상담원에게 전달할 브리핑
  • hold music track: 환자에게 들려줄 대기음악

권한은 다음처럼 분리된다.

환자   → hold_music track만 구독
상담원 → AI voice track만 구독

같은 AI Participant가 발행하는 오디오라도 Track SID 단위로 허용 목록을 다르게 구성한다.

6.2 원격 Participant가 발행한 Track의 구독 제어

set_track_subscription_permissions()는 AI 자신의 Track에만 적용된다. 상담원이 발행하는 Track을 환자가 듣지 못하게 하려면 LiveKit Server API의 update_subscriptions()가 필요하다.

또한 상담원 브리핑 중 환자 음성이 새어나가지 않도록 환자가 발행한 오디오를 mute_published_track()으로 차단한다.

LiveKit은 Participant가 Room에 입장하면 다른 Track을 자동 구독할 수 있다. 상담원이 새 audio track을 발행하는 순간 환자가 자동으로 구독할 가능성이 있으므로 두 가지 방어가 필요했다.

  1. 이미 발행된 상담원 Track을 즉시 차단한다.
  2. track_published 이벤트를 감시해 이후 생성되는 Track도 차단한다.

Single-Room 방식은 단순히 통화방 하나를 덜 만드는 최적화가 아니다. 별도 통화방이 제공하던 안전 경계를 음성 권한으로 직접 구현하는 설계다.


7. 정상 연결은 다섯 단계로 진행된다

1단계. 환자를 안전하게 대기시킨다

Warm Transfer가 시작되면 AI는 환자에게 연결 안내를 재생한 뒤 다음 작업을 수행한다.

  • AI의 일반 voice track 구독 차단
  • AgentSession의 오디오 입력 처리 중지
  • 환자가 발행한 오디오 mute
  • 대기음악 시작
  • 환자에게 hold music track만 허용

환자는 Room을 떠나지 않지만, 대화에서는 잠시 분리된다.

2단계. 요약 만들기와 상담원 호출을 함께 시작한다

상담원에게 전화를 건 다음 요약을 생성하면 대기시간이 길어진다. 반대로 요약이 끝난 뒤 전화를 걸면 SIP ringing 시간이 그대로 추가된다.

그래서 두 작업을 동시에 시작한다.

브리핑에는 환자의 신규·재진 여부, 이름, 지금까지의 대화 요약과 DTMF 안내를 포함한다. 요약 생성에 실패하거나 재시도하는 경우를 위해 만들어진 브리핑은 TransferRecord에 보관해 재사용할 수 있다.

3단계. 상담원에게만 상황을 설명한다

상담원이 SIP 전화를 받았다고 해서 즉시 브리핑을 재생하지 않는다. Participant가 연결된 시점과 실제 audio track이 발행된 시점에는 차이가 있을 수 있기 때문이다. 상담원 오디오가 준비될 때까지 기다린 뒤 다음과 같은 메시지를 재생한다.

AI 인바운드 전화입니다. 환자는 정형외과 예약 변경을 요청했고, 다음 주 수요일 오후 시간을 문의했습니다. 환자와 연결하려면 1번, 연결하지 않으려면 2번을 눌러주세요.

이때 환자는 계속 대기음악만 듣는다.

4단계. 버튼 입력으로 연결 여부를 확정한다

상담원의 결정을 LLM이 해석하게 하지 않고 DTMF로 받는다.

  • 1: 환자와 연결
  • 2: 거절 또는 콜백 처리
  • 무응답: timeout
  • 유효하지 않은 입력: 재안내 후 제한 횟수 초과 시 거절

DTMF 이벤트가 환자에게서 들어올 수도 있으므로 Participant identity를 확인해 상담원의 입력만 처리한다. 브리핑 도중 상담원이 미리 키를 누르는 early DTMF도 별도로 기록하고 처리한다.

5단계. 환자와 상담원을 연결한다

수락 후 다음 순서로 오디오를 전환한다.

  1. DTMF tone이 audio buffer에서 사라질 때까지 짧게 기다린다.
  2. 환자 audio track의 mute를 해제한다.
  3. 환자와 상담원 Track 구독을 양방향으로 활성화한다.
  4. 새 상담원 Track을 차단하던 이벤트 리스너를 제거한다.
  5. AI voice와 hold music track의 구독을 차단한다.
  6. AI가 환자와 상담원 Track 구독에서 빠진다.
  7. 대기음악 player를 정리한다.

연결이 완료되면 AI는 대화에 참여하지 않는다. 다만 Room에 남아 환자나 상담원이 통화를 종료하는지 모니터링하고, 나머지 Participant와 후속 자원을 정리한다.


8. 오디오 전환에서는 최종 상태보다 순서가 중요하다

개발 중 대표적으로 마주친 문제는 대기음악에서 상담원 음성으로 바뀌는 순간의 잡음이었다.

대기음악을 먼저 멈추고 상담원 Track을 나중에 활성화하면 다음과 같은 구간이 생긴다.

대기음악 → 무음 → 상담원 음성

짧은 무음과 오디오 파형 절단은 환자에게 “찌지직” 같은 잡음이나 끊긴 연결처럼 느껴질 수 있다.

우리는 상담원 음성을 먼저 활성화하고, 짧은 overlap 구간을 만든 뒤 대기음악을 제거하도록 순서를 바꿨다.

대기음악 → 대기음악 + 상담원 음성 → 상담원 음성

DTMF에도 비슷한 문제가 있었다. 상담원이 1을 누른 직후 연결을 열면 인밴드 DTMF tone이 오디오 버퍼에 남아 환자에게 “삐” 소리가 전달될 수 있다. 그래서 수락 직후 짧은 소거 시간을 둔 뒤 구독을 활성화한다.

이 경험이 보여주는 원칙은 간단하다.

실시간 음성 시스템에서는 시작과 끝 상태만 맞아서는 부족하다. A에서 B로 바뀌는 순서와 그 사이의 짧은 순간도 통화 경험의 일부다.


9. 실패는 예외가 아니라 상태다

Warm Transfer는 상담원이 받는 정상 경로만 구현해서는 완성되지 않는다.

SingleRoomWarmTransferTask는 결과를 다음과 같은 명시적 상태로 반환한다.

  • SUCCESS: 상담원이 수락하고 환자와 연결됨
  • DECLINED: 상담원이 거절하거나 DTMF 없이 종료함
  • TIMEOUT: 상담원 응답 또는 DTMF 입력이 시간 내에 없음
  • VOICEMAIL: 음성사서함으로 연결됨
  • CALLER_DISCONNECTED: 대기 중 환자가 통화를 종료함
  • ERROR: 복구 가능한 연결 오류가 발생함

연결에 실패하면 다음 순서로 환자와 AI의 대화를 복원한다.

  1. 대기음악을 정리한다.
  2. AI Track의 기본 구독 권한을 복원한다.
  3. 환자가 발행한 audio track의 mute를 해제한다.
  4. AgentSession의 오디오 입력과 STT를 다시 활성화한다.
  5. 재시도하거나 메모를 남길 수 있도록 다음 흐름으로 이동한다.

환자 Track 자체의 구독을 직접 끊지 않은 것도 복구를 고려한 결정이었다. 구독을 해제하면 LiveKit Agents의 room_io 오디오 파이프라인이 끊겨 다시 연결되지 않는 상황이 생길 수 있었다. 대신 세션 레벨에서 입력 처리만 중지해, 실패 시 set_audio_enabled(True)로 빠르게 복원할 수 있도록 했다.

Cleanup도 멱등하게 설계했다. 정상 종료, timeout, Participant disconnect와 예외 경로가 겹쳐도 대기음악 정지와 이벤트 리스너 제거가 안전하게 여러 번 호출될 수 있어야 한다.


10. 한 건의 Warm Transfer와 여러 건의 Warm Transfer는 다른 문제다

SingleRoomWarmTransferTask는 한 통화에서 환자와 상담원을 안전하게 연결한다. 하지만 실제 운영에서는 여러 환자가 동시에 같은 상담원 또는 전화 회선을 요청한다.

별도의 조정 없이 각 통화가 상담원에게 전화를 걸면 다음 문제가 생긴다.

  • 먼저 요청한 환자보다 나중 요청한 환자가 먼저 연결될 수 있다.
  • 사용 중인 trunk에 여러 세션이 동시에 dial을 시도할 수 있다.
  • 실패한 세션이 재시도하는 동안 새로운 세션과 순서가 뒤섞인다.
  • 연결 성공 직후 대기열에서 제거하면 상담 중인 자원을 다음 환자가 사용할 수 있다고 오인한다.

이를 해결하기 위해 병원 단위의 Redis ZSET FIFO 대기열을 도입했다.

동작 원칙은 다음과 같다.

  1. Warm Transfer 요청을 병원별 queue에 등록한다.
  2. 자신의 rank가 0일 때만 Trunk Allocation API를 확인한다.
  3. trunk가 사용 가능할 때만 상담원에게 전화를 건다.
  4. timeout이나 일시적 오류에서는 선두 자리를 유지하며 재시도한다.
  5. 상담원이 명시적으로 거절하거나 환자가 이탈하면 queue에서 제거한다.
  6. 연결에 성공해도 즉시 제거하지 않는다.
  7. 환자와 상담원의 실제 대화가 끝났을 때 다음 환자에게 순서를 넘긴다.

여기서 가장 중요한 정책은 다음 한 문장으로 정리된다.

SUCCESS는 자원 점유의 끝이 아니라, 사람 상담 점유의 시작이다.

대기열 맨 앞 항목에는 만료 시간을 계속 갱신하고, 비정상 종료로 정보가 사라진 오래된 항목은 정리한다. 같은 통화가 중복으로 들어가는 일도 막아 기존 순서를 지킨다.

결국 SingleRoomWarmTransferTask가 한 건의 연결을 책임진다면, Redis queue와 Trunk Allocation API는 여러 건의 연결이 서로 충돌하지 않게 책임진다.


11. 공식 Task와 Self-Hosted 구현 비교

항목LiveKit WarmTransferTask우리의 Single-Room 구현
주요 목표일반적인 Agent-assisted transfer 자동화Self-hosted 병원 운영 정책 통합
Room 구조환자 Room + 상담원 Consultation Room하나의 Room
오디오 격리Room 분리Track Subscription과 server-side 제어
상담원 연결수락 후 환자 Room으로 이동같은 Room에서 양방향 구독 활성화
대기음악기본 제공, 사용자 정의 가능queue 단계부터 수명주기 직접 관리
브리핑chat_ctx와 instruction 전달별도 LLM 요약 생성 및 캐시
상담원 결정Tool 기반 수락·거절·voicemailDTMF 1·2 기반 결정
실패 처리caller conversation 복귀상태별 재시도·복구·메모 수집
동시 요청애플리케이션에서 별도 관리Redis FIFO와 Trunk Allocation API
자원 점유단일 Task의 연결 수명주기사람 상담 종료까지 점유 유지
구현 부담낮음높음
당시 적용 환경우리 도입 시점에는 Cloud 전제Self-hosted 인프라에 맞춤

Single-Room 방식이 언제나 더 좋은 것은 아니다. 공식 WarmTransferTask를 사용할 수 있고 일반적인 상담원 연결이면 검증된 prebuilt workflow를 사용하는 편이 구현과 유지보수 비용이 낮다.

반대로 직접 운영하는 환경이거나, 통화방 이동 없이 전체 기록을 유지해야 하거나, 음성을 세밀하게 제어하고 병원별 대기열 정책이 필요하다면 맞춤형 흐름이 필요할 수 있다.

좋은 기준은 “우리가 직접 만들 수 있는가”가 아니라 공식 Task가 해결하지 못하는 요구사항이 무엇인가다.


12. 구현하면서 얻은 교훈

12.1 Warm Transfer는 전화 API가 아니라 상태 머신이다

SIP 전화를 거는 것은 전체 과정의 일부일 뿐이다. 환자 격리, 상담원 연결, 브리핑, 수락, 복구와 종료가 하나의 상태 머신으로 움직여야 한다.

12.2 오디오 권한은 보안 경계다

브리핑 중 환자의 음성이 새어나가거나 상담원 음성이 미리 들리는 것은 단순한 UX 문제가 아니다. 의료 상담에서는 개인정보와 신뢰의 문제다. Track Subscription은 편의 기능이 아니라 통화 단계별 접근 제어로 다뤄야 한다.

12.3 상태뿐 아니라 전환 순서를 테스트해야 한다

테스트에서 최종적으로 환자와 상담원이 연결됐다는 사실만 확인하면 부족하다. 대기음악이 언제 멈췄는지, DTMF tone이 들리지 않았는지, 브리핑이 환자에게 유출되지 않았는지까지 검증해야 한다.

12.4 실패 복구는 성공 경로와 함께 설계해야 한다

나중에 timeout과 disconnect를 덧붙이면 mute나 subscription이 복원되지 않는 경로가 생기기 쉽다. 각 단계에 진입할 때 실패하면 무엇을 되돌릴지 함께 정의해야 한다.

12.5 연결 성공과 업무 완료를 구분해야 한다

SIP 연결 성공은 상담원이 전화를 받았다는 뜻일 뿐이고, DTMF 수락은 환자와 연결할 준비가 됐다는 뜻이다. Warm Transfer 성공 후에도 실제 사람 상담이 끝날 때까지는 trunk와 queue의 점유가 계속된다.


13. 마치며: 좋은 전환은 복잡함을 감춘다

Warm Transfer의 목적은 AI 통화를 사람 통화로 바꾸는 것이 아니다. AI가 수집한 맥락을 사람에게 안전하게 넘기고, 환자가 같은 설명을 반복하지 않게 하며, 실패하더라도 통화 경험이 끊기지 않게 하는 것이다.

LiveKit의 공식 WarmTransferTask는 이 문제의 표준적인 해법을 제공한다. 별도 Room, 대기음악, 컨텍스트 전달, 상담원 결정과 통화 병합을 하나의 Task로 추상화한다.

우리는 self-hosted 환경이라는 제약 때문에 그 추상화를 그대로 사용할 수 없었다. 대신 LiveKit이 제공하는 Room, SIP Participant, Track Subscription과 Server API를 조합해 같은 목적을 수행하는 Single-Room workflow를 만들었다. 그리고 실제 병원 서비스에 필요했던 DTMF 수락, 오디오 전환 순서, 실패 복구, Redis FIFO와 상담 점유 정책을 그 위에 추가했다.

내부적으로는 여러 Participant와 Track, 비동기 이벤트와 분산 상태가 움직인다. 하지만 사용자에게 보여야 하는 경험은 단순하다.

환자는 잠시 음악을 듣고, 상담원은 필요한 맥락을 전달받고, 두 사람은 자연스럽게 대화를 시작한다.

좋은 Warm Transfer는 그 뒤의 복잡한 상태 전환을 사용자가 전혀 눈치채지 못하게 만드는 것이다.


References