음성 AI 에이전트 — 듣고, 찾고, 말하는 파이프라인
말로 부리는 도구 — 상태 기계·확인 질문·재시도·사람에게 넘기기
한 줄 요약
음성 에이전트의 사고는 대개 도구에서 난다. '예' 라는 대답 없이 예약이 만들어지고, 시간 초과를 다시 보내 예약이 둘이 되고, 알아듣지 못한 채 같은 질문을 되풀이한다. 그래서 무엇을 할 수 있는지는 상태 기계가 정하고, 모델은 사용자의 말을 칸(날짜·시각·이름)으로 바꾸는 일만 한다. 되돌릴 수 없는 도구는 확인 질문 뒤에만 부르고, 시간 초과를 다시 보내지 않는다. 두 번 못 알아들으면 사람에게 넘기며, 넘길 때 이미 들은 것을 요약해 준다.
왜 이게 필요했나
전화 예약 비서를 LLM 하나로 만들면 대화 전체를 모델에게 주고 '다음에 무엇을 할지' 도 맡기게 된다. 이 이미지의 0.5B 모델에게 의도 분류를 json_schema 로 시켜 봤더니, 형식은 완벽히 지켰는데 8문장 중 1개만 맞혔다 — 거의 모든 문장을 enum 의 첫 값인 'book' 으로 분류했다(이 모듈 실습 2단계에서 직접 잰다). 형식을 문법으로 강제해도 내용은 강제되지 않는다. 이런 모델에게 '이제 예약해도 되는가' 를 맡기면, '아니요' 에도 예약이 만들어질 수 있다.
어떻게 동작하나
칸 채우기와 상태. 예약에 필요한 칸은 날짜·시각·이름 셋이다. 상태는 LISTEN → ASK_DAY → ASK_TIME → ASK_NAME → CONFIRM → DONE 으로 흐르고, 어느 상태에서든 HANDOFF(사람에게) 로 갈 수 있다. 사용자가 한 번에 '화요일 오전 10시, 이름은 Jamie' 라고 말하면 빈 칸이 없으니 곧바로 CONFIRM 으로 간다. 상태가 할 수 있는 일을 정하므로 book 은 CONFIRM 에서 '예' 를 들었을 때만 불린다 — 모델이 무엇을 말하든 구조적으로 그렇다.
확인 질문의 '아니요'. '아니요, 두 시 반으로요' 는 거절이 아니라 수정이다. 새 시각이 들어 있으면 그 칸만 바꿔 다시 확인하고, 아무 정보가 없으면 무엇이 틀렸는지 모르니 시각부터 다시 묻는다. 음성은 글보다 오인식이 많아서, 되돌릴 수 없는 행동 직전의 되읽기는 선택이 아니다.
재시도와 백오프. 도구 오류는 두 가지로 가른다. 시간 초과(ToolTimeout)는 같은 요청을 다시 보내면 될 수도 있다. 거절(ToolError, 예: 그 자리가 방금 찼다)은 다시 보내도 같은 답이 온다. 읽기 전용 도구(빈 시각 찾기)의 시간 초과는 0.5초, 1.0초처럼 점점 길게 쉬며 두 번까지 다시 부르고, 그래도 안 되면 붙잡지 말고 넘긴다. 전화에서는 기다리는 몇 초가 곧 침묵이다.
되돌릴 수 없는 도구. 예약(book)이 시간 초과로 끝났다면 그것은 '안 됐다' 가 아니라 '모른다' 다. 서버는 예약을 만들었는데 응답만 늦었을 수 있다. 다시 보내면 예약이 둘이 된다. 그래서 되돌릴 수 없는 도구의 시간 초과는 재시도하지 않고 사람이 확인하도록 넘긴다(멱등 키가 있는 API 라면 같은 키로 다시 보내 안전하게 확인할 수 있다 — 그런 API 가 아니면 하지 않는다). 거절(자리 없음)은 빈 시각을 다시 찾아 새로 묻는다.
사람에게 넘기기. 넘길 때는 세 가지를 지킨다. 사용자가 사람을 찾으면 언제든 바로 넘긴다. 두 번 연달아 못 알아들으면 세 번째를 시도하지 않는다. 그리고 이미 들은 칸을 요약해 함께 넘긴다 — 받는 사람이 처음부터 다시 묻게 하는 것이 가장 나쁜 넘기기다.
시험은 녹음이 아니라 기록으로. 에이전트를 시험할 때 말의 문구를 비교하면 표현을 바꿀 때마다 시험이 깨진다. 실패 계획을 넣은 가짜 도구와 가짜 잠자기(sleep)를 끼워 넣고, 상태 흐름과 도구 호출 기록을 비교한다. 재시도의 백오프도 실제로 기다리지 않고 '얼마나 쉬려 했나' 로 확인한다.
현장에서 만나는 모습
LangGraph 코스가 같은 생각을 그래프로 다룬다 — 노드가 상태를 바꾸고, 조건 간선이 다음을 정하고, 사람의 승인이 필요한 곳에서 멈춘다. MCP 서버 코스는 도구 쪽에서 같은 경계를 지킨다 — 도구가 스스로 권한을 좁히고 파괴적인 작업에 확인을 요구한다. 음성 에이전트는 그 둘 사이에서, 사람이 화면 없이 귀로만 확인한다는 제약을 더한다.
규칙 파서도 완벽하지 않다. 이 코스의 파서는 'I need to see the doctor on Thursday afternoon' 을 예약(book)이 아니라 정보 제공(inform)으로 읽는다 — '예약' 을 뜻하는 낱말이 목록에 없기 때문이다. 그래도 상태 기계 안에서는 큰 사고가 나지 않는다. 날짜가 채워졌으니 다음 빈 칸(시각)을 묻고, 결국 확인 질문을 거친다. 이해 모듈이 틀려도 되돌릴 수 없는 일은 확인에서 멈춘다 — 이것이 상태 기계를 쓰는 이유다. 이해 모듈을 LLM 으로 바꾸더라도 이 구조는 그대로 둔다. 바뀌는 것은 칸을 채우는 부품뿐이다.
다음 실습에서 할 것
가짜 진료 예약 API 에 실패를 넣어 보고 도구의 성질을 적는다. LLM 과 규칙 파서의 의도 정답률을 잰다. 예약 에이전트를 상태 기계로 만들고 단계마다 기능을 더한다 — 행복한 흐름, 확인의 '아니요', 읽기 도구의 재시도, 사람에게 넘기기, 되돌릴 수 없는 도구의 실패. 채점기는 여러분의 agent.py 로 시나리오 아홉 개를 직접 돌린다.