LabHub
시작하기
배우기 러닝패스 코스

음성 AI 에이전트 — 듣고, 찾고, 말하는 파이프라인

예약 에이전트를 상태 기계로 — 확인·재시도·넘기기

LabHub 에서 이어서 보기

목표

전화 예약 에이전트를 상태 기계로 만들어, 확인 질문 없이는 예약하지 않고, 읽기 도구의 시간 초과만 다시 시도하고, 알아듣지 못하거나 사람을 찾으면 요약과 함께 넘기게 한다.

왜 중요한가

음성 에이전트의 사고는 도구에서 납니다. 되돌릴 수 없는 행동이 확인 없이 일어나거나, 시간 초과를 다시 보내 예약이 둘이 됩니다. 이 실습의 도구는 voicekit.clinic.ClinicAPI — 실패를 계획표로 넣을 수 있는 가짜 예약 API — 이고, 사용자의 말은 voicekit.nlu.parse 가 칸으로 바꿉니다. 채점기는 여러분의 /root/voice/agent/agent.py 에서 VoiceAgent 를 불러 시나리오 아홉 개(실패 계획 + 사용자 말)를 직접 돌리고, 말의 문구가 아니라 상태와 도구 호출 기록으로 판정합니다.

단계

  1. ClinicAPI 에 실패를 넣어 보고, 도구마다 되돌릴 수 없는지와 다시 해 볼 만한 오류를 /root/voice/agent/tools.json 에 적으세요.
  2. 문장 8개의 의도를 LLM(json_schema)과 규칙 파서로 뽑아 /root/voice/agent/intents.jsonl 과 정답 수 /root/voice/agent/intent_acc.json 을 만드세요.
  3. VoiceAgent(api, sleep) 와 handle(text) 를 가진 /root/voice/agent/agent.py 를 만들어, 확인에 '예' 를 받은 뒤에만 예약하게 하세요.
  4. 확인 질문에 '아니요'(새 시각이 있으면 그 시각으로 다시 확인)를 처리하세요.
  5. 읽기 도구(find_slots)의 시간 초과를 0.5·1.0초 쉬며 두 번까지 다시 부르고, 그래도 안 되면 tool_failure 로 넘기세요.
  6. 사람을 찾으면 user_request, 두 번 연달아 못 알아들으면 not_understood 로 넘기고, 넘길 때 채운 칸을 요약하세요.
  7. 예약 도구의 거절(자리 없음)은 빈 시각을 다시 찾아 묻고, 시간 초과는 다시 보내지 말고 넘기세요.
  8. 시나리오 아홉 개를 돌린 턴 기록을 /root/voice/agent/traces.jsonl 에 남기세요.

참고

도구의 성질을 적는다

voicekit.clinic 의 TOOLS 와 ClinicAPI(plan={"find_slots": ["timeout", "ok"], "book": ["taken"]}) 로 실패를 직접 일으켜 보고, /root/voice/agent/tools.json 에 tools(도구 이름마다 {"irreversible": true/false})와 retryable(다시 해 볼 만한 오류 이름 목록)을 적으세요.

TOOLS 의 값은 (되돌릴 수 없는가, 설명) 입니다. ToolTimeout 은 '응답이 없었다', ToolError 는 '거절됐다' — 어느 쪽을 다시 보낼 만한가요?

LLM 과 규칙, 누가 의도를 맞히나

문장 8개 — "I'd like to book an appointment for Tuesday"(book), "CAN I CANCEL MY VISIT ON FRIDAY"(cancel), "YEAH THAT WORKS"(yes), "NO THAT'S NOT RIGHT"(no), "CAN I TALK TO A REAL PERSON"(human), "WHAT TIME DO YOU CLOSE TODAY"(hours), "TEN THIRTY IN THE MORNING"(inform), "MY DOG ATE THE REMOTE"(unknown) — 를 이 순서로, intent 를 여덟 값의 enum 으로 묶은 json_schema 로 LLM 에 물은 결과(llm)와 voicekit.nlu.parse 결과(rule)를 /root/voice/agent/intents.jsonl 에 {"text", "llm", "rule"} 로 적고, 괄호 안 정답과 맞은 수를 /root/voice/agent/intent_acc.json 에 {"llm": n, "rule": m} 으로 적으세요.

voice-llm up 뒤 voicekit.llm.chat(messages, max_tokens=40, json_schema=schema). 형식은 문법이 강제하지만 내용은 모델 몫입니다. 결과가 이상하다면 그것이 이 단계에서 보려는 것입니다.

확인을 받은 뒤에만 예약한다

/root/voice/agent/agent.py 에 VoiceAgent(api, sleep=time.sleep, max_retries=2, backoff=(0.5, 1.0)) 와 handle(text) 를 만드세요. parse 로 칸(day·time·name)을 채우고, 빈 칸을 날짜 → (find_slots 로 빈 시각을 찾아 제시) 시각 → 이름 순으로 물은 뒤 CONFIRM 에서 되읽고, '예'(intent yes)를 들었을 때만 api.book(day, time, name) 을 불러 DONE 이 되게 하세요. 채점: 다섯 턴 대화와 한 번에 다 말하는 두 턴 대화에서 book 이 마지막 턴에 정확히 한 번.

'다음에 물을 것' 을 정하는 함수 하나(_next)를 두면 한 번에 여러 칸을 말한 경우도 같은 코드로 처리됩니다. 제시한 빈 시각(offered)에 없는 시각이면 다시 제시합니다. 이름 차례에 파서가 이름을 못 뽑으면 말의 마지막 낱말을 이름으로 씁니다.

확인의 '아니요' 는 수정이다

CONFIRM 에서 intent 가 no 일 때: 말에 새 시각·날짜가 있으면 그 칸만 바꿔 다시 확인(CONFIRM) 하고, 아무 정보가 없으면 시각을 비우고 빈 시각을 다시 제시하세요. 어느 쪽이든 book 은 부르지 않습니다. 채점: '…ten a m…' → 'no, make it two thirty' → 'yes' 에서 book 이 14:30 으로 마지막 턴에 한 번.

parse('no, make it two thirty') 는 intent no 와 time 14:30 을 함께 줍니다. no 라고 무조건 처음으로 돌아가면 사용자가 방금 말한 정보를 버립니다.

읽기 도구의 시간 초과는 다시 해 본다

find_slots 가 ToolTimeout 을 내면 self.sleep(backoff[시도 번호]) 로 0.5초, 1.0초 쉬며 최대 max_retries(2)번 다시 부르세요. 세 번 다 실패하면 상태를 HANDOFF 로, handoff.reason 을 tool_failure 로 두세요. 채점: 첫 호출만 시간 초과면 두 번 부르고 쉰 시간 [0.5] 로 ASK_TIME, 세 번 다 실패하면 세 번 부르고 쉰 시간 [0.5, 1.0] 로 HANDOFF.

sleep 을 생성자 인자로 받아 두어야 채점기가 기다리지 않고 '얼마나 쉬려 했나' 를 기록할 수 있습니다. 시험할 수 있게 만드는 것도 설계입니다.

사람에게 넘기기 — 요약과 함께

어느 상태에서든 intent 가 human 이면 곧바로 HANDOFF(user_request). intent 가 unknown 이면 한 번은 다시 말해 달라고 하고, 연달아 두 번째면 HANDOFF(not_understood). 알아들으면 횟수를 0 으로. 넘길 때 handoff.summary 에 지금까지 채운 day·time·name 을 담으세요. 채점: 'hmm' → 'blah blah' 에서 두 번째 턴에 넘김, 'Can I book on Friday' → 'can I talk to a person' 에서 요약에 friday.

이름 차례(ASK_NAME)에서는 파서가 unknown 을 내도 사람이 이름만 말한 것일 수 있습니다 — 그때는 알아들은 것으로 칩니다.

되돌릴 수 없는 도구가 실패할 때

book 이 ToolError(자리가 찼다)를 내면 다시 보내지 말고 시각을 비워 find_slots 로 빈 시각을 다시 찾아 제시하세요(ASK_TIME). book 이 ToolTimeout 을 내면 다시 보내지 말고 HANDOFF(tool_failure)로 넘기세요. 채점: 자리 없음 뒤 새 시각 14:30 으로 다시 확인받아 book 두 번(10:00 실패, 14:30 성공), 예약 시간 초과에서 book 한 번 뒤 HANDOFF.

예약의 시간 초과는 '안 됐다' 가 아니라 '모른다' 입니다. 서버는 예약을 만들었는데 응답만 늦었을 수 있습니다 — 다시 보내면 예약이 둘이 됩니다.

시나리오 아홉 개의 턴 기록

시나리오 아홉 개(happy · one_shot · confirm_no · retry_ok · retry_fail · not_understood · wants_human · taken · book_timeout — 실패 계획과 사용자 말은 각 단계 과제의 채점 설명과 같습니다)를 여러분의 agent.py 로 돌려, 턴마다 {"scenario", "turn", "user", "state", "say", "tools"(이번 턴에 부른 도구 이름), "outcomes"} 를 /root/voice/agent/traces.jsonl 에 적으세요. 채점기가 같은 시나리오를 다시 돌려 대조합니다.

도구 기록은 api.calls 에 (이름, 인자, 결과) 로 쌓입니다. 턴 전후의 길이 차이로 이번 턴에 부른 것만 뽑습니다. 이 기록이 곧 에이전트의 회귀 시험 자료입니다.