IVR을 넘어: 설정 기반 Voice AI Agent 설계하기


병원과 의원의 전화는 단순하지 않다. 예약, 변경, 취소, 진료 시간 문의, 길 안내, 보험 관련 질문처럼 서로 다른 목적의 통화가 끊임없이 들어온다. 전통적인 IVR은 이런 요청을 안정적으로 분기하지만 유연하지 않고, 순수 LLM 음성 에이전트는 자연스럽지만 운영자가 원하는 흐름을 항상 보장하기 어렵다.

실제 서비스에 필요한 것은 둘 중 하나가 아니라 결정론적인 흐름 제어와 대화형 AI의 결합이었다. 이 글에서는 병원 콜센터에 적용한 설정 기반 Voice AI Agent 구조를 중심으로, 두 접근법의 장점을 함께 가져오는 방법을 정리한다.

핵심 요약

  • IVR은 예측 가능하지만 경직되어 있고, 순수 LLM 에이전트는 유연하지만 실행 결과를 보장하기 어렵다.
  • JSON 기반 flow_config가 통화의 순서와 분기를 선언한다.
  • SupervisorAgent는 그래프를 순회하며 각 노드를 결정론적으로 실행한다.
  • 자연어 이해가 필요한 순간에만 예약·안내 같은 전문 LLM 에이전트로 제어권을 넘긴다.
  • DTMF와 자유 음성 방식을 하나의 아키텍처에서 함께 사용할 수 있다.

두 극단 사이의 문제

전통적인 IVR

“예약은 1번, 원무과는 2번”으로 대표되는 IVR은 단순하고 예측 가능하다. 같은 입력은 언제나 같은 결과로 이어지므로 테스트와 감사도 쉽다. 하지만 메뉴에 없는 요청을 처리하지 못하고, 작은 변경에도 개발과 배포가 필요하다. 통화자는 자신에게 맞는 선택지를 찾기 위해 긴 안내를 반복해서 들어야 한다.

순수 LLM 음성 에이전트

LLM은 통화자의 자유로운 표현을 이해하고 자연스럽게 대화할 수 있다. 반면 프로덕션 전화 시스템은 다음과 같은 강한 보장을 요구한다.

  • 인사말은 통화자의 발화를 받기 전에 반드시 재생되어야 한다.
  • 특정 조건을 만족하지 않으면 상담원에게 연결해서는 안 된다.
  • DTMF 1 입력은 항상 예약 업무로 연결되어야 한다.
  • 운영팀이 전체 통화 흐름을 검토하고 변경할 수 있어야 한다.

LLM의 모든 응답과 판단은 생성 결과다. 따라서 자유로운 LLM 하나에 전체 통화 흐름을 맡기면, 대화는 자연스러워도 중요한 분기와 실행 순서를 완전히 보장하기 어렵다.

중간 지점: 선언형 흐름과 전문 에이전트

해결책은 통화 전체를 설정으로 표현한 그래프와 이를 실행하는 런타임 감독자를 두는 것이다.

dial_info


CallSessionData

    ├── business_data      병원 정보, 운영 시간
    ├── recipient_data     통화자 정보
    ├── call_config_data   언어, 음성, 전환 설정
    ├── flow_config        통화 흐름 그래프
    └── agents_data        사용 가능한 AI 에이전트


      SupervisorAgent

    ┌───────┼────────┐
    ▼       ▼        ▼
  조건     안내     액션

             필요한 순간에만

       BookingAgent / InfoAgent

역할은 명확히 나뉜다.

구성 요소책임주된 관리 주체
flow_config무엇을 어떤 순서로 실행할지 선언제품·운영팀
SupervisorAgent각 노드를 어떻게 실행할지 결정엔지니어링팀
전문 AI Agent열린 대화에서 무엇을 말하고 처리할지 결정LLM 및 도구

운영 시간에 따른 분기, 안내 메뉴, 상담원 연결 번호는 설정으로 바꿀 수 있다. 예약 날짜를 이해하거나 복잡한 질문에 답하는 일은 전문 AI 에이전트가 맡는다. 민감한 라우팅 결정은 AI에게 넘어가기 전에 이미 SupervisorAgent가 끝낸다.

통화를 구성하는 다섯 가지 노드

flow_config는 방향성이 있는 노드 그래프다. 각 노드는 하나의 역할만 담당한다.

1. condition: 데이터 기반 분기

조건 노드는 세션의 값을 읽고 다음 노드를 결정한다. 음성 재생도, LLM 호출도 없는 순수한 규칙 실행이다.

{
  "id": "check_work_hours",
  "type": "condition",
  "field": "business_data.is_work_time",
  "branches": {
    "true": "greeting_open",
    "false": "greeting_closed"
  }
}

예를 들어 현재가 진료 시간이라면 기본 안내를 재생하고, 진료 시간이 아니라면 휴무 안내 후 통화를 종료할 수 있다.

2. greeting: 안내와 DTMF 입력

안내 노드는 TTS 메시지를 재생한다. input_methoddtmf라면 키 입력을 기다린 뒤 설정된 목적지로 이동한다.

{
  "id": "main_menu",
  "type": "greeting",
  "input_method": "dtmf",
  "message": "예약은 1번, 병원 안내는 2번을 눌러주세요.",
  "dtmf_options": {
    "1": {
      "next": "booking_agent",
      "user_request": "진료 예약"
    },
    "2": {
      "next": "info_agent",
      "user_request": "병원 안내"
    },
    "*": { "next": "main_menu" }
  }
}

여기서 user_request가 중요한 역할을 한다. 통화자가 이미 “예약”을 선택했다면 BookingAgent가 다시 “무엇을 도와드릴까요?”라고 묻지 않아도 된다. 에이전트는 “예약을 도와드리겠습니다. 희망 날짜가 언제인가요?”처럼 바로 다음 단계로 진입한다.

3. action: 전화 시스템의 실제 동작

액션 노드는 상담원 연결이나 로그 기록처럼 외부에 영향을 주는 작업을 실행한다.

액션동작
transfer전역 설정의 상담원 번호로 연결
transfer_direct노드에 지정된 번호로 직접 연결
log세션 로그를 기록하고 다음 노드로 이동

상담원 연결은 다시 두 방식으로 나뉜다.

  • Warm transfer: 먼저 상담원에게 통화 내용을 요약한 뒤 고객을 연결한다.
  • Cold transfer: 별도 설명 없이 고객을 바로 연결한다.

전환이 끝나면 그래프 순회도 종료된다. 이후 통화의 제어권은 사람에게 있다.

4. agent: 전문 AI 에이전트로 위임

에이전트 노드는 SupervisorAgent에서 대화형 AI로 제어권을 넘긴다.

{
  "id": "booking_agent",
  "type": "agent",
  "name": "예약 에이전트"
}

node.id는 세션 시작 시 등록된 에이전트 키와 정확히 일치해야 한다. booking_agentBookingAgent처럼 이름이 어긋나면 런타임에서 에이전트를 찾지 못하고 흐름이 멈출 수 있다. 설정을 배포하기 전에 노드와 에이전트 레지스트리의 참조 무결성을 검증해야 하는 이유다.

현재 구조에서 위임은 단방향이다. 전문 에이전트가 활성화되면 SupervisorAgent는 종료되고 그래프로 돌아오지 않는다. 예약 처리 후 병원 위치를 묻는 것과 같은 후속 요청은 전문 에이전트끼리 전달하거나 해당 에이전트가 직접 처리해야 한다.

5. exit: 안전하고 명시적인 종료

종료 노드는 마지막 안내를 재생하고 세션을 닫는다.

{
  "id": "end_call",
  "type": "exit",
  "message": "전화 주셔서 감사합니다. 좋은 하루 보내세요."
}

명시적인 종료 노드를 두면 통화가 왜 끝났는지 기록할 수 있고, 흐름에 목적지 없는 분기가 생기는 것도 방지할 수 있다.

SupervisorAgent는 그래프를 어떻게 실행하는가

SupervisorAgent의 핵심은 간단하다. 현재 노드를 찾고, 타입에 맞는 핸들러를 실행하고, 반환된 다음 노드로 이동한다.

process_node(node_id):
    node = flow_config.get_node(node_id)
    if node가 없으면:
        오류를 기록하고 종료

    next_node_id = dispatch(node)

    if next_node_id가 있으면:
        await process_node(next_node_id)

구현에서는 비동기 재귀를 사용했다. 각 노드가 TTS 재생, DTMF 대기, 전화 전환처럼 실제 I/O를 await하기 때문에, 핸들러가 작업을 끝내고 다음 노드로 자연스럽게 제어를 넘길 수 있다. 일반적인 콜 플로우 깊이는 몇 단계 수준이라 재귀 깊이도 문제가 되지 않았다.

오디오 입력 게이트

음성 시스템에서는 언제 마이크 입력을 처리할지가 매우 중요하다. 인사말이 재생되는 동안 통화자의 목소리가 LLM에 들어가면 안내가 중단되거나 의도하지 않은 행동이 시작될 수 있다.

통화 시작


[오디오 입력 OFF]

   ├── 안내 TTS 재생
   ├── DTMF 입력 처리
   └── 에이전트 노드 도달


       전문 AI Agent 활성화


       [오디오 입력 ON]

SupervisorAgent가 동작하는 동안에는 마이크 입력을 막고, 전문 에이전트가 자신의 시작 안내를 끝낸 뒤 입력을 다시 활성화한다. 단순해 보이지만 실제 운영에서는 이 타이밍을 맞추는 일이 가장 까다로운 부분 중 하나였다.

하나의 엔진, 두 가지 상호작용 방식

DTMF 방식

통화자가 키를 눌러 이동한다. 라우팅 결과가 명확하고 세션 로그에 SELECTED_KEY=1처럼 남기기 쉬우며, 음질이 좋지 않아도 안정적이다.

다만 메뉴가 길어질수록 경험이 나빠지고, 미리 정의하지 않은 의도를 표현하기 어렵다. 유효한 입력이 일정 횟수 안에 들어오지 않으면 무한 반복 대신 오류 안내 후 통화를 안전하게 종료해야 한다.

자유 음성 방식

간단한 인사말 다음에 TriageCoordinator를 활성화한다. 통화자가 “다음 주 화요일 예약을 취소하고 싶어요”라고 말하면 의도를 해석해 예약 에이전트로 바로 보낼 수 있다.

자연스럽지만 STT와 LLM 판단에 의존하므로 음질과 모델의 불확실성에 영향을 받는다. 규제가 강하거나 라우팅 결과를 명확히 설명해야 하는 단계에는 신중하게 적용해야 한다.

비교 항목DTMF자유 음성
라우팅 기준키 입력과 설정값STT와 LLM 의도 해석
결정성높음확률적
음질 민감도낮음높음
사용자 경험익숙하고 빠름자연스럽고 자유로움
감사 가능성선택 키가 명확히 기록됨판단 근거 기록을 별도로 설계해야 함

두 방식을 반드시 하나만 선택할 필요는 없다. 상위 분기는 DTMF로 명확하게 처리하고, 전문 업무에 진입한 뒤 자유로운 대화를 허용하는 혼합 방식이 현실적인 기본값이 될 수 있다.

운영 환경에서 배운 것

잘 작동했던 부분

운영 변경이 코드에서 분리됐다. 휴일 안내, 메뉴 추가, 상담원 번호 변경을 설정 수정으로 처리할 수 있었다. 설정을 API나 콜 메타데이터로 주입하면 애플리케이션 배포 없이도 흐름을 바꿀 수 있다.

노드의 책임이 작고 명확했다. 조건 노드는 분기만 하고, 안내 노드는 음성 재생과 입력만 처리한다. 문제 발생 시 확인해야 할 범위가 작아 테스트와 디버깅이 쉬웠다.

전문 에이전트를 재사용할 수 있었다. DTMF 경로와 자유 음성 경로가 결국 같은 에이전트 노드에서 만난다. 에이전트는 통화자가 어떤 방식으로 자신에게 도착했는지 알 필요가 없다.

선택한 의도를 미리 전달해 반복 질문을 줄였다. user_request를 공유 세션에 저장하면 통화자가 이미 제공한 정보를 에이전트가 다시 묻지 않는다.

특히 까다로웠던 부분

TTS, DTMF, 마이크의 타이밍이 복잡했다. 메뉴를 아는 통화자는 안내가 끝나기 전에 키를 누른다. 유효한 DTMF 이벤트가 들어오면 TTS를 중단하고 즉시 이동하되, 오래된 이벤트가 다음 단계에 영향을 주지 않게 해야 한다.

설정과 에이전트 레지스트리의 결합이 런타임 오류를 만들었다. 문자열 키 불일치는 코드 컴파일 단계에서 잡히지 않는다. 배포 전 그래프 검증, 존재하지 않는 목적지 확인, 도달 불가능한 노드 탐지 같은 정적 검사가 필요하다.

단방향 위임에는 명확한 소유권 규칙이 필요했다. 전문 에이전트가 자신의 주 업무를 끝낸 뒤 다른 요청을 받았을 때 직접 처리할지, 다른 에이전트로 넘길지 정책을 미리 정해야 한다.

공유 상태는 편리하지만 변경 범위를 통제해야 했다. 모든 노드와 에이전트가 같은 CallSessionData를 사용하면 컨텍스트 전달은 쉽지만, 어느 구성 요소든 상태를 바꿀 수 있다. 변경 가능한 필드를 제한하고 구조화된 로그를 남기는 편이 안전하다.

관측 가능성도 아키텍처의 일부다

SupervisorAgent는 각 단계에서 구조화된 태그를 세션 기록에 남긴다.

[RECIPIENT_DATA: USER_NAME=..., PHONE=...]
[GREETING: STATUS=STARTED]
[GREETING: STATUS=COMPLETED]
[GREETING_DTMF_TASK: SELECTED_KEY=1]
[CALL_TERMINATION: ACTOR=USER, REASON=EXPLICIT_REQUEST]

통화가 잘못 연결되거나 안내가 중간에 끊겼을 때 이 기록을 따라가면 어느 노드에서 어떤 입력을 받았는지 재구성할 수 있다. 음성 AI에서 관측 가능성은 사후 편의를 위한 기능이 아니라 운영 안정성을 위한 핵심 설계다.

이 패턴은 언제 적합한가

다음 조건이 많을수록 설정 기반 감독자 패턴이 잘 맞는다.

  • 예약, 안내, 상담원 연결처럼 라우팅 목적지가 여러 개다.
  • 결정론적 단계와 AI가 필요한 단계가 섞여 있다.
  • 제품·운영팀이 개발 배포 없이 흐름을 바꿔야 한다.
  • 라우팅 결정의 감사 기록이 필요하다.
  • TTS, DTMF, SIP 전환이 포함된 실시간 전화 시스템이다.

반대로 하나의 에이전트가 모든 요청을 충분히 처리하고 별도 분기가 없다면 이 구조는 과할 수 있다. 조건식과 반복문이 매우 복잡해져 JSON이 작은 프로그래밍 언어처럼 변하기 시작한다면 코드 기반 상태 머신이 더 적합할 수도 있다. 설정 자체를 리뷰하고 버전 관리하는 운영 절차가 없는 조직에서도 장점이 줄어든다.

마무리

프로덕션 Voice AI의 핵심 긴장은 분명하다. LLM은 유연하지만 예측하기 어렵고, 규칙 기반 시스템은 예측 가능하지만 경직되어 있다. flow_configSupervisorAgent의 결합은 어느 한쪽을 포기하는 대신 각자 잘하는 영역을 분리한다.

SupervisorAgent는 실행 순서, 조건 분기, 오디오 게이트, 상담원 전환처럼 보장이 필요한 부분을 담당한다. 전문 AI 에이전트는 통화자의 의도를 이해하고 자연스럽게 응답하며 실제 업무를 처리한다.

결과적으로 운영자는 JSON 설정으로 메뉴와 라우팅을 바꿀 수 있고, 엔지니어는 전체 흐름을 건드리지 않고 새 전문 에이전트를 추가할 수 있다. 무엇보다 통화자에게는 이미 말한 내용을 반복하지 않고 곧바로 필요한 도움을 제공할 수 있다.

의료, 고객 지원, 예약, 금융처럼 라우팅 복잡도와 운영 책임이 모두 큰 Voice AI를 만들고 있다면, 하나의 만능 에이전트를 선택하기 전에 이 구조를 고려해볼 만하다.