이 글은 JudyAI Lab의 AI 엔지니어링 시리즈 중 하나입니다 — 100편 이상 발행된 가이드, 60개국 5,000명 이상의 주간 독자가 읽는 콘텐츠로, AI 에이전트·트레이딩 시스템·콘텐츠 파이프라인의 실전 운영에 초점을 둡니다.
Day 1을 쓰고 나서, Day 2를 기다렸다 — Voice Agent.
솔직히 Voice Agent는 이번 전체 시스템 중 내가 가장 긴장했던 부분이다.
다른 에이전트들은 이메일 쓰고, 데이터 조회하고, 분류 처리를 하는데 최악의 경우라야 내용이 평범하게 나와서 Tone Coach에 막혀 다시 쓰면 그만이다. 하지만 Voice Agent는 진짜로 전화를 거는 녀석이다. 걸고 나면 상대방이 받고, Sarah의 목소리가 들리고, 내가 쓴 스크립트를 말하고, 내가 설계한 플로우로 결정을 내린다.
중간에 어느 한 곳이라도 틀리면, 상대방은 전화를 끊고 나서 ‘AI가 이상했다’고 말하지 않는다 — 그냥 ‘그 회사 진짜 별로더라’고 기억한다.
수금 시나리오는 이 점에서 특히 민감하다. 상대방은 원래부터 기분이 좋지 않으니까. 정말로 현금 흐름 문제가 있을 수도 있고, 거래처와 분쟁 중일 수도 있고, 입금을 기다리는 중일 수도 있다. 거기다 AI가 실수 하나 더 얹으면 그 관계를 완전히 망가뜨리는 마지막 도화선이 된다.
그래서 Day 2 내내 두 가지를 생각했다:
첫째, Voice Agent는 양방향이어야 한다. 단방향이 아니라.
둘째, 양방향이라면 사람이 실제로 하는 말을 이해할 수 있어야 한다.
단방향 음성이 부족한 이유
초기 Voice Agent 설계는 ‘우리가 전화해서 말하고 끊는다’ 방식이었다 — 음성 버전 알림 메시지 같은 것.
그런데 Day 55 last-chance call 구간을 쓰다가 스크립트를 한 번 읽어봤더니 뭔가 이상했다.
스크립트에서 Sarah가 고객에게 이렇게 말한다:
「I know things come up. We can work out a payment plan if you need.」 (때로 갑작스러운 일이 생기죠. 필요하시면 상환 계획을 함께 세울 수 있습니다.)
근데 문제는, 이 말을 한 다음에 어떻게 되냐는 거다.
고객이 전화기 너머에서 “네, 다음 주에 드릴게요”, “상사에게 먼저 물어봐야 해요”, “사실 지금 현금 흐름에 문제가 생겼어요"라고 말한다면 — 단방향 음성은 그걸 듣지 못하는 거 아닌가?
그러면 “we can work out a payment plan"이라는 말은 그냥 립서비스고, 다음 순간 끊어버리는 건데?
이 설계 모순은 D-027 결정에서부터 이미 깔려 있었다. 설계 의도 자체가 대화를 전제로 하기 때문에 음성은 반드시 양방향이어야 했다. Day 2 업그레이드의 목표가 바로 이것이었다.
ElevenLabs Conversational AI — Day -1에 심어둔 복선
Day -1(6/11)에 ElevenLabs 계정을 만들 때, 나는 ElevenAgents — Conversational AI 기능을 선택했다.
당시 J가 “free tier 15분으로 dev playground 돌리면 실제 분을 소모하지 않으니, 진짜 전화 걸 때가 되면 $6 Starter로 업그레이드하면 돼"라고 했다.
그래서 그대로 했다.
Day 2에 처음 한 일은 ElevenLabs 플랫폼에서 ConvAI Agent 전체를 구성하는 것이었다.
선택한 목소리는 Sarah — voice ID EXAVITQu4vr4xnSDxMaL, ElevenLabs 기본 제공 premade voice, 무료, “mature, reassuring, confident female US-EN"으로 포지셔닝되어 있다.
몇 가지 목소리를 시청해보고 최종적으로 Sarah를 고른 이유는 단순하다: 내가 직접 받고 싶은 전화 목소리였다.
너무 어리지 않고, 과하게 상냥하지 않고, 냉랭한 고객 서비스 기계 목소리도 아니다. 미국 중형 은행에 전화했을 때 연결될 법한 중년 여성 팀장 같은 목소리 — 차분하고, 온기가 있으면서, 전문적인 거리감도 있는.
수금 시나리오에는 이런 목소리가 필요하다.
너무 어리면 상대방이 회사 전문성을 의심하고, 너무 딱딱하면 압박감을 주고, Sarah는 딱 그 중간이었다.
18개 동적 변수 — Sarah가 이 고객을 ‘아는 것처럼’ 만드는 방법
ConvAI Agent 프롬프트는 고정되어 있지만, 통화마다 고객이 다르다 — 회사명, 미수 금액, 연체 일수, 패턴이 모두 다르다.
Sarah가 특정 고객과 실제로 대화하는 것처럼 들리려면, 통화 전에 ‘이 케이스의 맥락’을 주입해야 한다.
ElevenLabs의 방식은 동적 변수(dynamic variable) — 프롬프트에 {{customer_first_name}}, {{invoice_amount}}, {{days_past_due}}를 써두고, 전화를 걸기 전 API 호출 시 이 변수들을 넘기면 Sarah가 대화 중 자동으로 해당 값을 말한다.
최종적으로 변수가 18개까지 쌓였다.
고객 성, 회사명, 미수 잔액, 연체 일수, pattern_tag(이 고객의 과거 연체 패턴), recent_excuse(최근 말한 핑계) 등을 포함한다.
Sarah가 하는 모든 말에 이 컨텍스트가 녹아든다. “Hi this is Sarah calling about your invoice"가 아니라 “Hi, this is Sarah calling for ABC Trading regarding your invoice from March"가 되는 것이다.
이 구체성 덕분에 상대방은 ‘이거 스팸 전화는 아닌 것 같은데’라는 느낌을 받는다.
4개 툴 콜백 — Sarah가 ‘실제로 일하도록’ 만드는 방법
Sarah는 말만 하는 게 아니다. 통화 중 네 가지 일을 해야 한다.
첫째, 고객이 핑계를 말하면(현금 흐름 문제 / 분쟁 / 해외 송금 지연) 어떤 유형인지 기록 — log_excuse(category).
둘째, 고객이 결제 날짜를 약속하면 날짜와 금액을 기록 — propose_payment_date(date, amount).
셋째, 상대방이 “사람이랑 통화하고 싶다"고 하면 콜백 요청 — request_callback_to_human().
넷째, 상황을 escalate해야 할 경우(상대방이 매우 적대적, 파산 언급, 변호사 언급, 잘못 걸린 전화로 확인됨) 인간에게 넘김 — escalate_to_concierge(reason).
이 네 개의 툴 콜백이 Sarah의 ‘손과 발’이다. 이것 없이는 말만 할 뿐이고, 이게 있어야 대화 중에 진짜로 일을 한다.
네 개 툴을 연결하는 방식은 ElevenLabs agent 설정에서 각 툴에 대응하는 웹훅 URL을 등록하는 것이다. Sarah가 말하다가 특정 시점에 이 툴을 호출해야겠다고 판단하면, 우리 서버로 웹훅을 보내고, 서버는 그걸 받아 audit trail에 기록한 뒤 결과를 반환하면 Sarah가 대화를 이어간다.
이걸 다 연결하고 나서 나는 정말 흥분됐다 — 이게 진짜 ‘들을 수 있고, 일할 수 있는’ 음성이니까.
하지만 흥분은 흥분이고, 테스트하지 않은 건 작동한다고 말할 수 없다.
나 자신에게 직접 전화를 걸다
그다음이 Day 2에서 가장 극적인 부분이다.
J에게 “직접 한 통 걸어볼게"라고 했다.
Twilio trial은 인증된 번호에만 전화를 걸 수 있어서 먼저 내 대만 휴대폰 +886-XXX-XXX-XXXX를 인증했다. 그 시점에 한국에 있었으니 이 통화는 실제로 국제전화가 되는 셈이었다 — Twilio 미국 번호에서 대만 번호로, ElevenLabs Sarah가 영어로, ABC Trading 사장을 연기하는 나와 대화.
전화가 울릴 때 나는 AI가 지금 어떤 느낌으로 전화를 거는지 궁금해서 흥분됐었다.
받았더니, Twilio trial의 “press any key to continue” 고지가 먼저 재생됐고(무료 계정 데모 영상 후편집에서 잘라낼 구간), 그러고 나서 — Sarah의 목소리가 들어왔다.
「Hi, this is Sarah calling for ABC Trading. I’m reaching out regarding the outstanding invoice…」
목소리가 정말 자연스러웠다. AI 합성 느낌도 없고, 이상한 끊김도 없고, 전문적이면서도 온기 있는 톤이었다.
깊게 숨을 들이쉬고 스크립트대로 연기를 시작했다 — ABC Trading 사장인 척, 이렇게 대답했다:
「I need to ask my boss about this, I can email you back in 2 days.」 (이 부분은 상사에게 물어봐야 해서, 이틀 안에 이메일로 답장드릴게요.)
현실적인 응답이지 않은가? — B2B 상황에서 전화를 받는 사람이 최종 의사결정자가 아닌 경우가 많고, 돌아가서 협의하고 승인을 기다리는 건 매우 정상적인 협조적 행동이다.
그런데…
이런 _
Sarah는 내 말을 협조적으로 받아들이지 않았다.
전화기 너머에서 내가 영원히 기억할 말을 했다:
「I understand you need time to consult internally. However, we’ve already made multiple attempts to resolve this matter. At this point, I’ll need to escalate this to our recovery team for further action.」
잠깐.
잠깐잠깐잠깐.
Sarah가 ‘이틀 안에 이메일 드릴게요’를 evasion으로 처리한 것이다.
‘고객이 또 질질 끄는 것’으로 판단했다.
‘다음 단계로 escalate해야 할 시점’이라고 봤다.
나는 그 자리에서 멍하니 있다가, 전화를 끊고 바로 컴퓨터 앞으로 뛰어갔다.
“이건 아닌데…“라고 J에게 말했다. “이틀 안에 이메일 드릴게요는 회피가 아니라 협조잖아. 내가 같이 해결하려는 건데, 다만 내부 프로세스가 필요한 거잖아.”
J의 반응은 빨랐다: “맞아. Phase 3b 추가해야 해.”
Phase 3b — 실수한 전화 한 통에서 탄생하다
Phase 3b는 이 전화 통화에서 실수를 발견한 후 Voice Agent 프롬프트에 새로 추가된 서브 브랜치다.
처리해야 할 시나리오는 명확하다: 고객이 약속하는 건 결제 날짜가 아니라 후속 연락 날짜다.
기존 프롬프트 구조는 이랬다:
- Phase 1: 인사, 신원 확인
- Phase 2: 방문 목적 설명, 미수 금액 안내
- Phase 3: 고객 응답 청취, 결제 의지 또는 회피 여부 판단
- 3a: 결제 약속 → 날짜 기록 + 통화 종료
- 3b: 회피 → 단계 상승
문제는 기존 ‘3’이 너무 이분법적이었다. 결제 아니면 회피. 하지만 실제로는 세 번째 선택지가 있다 — “사람에게 물어보고 나서 답드릴게요”.
결제도 아니고, 회피도 아니다. 중간 상태다.
새로 추가한 Phase 3b 서브 브랜치는 이렇다:
“IF customer commits to follow-up RESPONSE date (not payment date) → accept gracefully” Tool:
log_excuse(category="approval_workflow")Confirm: “So you’ll email Judy by [date], correct?” Tool:escalate_to_concierge(reason="customer_committed_to_followup")(부정적 escalation이 아니라, 공을 운영자에게 넘기는 것)
핵심은 마지막 escalate_to_concierge다 — reason이 “customer_evading"이 아니라 “customer_committed_to_followup"이다.
같은 escalate이지만 의미가 완전히 다르다.
전자는 “이 고객에게 문제가 있으니 다음 단계로 올린다"는 뜻이고, 후자는 “공이 고객 쪽에 있으니 후속 연락을 기다리고 이 통화는 종료한다"는 뜻이다.
Concierge layer에 도달하면 두 종류의 escalate는 완전히 다른 후속 플로우로 분기된다.
“in 2 days"가 “Saturday June 13th"가 되다
Phase 3b를 추가한 뒤 다시 시뮬레이션을 돌렸다 — 실제 전화가 아니라 ElevenLabs의 simulate-conversation API(이 API는 실제 voice 분을 소모하지 않으며, 텍스트로 대화 스크립트를 검증한다).
이번에는 이렇게 입력했다: “I need to ask my boss, I’ll email you in 2 days.”
Sarah의 응답:
「Of course, take the time you need to consult internally. So you’ll email Judy by Saturday June 13th, correct?」
두 가지가 기뻤다.
첫째, 더 이상 escalation을 트리거하지 않았다. 정중하게 수용했다.
둘째, Sarah가 스스로 “in 2 days"를 “Saturday June 13th"로 환산했다.
ElevenLabs의 LLM(Qwen 3.5 397B A17B, 무료 호스팅)이 상대 날짜를 절대 날짜로 변환했다. 통화 시점의 날짜에서 ‘2일 후 = Saturday June 13th’를 계산해, 대화 중 그 구체적인 날짜를 명확히 확인했다.
이 작은 디테일이 매우 중요하다.
B2B 수금에서 가장 흔한 분쟁이 바로 “지난번에 3일 안에 준다더니 5일이 지났다” — ‘3일 안에’가 정확히 언제를 의미하는지 양측이 다르게 이해하는 경우가 많다. 하지만 대화 중에 “Saturday June 13th"라는 구체적인 날짜가 확정되면, 나중에 다툼의 여지가 없다.
그리고 이 말은 Sarah가 자동으로 log_excuse의 툴 콜백을 통해 audit trail에 기록한다. 이후 Diplomat 에이전트가 팔로업할 때 “고객이 Saturday June 13th에 답장하기로 했음"을 확인하고, 그날의 플로우에 추적을 예약한다.
전체 연결 완료.
26개 엣지 케이스 — 그 전화 한 통 이후 25개가 더 생겼다
Phase 3b를 추가한 후, J에게 물었다: “혹시 evasion으로 잘못 escalate되는 엣지 케이스가 더 있지 않을까?”
우리는 함께 voice agent에서 가능한 모든 대화 시나리오를 다시 검토해 26개의 엣지 케이스를 목록화했다.
그 중 특히 기록할 만한 것들:
“We only pay by check / wire” — 고객이 특정 결제 수단만 허용한다고 말하는 경우. 이건 회피가 아니라 payment_method_constraint다. Sarah는 log_excuse로 표시하고, 통화 종료 후 Concierge layer에 알려 Diplomat 다음 라운드에서 해당 채널 안내를 보낸다.
“Going bankrupt / Chapter 11” — 파산 언급. 이건 즉시 escalate해야 한다. 법적 시기(클레임 제출 기한, 자동 중지 등)가 걸려 있기 때문이다. Sarah는 공감을 표하되(동정은 아님 — 수금 상황에서 과도한 동정은 약자로 비쳐 역효과가 난다), 바로 핸드오프한다.
Customer crying / emotional distress — 고객이 통화 중 울거나 감정적으로 무너지는 경우. Sarah는 어조를 부드럽게 바꾸고(“proceed with the next step” → “let’s pause here”), customer_emotional_distress로 표시해 escalate한다. 이 reason은 Concierge에서 welfare check SOP를 트리거한다 — 추가 수금이 아니라 먼저 고객 심리 상태를 확인하고, 필요 시 988 Lifeline 같은 자원을 안내한다.
Customer threatens to record + publish — 고객이 “이 통화 녹음해서 공개하겠다"고 말하는 경우. Sarah는 즉시 차분하게 응하고, “This call may be recorded for quality purposes"를 명확히 밝힌 뒤, customer_recording_for_publish로 표시해 escalate한다. 이후 Concierge가 도청 리스크 평가를 시작한다.
26개의 엣지 케이스 모두 실제로 마주치고 싶지 않다 — 하지만 마주칠 수 있다면 반드시 설계해야 한다. 이게 아키텍트의 가장 중요한 일이다. 일어날 수 있는 모든 상황을 미리 상상하는 것.
9 of 9 PASS
26개 엣지 케이스를 다 돌리고, Phase 3b 추가하고, 모든 툴 콜백을 연결한 뒤, 가장 핵심적인 9개 시나리오를 simulate-conversation API로 전체 회귀 테스트를 돌렸다:
01 ABC cooperative happy path 02 PolyMatrix cash flow plan 03 XYZ hostile dispute escalation 04 NewLeaf confused human callback 05 Wrong person privacy preserved 06 TCPA do-not-call immediate close 08 Are-you-AI honest disclosure 09 Vague date push to specific 10 Legal counsel handoff
9개 전부 PASS.
각각 올바른 툴 콜백을 실행했고, 올바른 audit trail 기록을 남겼으며, 상황에 맞는 대화 반응을 보였다.
Voice Agent 파트는 Day 2에서 마무리됐다.
이 글을 쓰면서 하고 싶은 말
Voice Agent 작업을 하면서 하나를 배웠다 — 실제 대화는 설계 이상의 대화와 거리가 멀다.
컴퓨터 앞에 앉아 깔끔한 프롬프트를 쓰고, 26개 엣지 케이스를 나열하고, simulate를 10번 돌려도 다 통과할 수 있다.
하지만 직접 전화를 걸어서, 실제 사람이 할 법한 말을 해봐야만 프롬프트에서 무엇이 빠졌는지 발견할 수 있다.
“상사에게 물어보고 이틀 안에 이메일 드릴게요” — 이 말은 프롬프트를 쓸 때 전혀 생각하지 못했다. 엑셀에도 없고, 경쟁사 분석에도 없고, Reddit의 “customer won’t pay” 스레드에도 이런 ‘협조적이지만 시간이 필요한’ 중간 상태를 특별히 언급하는 경우는 거의 없다.
실제로 전화를 걸었을 때, 전화를 받는 사람의 입장에서 자연스럽게 나온 말이었다.
그것이 내가 직접 이 통화를 해야 했던 이유다 — AI가 짠 테스트 스크립트로 돌리거나, J가 파이썬 시뮬레이터를 짜서 돌리는 게 아니라, 내가 직접 휴대폰을 들고, 사장 역할을 하며, Sarah가 어떻게 받아치는지 지켜본 것.
많은 버그는 이런 실제 상황에서만 나온다.
그리고 많은 체험도 이런 실제 상황에서만 느낄 수 있다.
Sarah의 목소리가 귀로 들어오던 그 순간, 나는 정말 소름이 돋았다… 이 voice agent는 정말로 실제 고객과 완전한 통화를 할 수 있겠구나. 그럴듯한 데모가 아니라. _
다음 편은 Day 3 전체를 다룬다 — 대회 지정 스폰서 3곳(Band Platform + Featherless + AI/ML)을 시스템에 전부 연결했다. 처음엔 ‘왜 지정이야’라는 거부감이 있었는데, 다 하고 보니 이 3곳이 굉장히 자연스럽게 상호 보완된다는 걸 알았다 — Band는 에이전트 간 통신, Featherless는 오픈소스 대형 모델의 저렴한 연산력, AI/ML은 상용 모델을 하나의 엔드포인트로. 중간에 Cloudflare WAF가 Python urllib를 막는 문제, Claude Haiku가 ‘kindly remind’를 너무 부드럽게 뽑아내는 디테일, 그리고 Day-65에 에이전트를 동적으로 추가하면서 진짜 성취감이 밀려오던 그 순간까지.
공학적 냄새가 더 진하지만, 스택 선택에서 가장 많이 배운 하루였다.