손이 바쁠 때 로봇은 귀가 필요하다
지금까지 우리는 자연어 명령을 텍스트로 입력받아 로봇 스킬로 바꾸고, 실행 결과를 다시 확인하는 구조를 만들었다. 사용자가 키보드로 "회의실 앞으로 가줘"라고 입력하면 LLM 계획기가 목표를 해석하고, 디스패처가 navigate_to 같은 스킬을 호출하며, 로봇은 실행 결과를 상태 메시지로 돌려준다.
그런데 실제 로봇을 떠올려 보자. 사용자는 항상 노트북 앞에 앉아 있지 않다. 양손에 물건을 들고 있을 수도 있고, 실험실 안을 걸어 다니고 있을 수도 있으며, 로봇이 움직이는 모습을 보면서 즉시 "멈춰!"라고 말해야 할 수도 있다. 이때 키보드 입력은 너무 느리고 불편하다.
음성 인터페이스를 추가하면 로봇 에이전트는 훨씬 자연스러운 동료처럼 느껴진다. 사용자가 말하면 로봇이 듣고, 이해하고, 실행하고, 다시 말로 대답한다. 이 장의 목표는 단순히 음성을 텍스트로 바꾸는 것이 아니다. 음성 입력에서 시작해 STT, 자연어 해석, 로봇 스킬 호출, TTS 응답까지 이어지는 대화형 로봇 인터페이스를 만드는 것이다.
전체 흐름은 다음과 같다.
사용자 음성
↓
마이크 입력
↓
STT: 음성을 텍스트로 변환
↓
명령 전처리와 안전 검사
↓
LLM 계획기: 텍스트 명령을 작업 순서로 변환
↓
디스패처: 로봇 스킬 호출
↓
실행 결과 수집
↓
TTS: 결과를 음성으로 응답이 구조에서 중요한 점은 LLM이 음성을 직접 다루지 않는다는 것이다. LLM은 여전히 텍스트 명령을 해석하고 계획을 만든다. 음성 인터페이스는 그 앞과 뒤에 붙는 입출력 계층이다. 앞쪽에서는 사람의 말을 텍스트로 바꾸고, 뒤쪽에서는 로봇의 상태를 사람이 이해할 수 있는 말로 바꾼다.

마이크 입력에서 텍스트 명령까지
음성 명령 로봇의 첫 번째 관문은 마이크다. 마이크는 공기의 진동을 전기 신호로 바꾸고, 컴퓨터는 이 신호를 일정한 간격으로 샘플링해 숫자 배열로 저장한다. 우리가 듣는 "빨간 컵을 집어 줘"라는 문장은 컴퓨터 입장에서는 시간에 따라 변하는 파형 데이터다.
STT, 즉 Speech-to-Text는 이 파형 데이터를 문자로 변환하는 기술이다. 한국어로 "로봇아, 빨간 컵을 집어서 오른쪽에 놓아줘"라고 말하면 STT 모듈은 다음과 같은 텍스트를 출력한다.
로봇아 빨간 컵을 집어서 오른쪽에 놓아줘여기서 바로 LLM 계획기로 보내도 될 것 같지만, 실제 시스템에서는 몇 가지 단계를 더 거치는 편이 좋다.
audio_stream
→ voice_activity_detection
→ speech_segment
→ stt_result
→ text_normalization
→ command_candidatevoice_activity_detection은 음성이 있는 구간과 없는 구간을 구분한다. 계속 녹음된 모든 소리를 STT에 보내면 에어컨 소리, 키보드 소리, 주변 대화까지 명령으로 오해할 수 있다. 따라서 먼저 "지금 사람이 말하고 있는가?"를 판단해야 한다. 보통은 일정 크기 이상의 소리가 일정 시간 이상 이어질 때 음성 구간으로 간주한다. 더 정교하게 만들면 사람 목소리의 주파수 특성이나 작은 음성 인식 모델을 사용할 수 있다.
STT 결과는 항상 완벽하지 않다. "컵을 집어"가 "컵을 집어줘"로 바뀌는 정도는 괜찮지만, "멈춰"가 "먼저"로 인식되면 위험하다. 그래서 STT 결과는 원문 그대로 명령으로 확정하지 않고, 후보 명령으로 다루는 것이 안전하다.
예를 들어 STT 모듈이 다음과 같은 결과를 반환했다고 하자.
{
"text": "빨간 컵을 집어서 오른쪽에 놓아줘",
"confidence": 0.87,
"start_time": 12.3,
"end_time": 15.1
}여기서 confidence는 인식 결과에 대한 신뢰도다. 모든 STT 시스템이 같은 방식의 신뢰도를 제공하는 것은 아니지만, 비슷한 개념의 점수를 사용할 수 있다면 매우 유용하다. 신뢰도가 높으면 명령 후보로 넘기고, 낮으면 사용자에게 다시 물을 수 있다.
"빨간 컵을 오른쪽에 놓으라는 말씀이 맞나요?"이 한 문장이 로봇을 훨씬 안전하게 만든다. 로봇은 모호한 명령을 억지로 실행하지 않고, 확인이 필요할 때 확인한다.
호출어와 명령 구간 나누기
실제 공간에서는 사용자가 로봇에게만 말하지 않는다. 옆 사람과 대화할 수도 있고, 전화 통화를 할 수도 있다. 로봇이 모든 말을 명령으로 받아들이면 곤란하다. 그래서 많은 음성 로봇은 호출어를 사용한다. "로봇아", "에이전트", "헬퍼" 같은 단어가 먼저 들렸을 때만 명령을 듣기 시작하는 방식이다.
예를 들어 사용자가 이렇게 말한다.
로봇아, 회의실 앞으로 가줘.시스템은 먼저 호출어 "로봇아"를 감지하고, 그 뒤의 문장 "회의실 앞으로 가줘"를 명령 후보로 추출한다. 반대로 다음 문장은 로봇 명령이 아니다.
저 로봇은 회의실 앞으로 가면 될 것 같아.이 문장에는 "로봇"이라는 단어가 있지만, 로봇에게 직접 내린 명령은 아니다. 호출어와 명령 형태를 구분하지 않으면 로봇이 갑자기 움직일 수 있다.
간단한 규칙은 다음과 같이 만들 수 있다.
WAKE_WORDS = ["로봇아", "에이전트", "헬퍼"]
def extract_command(text: str):
text = text.strip()
for wake in WAKE_WORDS:
if text.startswith(wake):
command = text[len(wake):].strip(" ,.")
return {
"accepted": True,
"command": command,
"reason": "wake_word_detected"
}
return {
"accepted": False,
"command": None,
"reason": "wake_word_missing"
}물론 실제 시스템에서는 문장 앞에만 호출어가 오지 않을 수도 있다. "헬퍼, 잠깐만, 저 컵 치워 줄래?"처럼 중간에 머뭇거림이 들어갈 수 있다. 처음에는 단순한 규칙으로 시작하되, 나중에는 LLM이나 의도 분류 모델을 사용해 "이 문장이 로봇에게 내린 명령인가?"를 판단할 수 있다.
다만 긴급 정지 명령은 호출어가 없어도 처리하는 편이 좋다. 사용자가 위험한 순간에 "로봇아 멈춰"라고 예쁘게 말할 여유가 없을 수 있기 때문이다. "멈춰", "정지", "스톱", "그만" 같은 말은 별도의 고우선순위 안전 명령으로 다룬다.
일반 명령: 호출어 필요
긴급 정지: 호출어 없이도 감지이 작은 차이가 실제 로봇에서는 매우 중요하다.
STT 결과를 LLM 계획기로 보내기
STT가 텍스트 명령을 만들었다면, 다음 단계는 이미 앞 장들에서 만든 자연어 명령 처리 구조와 거의 같다. 텍스트 명령을 LLM 계획기에 보내고, LLM은 사용할 수 있는 로봇 스킬 목록 안에서 실행 계획을 만든다.
예를 들어 사용자가 말한다.
로봇아, 빨간 컵을 집어서 노트북 오른쪽에 놓아줘.STT와 전처리를 거친 명령은 다음처럼 정리된다.
{
"source": "voice",
"text": "빨간 컵을 집어서 노트북 오른쪽에 놓아줘",
"language": "ko",
"confidence": 0.91,
"timestamp": "2026-05-14T10:15:22Z"
}이제 LLM 계획기는 이 문장을 로봇이 실행 가능한 스킬 호출 순서로 바꾼다. 중요한 것은 LLM에게 자유롭게 코드를 만들게 하는 것이 아니라, 허용된 스킬 안에서만 계획하게 하는 것이다.
{
"goal": "빨간 컵을 노트북 오른쪽에 놓기",
"steps": [
{
"skill": "detect_object",
"args": {"object": "red cup"}
},
{
"skill": "detect_object",
"args": {"object": "laptop"}
},
{
"skill": "pick",
"args": {"object": "red cup"}
},
{
"skill": "place",
"args": {"object": "red cup", "relation": "right_of", "reference": "laptop"}
},
{
"skill": "check_status",
"args": {"target": "red cup"}
}
]
}음성 명령이라는 점 때문에 추가로 다뤄야 할 정보도 있다. 텍스트 입력에서는 사용자가 문장을 보고 수정할 수 있지만, 음성 입력에서는 오인식이 숨어 있을 수 있다. 따라서 계획기로 보내기 전에 다음과 같은 검사를 두는 것이 좋다.
STT 신뢰도가 너무 낮으면 실행하지 않고 재확인한다.
위험한 동작이 포함되면 사용자 확인이나 운영자 승인을 요구한다.
이전 명령과 너무 비슷하고 시간 간격이 짧으면 중복 명령으로 판단한다.
예를 들어 사용자가 한 번 말했는데 마이크가 같은 문장을 두 번 잡을 수 있다. 이때 로봇이 컵을 두 번 집으려 하면 실패하거나 이상한 동작을 할 수 있다. 그래서 명령 관리자에는 최근 명령 기록이 필요하다.
class VoiceCommandFilter:
def __init__(self):
self.last_command = None
self.last_time = 0.0
def is_duplicate(self, command: str, now: float) -> bool:
if self.last_command is None:
return False
same_text = command == self.last_command
too_soon = now - self.last_time < 3.0
return same_text and too_soon
def accept(self, command: str, now: float):
self.last_command = command
self.last_time = now물론 실제 문장은 완전히 같지 않을 수 있다. "빨간 컵 집어 줘"와 "빨간 컵을 집어 줘"는 거의 같은 명령이다. 나중에는 문장 임베딩이나 LLM을 사용해 의미적으로 같은 명령인지 판단할 수 있다. 하지만 첫 번째 버전에서는 짧은 시간 안에 같은 STT 결과가 반복되는 경우만 막아도 큰 도움이 된다.
디스패처는 말이 아니라 계획을 실행한다
음성 명령 로봇에서 흔히 저지르는 실수는 STT 결과를 바로 로봇 제어 코드에 연결하는 것이다. 예를 들어 텍스트에 "앞으로"가 들어 있으면 전진하고, "왼쪽"이 들어 있으면 회전하는 식이다. 장난감 프로젝트에서는 가능하지만, 실제 로봇 에이전트에서는 위험하다.
사용자의 말은 언제나 모호하다. "저쪽으로 가줘"에서 저쪽이 어디인지 알기 어렵고, "컵 좀 치워 줘"에서 컵을 어디로 옮길지도 명확하지 않다. 따라서 음성 텍스트는 반드시 자연어 해석과 계획 단계를 거쳐야 한다.
디스패처는 사용자의 말을 실행하지 않는다. 디스패처는 검증된 계획을 실행한다.
음성 문장: "저 컵 좀 치워 줘"
↓
LLM 계획기: 목표와 필요한 정보 분석
↓
확인 질문: "컵을 어디에 놓을까요?"
↓
사용자 응답: "트레이 위에 놓아 줘"
↓
계획 확정
↓
디스패처 실행이 구조를 사용하면 로봇은 모호한 말을 들었을 때 바로 움직이지 않고 대화할 수 있다. 이것이 음성 인터페이스의 큰 장점이다. 텍스트 명령에서는 사용자가 명령을 길게 써야 하지만, 음성에서는 로봇이 필요한 정보를 되물을 수 있다.
디스패처에 전달되는 실행 요청은 다음처럼 정리할 수 있다.
{
"request_id": "voice-20260514-0012",
"source": "voice",
"user_text": "빨간 컵을 집어서 노트북 오른쪽에 놓아줘",
"plan": [
{"skill": "detect_object", "args": {"object": "red cup"}},
{"skill": "detect_object", "args": {"object": "laptop"}},
{"skill": "pick", "args": {"object": "red cup"}},
{"skill": "place", "args": {"relation": "right_of", "reference": "laptop"}}
],
"safety": {
"requires_confirmation": false,
"emergency": false
}
}여기서 request_id는 중요하다. 음성 명령은 중간에 끊기거나 중복될 수 있고, 사용자가 실행 중에 취소할 수도 있다. 각 요청에 고유한 ID를 붙이면 로그를 추적하고, 실행 중인 명령을 취소하고, 결과를 사용자에게 정확히 알려 줄 수 있다.
로봇의 대답을 설계하기
음성 인터페이스는 듣기만 해서는 완성되지 않는다. 로봇도 말해야 한다. 사용자는 로봇의 내부 상태를 볼 수 없다. 지금 명령을 이해했는지, 실행 중인지, 실패했는지, 다시 시도하고 있는지 알 수 없다. 화면 없이도 상태를 전달하려면 TTS가 필요하다.
TTS, 즉 Text-to-Speech는 텍스트를 음성으로 변환하는 기술이다. 로봇 시스템에서는 실행 결과를 사람이 이해하기 쉬운 문장으로 바꾼 뒤 TTS에 전달한다.
예를 들어 디스패처가 다음 결과를 반환했다고 하자.
{
"request_id": "voice-20260514-0012",
"status": "success",
"summary": "red cup placed right_of laptop",
"duration_sec": 18.4
}이 결과를 그대로 읽으면 사용자는 어색하게 느낀다.
red cup placed right_of laptop로봇은 기계의 로그가 아니라 사람의 언어로 말해야 한다.
빨간 컵을 노트북 오른쪽에 놓았어요.실패했을 때는 더 중요하다. 좋은 실패 응답은 사용자가 다음 행동을 결정할 수 있게 해 준다.
{
"status": "failed",
"stage": "detect_object",
"reason": "object_not_found",
"detail": "red cup was not detected"
}이 결과는 다음처럼 말할 수 있다.
빨간 컵을 찾지 못했어요. 컵이 카메라에 보이도록 조금 옮겨 주시겠어요?여기에는 두 가지 정보가 들어 있다. 첫째, 무엇이 실패했는지 알려 준다. 둘째, 사용자가 어떻게 도와줄 수 있는지 제안한다. 단순히 "실패했습니다"라고 말하는 로봇보다 훨씬 쓸모 있다.
응답 문장은 상황에 따라 다르게 설계해야 한다. 명령을 받았을 때는 짧게 확인하고, 실행 중에는 너무 자주 말하지 않으며, 실패했을 때는 원인과 다음 선택지를 알려 주는 것이 좋다.
| 상황 | 나쁜 응답 | 좋은 응답 |
|---|---|---|
| 명령 수신 | "입력 완료" | "알겠어요. 빨간 컵을 집어 볼게요." |
| 실행 중 | "스킬 실행 중, 스킬 실행 중" | "컵을 찾았어요. 이제 집어 볼게요." |
| 성공 | "성공" | "빨간 컵을 노트북 오른쪽에 놓았어요." |
| 실패 | "오류" | "컵을 집지 못했어요. 위치를 조금 바꿔 다시 시도할까요?" |
| 안전 정지 | "정지됨" | "멈췄어요. 다시 시작하려면 명령해 주세요." |
TTS 응답은 너무 길어도 안 된다. 로봇이 장황하게 말하는 동안 사용자는 답답해진다. 특히 긴급 상황에서는 짧고 명확해야 한다.
"멈췄어요."이 한마디면 충분할 때가 많다.
대화형 인터페이스의 상태 기계
음성 명령 로봇은 단순한 함수 호출보다 상태 기계로 생각하는 것이 좋다. 상태 기계는 시스템이 현재 어떤 상태에 있는지 정의하고, 이벤트에 따라 다음 상태로 넘어가게 만드는 구조다.
가장 단순한 대화 상태는 다음과 같다.
IDLE
↓ 호출어 감지
LISTENING
↓ 음성 구간 종료
TRANSCRIBING
↓ STT 완료
UNDERSTANDING
↓ 계획 생성
CONFIRMING 또는 EXECUTING
↓ 실행 완료
RESPONDING
↓ 응답 완료
IDLEIDLE은 대기 상태다. 로봇은 주변 소리를 듣고 있지만 일반 대화에는 반응하지 않는다. 호출어가 감지되면 LISTENING으로 넘어간다. 사용자의 말이 끝나면 TRANSCRIBING에서 STT를 수행하고, UNDERSTANDING에서 LLM 계획기가 명령을 해석한다.
명령이 명확하고 안전하면 바로 EXECUTING으로 간다. 하지만 모호하거나 위험하면 CONFIRMING으로 가서 사용자에게 확인 질문을 한다.
로봇: "빨간 컵을 오른쪽에 놓으라는 말씀이 맞나요?"
사용자: "응, 맞아."
로봇: "알겠어요. 실행할게요."이런 확인 대화는 로봇의 행동을 예측 가능하게 만든다. 사용자는 로봇이 무엇을 하려는지 알고, 잘못 이해했을 때 바로 막을 수 있다.
상태 기계를 코드로 표현하면 다음과 같은 형태가 된다.
class VoiceAgentState:
IDLE = "idle"
LISTENING = "listening"
TRANSCRIBING = "transcribing"
UNDERSTANDING = "understanding"
CONFIRMING = "confirming"
EXECUTING = "executing"
RESPONDING = "responding"
STOPPED = "stopped"그리고 이벤트에 따라 상태를 바꾼다.
if state == VoiceAgentState.IDLE and wake_word_detected:
state = VoiceAgentState.LISTENING
elif state == VoiceAgentState.LISTENING and speech_finished:
state = VoiceAgentState.TRANSCRIBING
elif state == VoiceAgentState.TRANSCRIBING and stt_done:
state = VoiceAgentState.UNDERSTANDING
elif state == VoiceAgentState.UNDERSTANDING and plan_requires_confirmation:
state = VoiceAgentState.CONFIRMING
elif state == VoiceAgentState.UNDERSTANDING and plan_is_safe:
state = VoiceAgentState.EXECUTING여기서 긴급 정지는 특별하다. 현재 상태가 무엇이든 즉시 우선 처리되어야 한다.
if emergency_stop_detected:
cancel_current_skill()
publish_stop_command()
state = VoiceAgentState.STOPPED
speak("멈췄어요.")음성 인터페이스에서 가장 위험한 설계는 긴급 정지를 일반 명령과 같은 줄에 세우는 것이다. "멈춰"를 LLM 계획기에 보내고, LLM이 해석하고, 계획을 만들고, 디스패처가 실행하는 동안 로봇은 계속 움직일 수 있다. 긴급 정지는 LLM보다 앞에 있어야 한다. STT 결과에서 정지 의도가 감지되는 즉시 로봇 제어 계층으로 전달해야 한다.
음성 오인식을 안전하게 다루기
음성 명령은 편리하지만 불완전하다. 마이크 품질, 주변 소음, 사용자의 발음, 방의 울림, 여러 사람의 대화가 모두 인식 결과에 영향을 준다. 따라서 음성 로봇은 "잘 알아듣는 로봇"이기 전에 "잘못 알아들었을 때 위험하지 않은 로봇"이어야 한다.
오인식을 다루는 첫 번째 방법은 신뢰도 기반 확인이다. STT 신뢰도가 낮거나 문장이 이상하면 실행하지 않고 되묻는다.
"명령을 정확히 듣지 못했어요. 다시 말씀해 주시겠어요?"두 번째 방법은 위험도 기반 확인이다. 명령이 명확하더라도 위험한 동작이라면 확인을 요구한다. 예를 들어 로봇팔이 사람 가까이 움직이거나, 이동 로봇이 문밖으로 나가거나, 물체를 버리는 작업은 한 번 더 확인하는 편이 좋다.
"이 물건을 쓰레기통에 넣을까요? 맞으면 '응'이라고 말씀해 주세요."세 번째 방법은 실행 전 요약이다. 로봇이 이해한 내용을 짧게 말한 뒤 실행한다.
"알겠어요. 파란 상자를 선반 아래칸으로 옮길게요."이 문장을 들은 사용자는 잘못된 부분을 즉시 알아차릴 수 있다. 만약 사용자가 "아니, 빨간 상자야"라고 말하면 로봇은 실행을 시작하지 않고 계획을 수정해야 한다.
음성 오인식 처리는 다음 규칙으로 정리할 수 있다.
확신이 낮으면 실행하지 말고 다시 묻는다.
위험도가 높으면 확신이 높아도 확인한다.
실행 전에는 로봇이 이해한 목표를 짧게 요약한다.
사용자의 부정 응답이 들어오면 기존 계획을 폐기한다.
긴급 정지 표현은 어떤 상태에서도 최우선 처리한다.
이 규칙들은 로봇을 느리게 만들 수 있다. 하지만 실제 환경에서는 약간 느린 로봇이 위험하게 빠른 로봇보다 낫다. 사용자는 안전하고 예측 가능한 로봇을 더 신뢰한다.
중복 명령과 끼어드는 명령 처리
음성 인터페이스에서는 한 가지 까다로운 문제가 더 있다. 사용자는 로봇이 작업 중일 때도 말을 한다. "잠깐만", "아니야", "그거 말고", "멈춰", "다시 해 봐" 같은 말이 실행 도중에 들어온다. 로봇은 이를 무시해서도 안 되고, 모두 새 작업으로 실행해서도 안 된다.
실행 중 들어오는 음성은 크게 세 종류로 나눌 수 있다.
| 입력 종류 | 예시 | 처리 |
|---|---|---|
| 긴급 정지 | "멈춰", "스톱" | 즉시 현재 스킬 취소 및 정지 |
| 수정 명령 | "아니, 왼쪽 말고 오른쪽" | 현재 계획 일시 중지 후 재계획 |
| 잡담 또는 설명 | "잘하고 있네" | 실행에는 반영하지 않음 |
긴급 정지는 언제나 가장 높은 우선순위다. 수정 명령은 조금 더 어렵다. 로봇팔이 이미 컵을 집고 이동 중인데 사용자가 "오른쪽 말고 왼쪽에 놔"라고 말할 수 있다. 이때 로봇은 컵을 든 상태에서 새 place 목표를 계산할 수 있다면 재계획하고, 그렇지 않으면 안전한 중간 자세로 이동한 뒤 사용자에게 확인해야 한다.
사용자: "오른쪽 말고 왼쪽에 놔."
로봇: "알겠어요. 현재 동작을 멈추고 왼쪽 위치로 다시 계획할게요."중복 명령은 또 다른 문제다. 사용자가 "회의실 앞으로 가줘"라고 말했는데 STT가 두 번 결과를 내거나, 사용자가 로봇이 반응하지 않은 줄 알고 같은 말을 반복할 수 있다. 이때 로봇은 같은 목표로 여러 번 내비게이션을 시작하지 않아야 한다.
실행 중 같은 목표가 들어오면 다음처럼 응답할 수 있다.
"이미 회의실 앞으로 이동 중이에요."이 응답은 사용자를 안심시킨다. 로봇이 명령을 못 들은 것이 아니라 이미 처리 중이라는 사실을 알려 주기 때문이다.
명령 관리자는 현재 실행 중인 목표와 새 명령을 비교해야 한다.
def handle_new_command(command):
if is_emergency_stop(command):
stop_now()
return
if dispatcher.is_running():
if is_same_goal(command, dispatcher.current_goal):
speak("이미 그 작업을 진행하고 있어요.")
return
if is_modification(command):
dispatcher.pause_or_cancel()
replan(command)
return
speak("지금 작업 중이에요. 끝난 뒤 다시 말씀해 주세요.")
return
start_new_plan(command)이 구조는 대화형 로봇을 훨씬 안정적으로 만든다. 로봇은 한 번에 하나의 물리 작업을 수행하고, 새 음성 입력은 현재 상태에 맞게 해석된다.
ROS2 노드로 나누어 보기
이제 전체 시스템을 ROS2 관점에서 나누어 보자. 음성 명령 로봇은 다음과 같은 노드들로 구성할 수 있다.
/microphone_node
→ 오디오 캡처
/stt_node
→ 음성 구간을 텍스트로 변환
/voice_command_manager
→ 호출어, 중복 명령, 긴급 정지, 확인 대화 처리
/llm_planner_node
→ 자연어 명령을 스킬 계획으로 변환
/skill_dispatcher
→ detect_object, pick, place, navigate_to 등 실행
/tts_node
→ 응답 문장을 음성으로 출력각 노드는 역할을 작게 유지하는 것이 좋다. stt_node는 음성을 텍스트로 바꾸는 일에 집중한다. 로봇이 움직여도 되는지는 판단하지 않는다. llm_planner_node는 계획을 만들지만 직접 모터를 움직이지 않는다. skill_dispatcher는 허용된 스킬만 실행하고, 위험 상황에서는 취소와 정지를 처리한다.
메시지 흐름은 다음처럼 설계할 수 있다.
/audio_chunk
microphone_node → stt_node
/stt_result
stt_node → voice_command_manager
/command_request
voice_command_manager → llm_planner_node
/plan_request
llm_planner_node → skill_dispatcher
/execution_status
skill_dispatcher → voice_command_manager
/tts_request
voice_command_manager → tts_node
/emergency_stop
voice_command_manager → safety_controller, skill_dispatcher여기서 /emergency_stop은 일반 명령 흐름과 분리한다. 긴급 정지 메시지는 LLM 계획기나 일반 디스패처 큐를 기다리면 안 된다. 별도의 토픽이나 서비스, 하드웨어 정지 입력과 연결해 가장 빠른 경로로 전달해야 한다.
명령 관리자 노드는 음성 인터페이스의 중심이다. 이 노드는 STT 결과를 받아 다음을 결정한다.
이 문장이 로봇에게 한 말인가?
긴급 정지인가?
신뢰도가 충분한가?
현재 실행 중인 작업과 충돌하지 않는가?
사용자 확인이 필요한가?
LLM 계획기로 보낼 명령인가?
TTS로 어떤 응답을 해야 하는가?
즉, 음성 명령 로봇의 핵심은 STT 모델 자체가 아니라 음성에서 발생하는 불확실성을 로봇 실행 구조 안에서 안전하게 다루는 것이다.
하나의 예제로 끝까지 따라가기
사용자가 실험실 책상 앞에서 말한다.
로봇아, 빨간 컵을 집어서 오른쪽 트레이에 놓아줘.마이크 노드는 음성 파형을 캡처하고, STT 노드는 다음 결과를 만든다.
{
"text": "로봇아 빨간 컵을 집어서 오른쪽 트레이에 놓아줘",
"confidence": 0.93
}명령 관리자는 호출어를 제거한다.
{
"command": "빨간 컵을 집어서 오른쪽 트레이에 놓아줘",
"source": "voice",
"confidence": 0.93
}신뢰도가 충분하고 긴급 정지가 아니며 중복 명령도 아니다. 명령 관리자는 사용자에게 짧게 응답한다.
"알겠어요. 빨간 컵을 오른쪽 트레이에 놓을게요."LLM 계획기는 명령을 스킬 계획으로 바꾼다.
{
"steps": [
{"skill": "detect_object", "args": {"object": "red cup"}},
{"skill": "detect_object", "args": {"object": "right tray"}},
{"skill": "pick", "args": {"object": "red cup"}},
{"skill": "place", "args": {"object": "red cup", "target": "right tray"}},
{"skill": "check_status", "args": {"object": "red cup", "target": "right tray"}}
]
}디스패처는 먼저 detect_object를 호출한다. 카메라가 빨간 컵과 오른쪽 트레이를 찾는다. 그다음 로봇팔은 접근 자세를 만들고, MoveIt2 경로 계획을 수행하고, 컵을 집어 들어 트레이 위로 이동한다. 실행 중 사용자가 로봇을 보고 말한다.
잠깐, 멈춰!여기서 호출어는 없다. 하지만 "멈춰"는 긴급 정지 표현이다. 명령 관리자는 LLM 계획기를 기다리지 않고 즉시 정지 메시지를 보낸다.
/emergency_stop → skill_dispatcher
/emergency_stop → safety_controller로봇은 현재 동작을 취소하고 안전 정지한다. TTS는 짧게 응답한다.
"멈췄어요."이후 사용자가 다시 말한다.
다시 해도 돼. 계속해.이 문장은 새 작업이라기보다 정지된 작업을 재개하라는 말이다. 하지만 로봇이 물체를 든 상태인지, 중간 자세인지, 그리퍼가 열린 상태인지에 따라 안전한 재개 방법이 달라진다. 따라서 로봇은 무조건 계속하지 말고 현재 상태를 확인한다.
"현재 컵을 집은 상태예요. 오른쪽 트레이에 놓는 작업을 계속할까요?"사용자가 "응"이라고 답하면 재계획 후 실행한다. 실행이 끝나면 로봇은 결과를 말한다.
"빨간 컵을 오른쪽 트레이에 놓았어요."이 예제에서 음성 인터페이스는 단순한 입력 장치가 아니다. 로봇과 사용자가 작업 중에도 상태를 공유하고, 위험할 때 즉시 멈추고, 필요하면 다시 계획하는 대화 채널이다.
구현할 때의 현실적인 선택
처음부터 완벽한 음성 로봇을 만들 필요는 없다. 오히려 작은 범위에서 안정적으로 동작하는 버전을 먼저 만드는 것이 좋다. 예를 들어 첫 번째 버전은 조용한 실내에서 한 명의 사용자 음성만 처리하고, 정해진 호출어와 몇 가지 명령만 지원해도 충분하다.
초기 버전의 목표는 다음 정도면 좋다.
호출어가 있는 짧은 명령을 인식한다.
STT 결과를 텍스트 명령으로 변환한다.
기존 LLM 계획기와 디스패처에 연결한다.
성공과 실패를 TTS로 알려 준다.
"멈춰" 명령을 즉시 처리한다.
그다음 단계에서 소음 환경, 여러 사용자, 확인 대화, 명령 수정, 작업 재개, 사용자별 음성 프로필 같은 기능을 추가하면 된다.
음성 인터페이스를 만들 때는 로그를 꼭 남겨야 한다. 특히 음성 로봇의 오류는 눈으로 보기 어렵다. 사용자는 "나는 분명히 컵이라고 말했는데 로봇이 컵받침으로 알아들었다"고 느낄 수 있다. 이때 원인을 찾으려면 오디오 구간, STT 결과, LLM 계획, 디스패처 결과, TTS 응답이 모두 기록되어야 한다.
{
"request_id": "voice-20260514-0031",
"audio_id": "audio-0031.wav",
"stt_text": "파란 컵을 집어 줘",
"stt_confidence": 0.82,
"normalized_command": "파란 컵을 집어 줘",
"llm_plan_id": "plan-0031",
"execution_status": "failed",
"failure_reason": "object_not_found",
"tts_response": "파란 컵을 찾지 못했어요."
}이런 로그는 디버깅뿐 아니라 안전 감사에도 중요하다. 로봇이 왜 움직였는지, 어떤 명령을 들었는지, 어떤 계획을 실행했는지 나중에 확인할 수 있어야 한다.
음성은 로봇을 더 로봇답게 만든다
키보드로 명령을 입력하는 로봇은 아직 컴퓨터 프로그램처럼 느껴진다. 반면 말을 알아듣고, 되묻고, 실행 결과를 알려 주는 로봇은 물리 세계의 동료처럼 느껴진다. 음성 인터페이스는 로봇 에이전트에 자연스러움을 더한다.
하지만 자연스러움만 추구하면 위험해진다. 음성은 오인식될 수 있고, 사용자는 애매하게 말할 수 있으며, 주변 사람의 말이 섞일 수 있다. 그래서 음성 명령 로봇의 핵심은 "말을 잘 알아듣는 것"과 "잘못 알아들어도 안전한 것"을 함께 설계하는 데 있다.
이 장에서 만든 구조를 정리하면 다음과 같다.
마이크는 소리를 듣는다.
STT는 소리를 텍스트로 바꾼다.
명령 관리자는 호출어, 중복, 긴급 정지, 확인 절차를 처리한다.
LLM 계획기는 텍스트 명령을 스킬 순서로 바꾼다.
디스패처는 검증된 스킬만 실행한다.
TTS는 실행 상태와 결과를 사용자에게 말한다.이제 로봇은 사용자의 말을 듣고 행동하며, 자신의 상태를 다시 말할 수 있다. 다음 장에서는 이 대화 능력을 교육용 인터페이스로 확장한다. 로봇이 단순히 명령을 수행하는 것을 넘어, 자신의 상태를 설명하고, 센서 데이터를 해석하고, 학습자에게 실험 가이드를 제공하는 AI 로봇 튜터로 발전하게 된다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.