본문으로 바로가기

Grok Voice Think Fast 2.0 API 튜토리얼: Python으로 실시간 음성 에이전트 만들기

Grok Voice Think Fast 2.0으로 실시간 음성 에이전트를 만들어, 음성 대화를 처리하고, 도구를 호출하며, 중단을 관리하고, 끊긴 세션을 재개하는 방법을 배웁니다.
업데이트됨 2026년 8월 9일  · 15분 읽다

AI로 탐색하기

ChatGPTClaudePerplexity

SpaceXAI의 Grok Voice Think Fast 2.0은 음성-대-음성 모델입니다. WebSocket으로 오디오를 보내면 오디오로 응답이 돌아오고, 그 사이에 모델은 스스로 호출하기로 한 함수가 이미 실행 중인 동안에도 추론하며 말을 이어갑니다. 별도의 음성-텍스트 변환이나 텍스트-음성 변환 단계가 없습니다.

SpaceXAI는 2026년 7월 29일 Think Fast 2.0을 발표했습니다. 첫 오디오가 더 빨라지고, 엄격히 교대하지 않고 말하면서 듣는 풀듀플렉스 동작이 더 안정적이며, 턴 초기에 도구 호출이 시작됩니다. 튜토리얼에서 중요한 건 벤치마크보다 코드가 어떻게 달라지는지이므로 수치는 간단히만 짚겠습니다.

온라인 스토어용 고객 지원 음성 에이전트를 만들어 보겠습니다. 발신자는 주문을 물어보고, 이메일로 주문을 찾고(번호가 없을 때), 배송 지시를 변경하거나 취소할 수 있으며, 에이전트의 말을 중간에 끊고, 연결이 끊긴 뒤 대화를 다시 이어갈 수 있습니다. 이는 API 경로를 사용하는 방식으로, 코드 없이 만드는 Voice Agent Builder는 Grok Voice Agent Builder 튜토리얼에서 다룹니다. 콘솔 중심으로 시작하려면 그 문서를 먼저 보세요.

Grok Voice Think Fast 2.0이란?

Grok Voice Think Fast 2.0은 SpaceXAI가 Speech to Speech API를 위해 공개한 최신 모델입니다. 대부분 그냥 Grok Voice라고 부르는 제품의 정식 명칭이죠. 회사를 아직 xAI로 기억한다면, 같은 조직입니다. 2026년 7월 6일 SpaceX에 통합되며 SpaceXAI로 리브랜딩되었습니다. 다만 API 식별자는 바뀌지 않아, 아래 모든 곳에서 xai를 그대로 사용합니다. XAI_API_KEY 환경 변수나 api.x.ai 호스트 등이 그렇습니다.

전통적인 음성 스택은 음성-텍스트, 언어 모델, 텍스트-음성의 세 서비스를 체인으로 엮습니다. 매 단계마다 지연이 늘고 맥락이 누락될 지점이 생기죠. Think Fast 2.0은 이를 하나의 모델로 묶어, 같은 연결에서 오디오 또는 텍스트를 입력받아 오디오 또는 텍스트로 출력합니다.

모듈식 STT-LLM-TTS 파이프라인과 단일 Grok Voice WebSocket 연결 비교 다이어그램.

음성-대-음성 WebSocket과 3-서비스 파이프라인 아키텍처 비교. 이미지: 필자.

말만 하는 에이전트가 아니라 행동하는 에이전트를 위해 중요한 점은 추론과 발화가 병렬로 돈다는 것입니다. SpaceXAI에 따르면 도구 호출은 대체로 첫 문장을 끝내기 전에 실행을 시작하며, 여기서의 "대체로"라는 표현에는 실제 의미가 담겨 있습니다.

SpaceXAI가 Artificial Analysis를 인용한 벤치마크에서 Think Fast 2.0은 Speech to Speech Index 82.9%를 기록했으며, 1.0의 75.7% 대비 향상되었습니다. 첫 오디오까지의 시간을 1.25초에서 0.70초로 줄였습니다. 다만 벤더의 일반 벤치마크 수치는 귀하의 콜 플로우에 대한 가설이지 테스트 계획은 아닙니다.

모델 문자열은 세 가지가 보입니다: grok-voice-latest, grok-voice-think-fast-2.0, grok-voice-think-fast-1.0. 프로토타이핑 동안 별칭은 편리하지만 그 이상에는 안정적이지 않습니다.

2026년 8월 4일 테스트 당시 grok-voice-latest는 여전히 grok-voice-think-fast-1.0으로 해석되었고, SpaceXAI의 릴리스 노트에는 다음 날 Think Fast 2.0으로 전환 예정이라고 적혀 있었습니다. 이 전환은 모델 변경이자 가격 변경으로, 1.0의 분당 $0.05에서 2.0은 분당 $0.08입니다. 버전 고정 없이 별칭을 쓰면 코드 한 줄 건드리지 않아도 비용이 올라갑니다. 배포에는 반드시 버전이 포함된 문자열을 고정하세요.

무엇을 만들 것인가

에이전트는 지원 라인에서 자주 받는 질문을 다룹니다. 주문 조회, 번호 없이 이메일로 찾기, 배송 지시 변경, 취소, 티켓 개설 또는 조회, 상담원 연결까지. 그 과정에서 중단과 연결 끊김도 처리합니다.

모든 기능을 하나의 스크립트로 묶기보다, 역할이 다른 작은 파일들로 나눕니다. 각 조각을 따로 테스트하기도 쉽습니다. 구조는 다음과 같습니다.

  • config.py: API 키를 로드하고 모델 문자열, 샘플레이트, 엔드포인트 URL을 보관

  • voice_client.py : WebSocket 래핑, 과금 추적, 송수신 헬퍼 제공

  • tools.py: 주문 관련 함수 정의와, 실제 데이터베이스를 대신하는 소규모 인메모리 주문 저장소

  • assistant.py : 시스템 프롬프트, 세션 설정, 모든 것을 연결하는 이벤트 루프 보관

  • token_server.py: 에페메럴 토큰을 발급하는 작은 FastAPI 엔드포인트

  • app_streamlit.py: 동일한 클라이언트를 실시간 브라우저 통화로 감싸며, 테스트 섹션 뒤에 다시 다룹니다

학습 경로는 터미널에서 시작합니다. 데모는 마이크를 더합니다.

사전 준비

API 키가 있는 SpaceXAI 계정과 결제 수단이 필요합니다(상시 무료 티어는 없고, 신규 계정의 프로모션 크레딧만으로는 부족합니다). 또한 asyncio와 WebSocket에 익숙해, await를 줄 단위로 설명하지 않아도 따라올 수 있어야 합니다.

SpaceXAI의 퀵스타트 예제는 전용 SDK 대신 순수 websockets 패키지를 사용하며, 우리도 동일합니다. 문서에는 필요한 Python 버전이 명시돼 있지 않습니다. 저는 3.11에서 테스트했습니다.

API 키는 서버에 보관하세요. 브라우저나 모바일 앱이 Voice API에 직접 연결한다면 실제 키 대신 에페메럴 토큰을 사용합니다. 아래 보안 섹션에서 다룹니다.

프로젝트 설정

아래 모든 파일은 프로젝트 저장소에 있으니, 코드 조각을 복사하지 않고 바로 클론하셔도 됩니다.

git clone https://github.com/KhalidAbdelaty/grok-voice-think-fast-2.0.git
cd grok-voice-think-fast-2.0
pip install -r requirements.txt

websockets는 실시간 연결을 담당하고 python-dotenv는 키를 읽어옵니다. 나머지는 토큰 엔드포인트와 브라우저 데모용입니다. 키는 .env에 넣으세요.

XAI_API_KEY=xai-your-key-here

준비는 이게 대부분입니다. 흥미로운 부분은 연결입니다.

Grok Voice Realtime API 이해하기

Grok Voice는 제품 이름입니다. 실제로 코드를 붙이는 대상은 wss://api.x.ai/v1/realtime WebSocket 엔드포인트이며, 전체 대화는 하나의 소켓 위에서 JSON 이벤트 스트림으로 오갑니다.

이벤트 생명주기

연결은 정해진 형태를 따릅니다. 연결 즉시 서버가 session.created conversation.created를 보내고, 클라이언트는 음성, 도구 등을 설정하려고 session.update 를 보냅니다. 서버는 session.updated로 확인을 돌려주며, 그다음부터는 대화 아이템을 만들고 응답을 요청합니다. 실제 키로 테스트했을 때 문서와 순서가 정확히 일치했습니다.

  • session.update (클라이언트): 음성, 지침, 도구, 오디오 포맷 구성

  • conversation.item.create (클라이언트): 사용자 메시지, 어시스턴트 메시지, 도구 결과 추가

  • response.create (클라이언트): 모델에 발화를 요청; 서버 VAD가 자동으로 전송

  • response.output_audio.deltaresponse.output_audio_transcript.delta (서버): 생성되는 답변을 스트림으로 전송

  • response.done (서버): 턴 종료

두 가지가 사람들을 자주 헷갈리게 합니다. 위에서 링크한 Speech to Speech 문서 페이지에는 세션 재개 중 conversation.item.created 이벤트가 언급되지만, 정본인 이벤트 레퍼런스에는 conversation.item.added만 나옵니다. 제가 실행한 모든 테스트에서도 도착한 것은 이것뿐이니, 여기에 맞춰 코드를 작성하세요. 또한 문서화되지 않은 ping 이벤트가 대부분의 연결에서 몇 초 뒤 나타납니다. 오류로 오해하지 않도록 여기서만 언급합니다.

오디오 포맷과 전송

코덱과 전송 방식은 별개의 선택입니다. 코덱은 audio.input.format audio.output.format에서 설정하며, audio/pcm(Linear16, 기본 24000 Hz), audio/pcmu 또는 audio/pcma (전화용 G.711, 8 kHz), 혹은 audio/opus(24 kHz)입니다. 전송은 이런 바이트가 와이어를 타고 이동하는 방식입니다.

  • json(기본): 오디오를 input_audio_buffer.append response.output_audio.delta 내부의 base64 텍스트로 전송, 로깅과 디버그가 쉬움

  • binary : base64 오버헤드를 건너뛰고 코덱 바이트를 WebSocket 바이너리 프레임으로 전송하지만, 수신 루프에서 메시지 타입에 따라 분기해야 함

JSON으로 시작하세요. 문서의 모든 예제가 이를 사용하며, 확인이 쉽고, 지원 에이전트 빌드에서 병목은 base64 오버헤드가 아닙니다. 필요가 측정되면 바이너리로 옮기세요.

OpenAI Realtime API와의 호환성

OpenAI의 Realtime API를 써본 적 없다면 이 섹션은 건너뛰셔도 됩니다. 사용해본 분들을 위해, 이 Speech to Speech API는 OpenAI Realtime API를 매우 가깝게 따라가므로, 대부분의 클라이언트 코드는 베이스 URL과 키만 바꿔도 포팅됩니다. 다만 완벽한 드롭인 호환은 아닙니다.

여기서는 트랜스크립트가 OpenAI의 delta 대신 conversation.item.input_audio_transcription.updated로 도착합니다. 몇몇 OpenAI 이벤트는 지원되지 않으며, SpaceXAI 고유 확장도 있습니다. 고지 멘트를 스크립트대로 내보내는 force_message, 재연결용 resumption, 텍스트-음성 변환 전에 잘못 발음되는 브랜드명을 고치는 replace 같은 것들입니다.

실시간 음성 에이전트 만들기

프로토콜 설명은 충분합니다. 이제 여기에 연결하는 클라이언트입니다.

연결 및 세션 설정

연결은 베어러 토큰과 모델 쿼리 파라미터로 열며, 첫 메시지에서 에이전트의 동작 방식을 모두 구성합니다.

import asyncio
import json
import os
import websockets

MODEL = "grok-voice-think-fast-2.0"  # pin the version, not grok-voice-latest

async def connect():
    url = f"wss://api.x.ai/v1/realtime?model={MODEL}"
    ws = await websockets.connect(
        url, additional_headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"}
    )
    await ws.send(json.dumps({
        "type": "session.update",
        "session": {
            "voice": "eve",
            "instructions": SYSTEM_PROMPT,
            "turn_detection": {"type": "server_vad"},
            "tools": ORDER_TOOLS,
            "resumption": {"enabled": True},
        }
    }))

return ws

instructions는 시스템 프롬프트입니다. 이 모델은 짧은 프롬프트를 선호합니다. SpaceXAI의 마이그레이션 노트에 따르면 과거 GPT 계열 음성 모델용 프롬프트를 그대로 옮기기보다 간결하게 다듬으라고 합니다. 제 프롬프트는 에이전트에게 짧게 답하고, 한 번에 한 질문만 하며, 쓰기 작업 전에 읽어서 확인하라고 지시합니다. 음성 확인은 UX 차원의 배려이지 보안 통제는 아닙니다. 실제 권한 검증은 애플리케이션이 쓰기 작업 자체에서 시행해야 합니다.

한 가지 놀랐던 점: 인식되지 않는 모델 문자열을 사용해도 연결 시점에 오류를 내지 않고, 조용히 grok-voice-think-fast-1.0으로 폴백합니다. 오타 때문에 유료 요청이 다운그레이드되는데도 알리지 않는 건 이례적입니다. 시작 시 한 번 session.createdsession.model 필드를 로깅해, 요청한 모델이 맞는지 확인하세요.

WebSocket 연결 후 session.created 이벤트를 출력하는 터미널

연결 후 session.created가 표시된 터미널 출력. 이미지: 필자.

사용자 오디오 스트리밍

turn_detection.typeserver_vad로 설정하면 오디오를 계속 추가하기만 하면 됩니다. 발신자가 말을 멈췄는지 서버가 판단하고, 응답 트리거도 서버가 처리합니다. 이를 null로 두면 그 판단을 직접 맡아, 턴이 끝났다고 보이는 시점에 버퍼를 명시적으로 커밋해야 합니다.

async def send_audio_chunk(ws, pcm_bytes: bytes):
    await ws.send(json.dumps({
        "type": "input_audio_buffer.append",
        "audio": base64.b64encode(pcm_bytes).decode(),
    }))

서버 VAD에는 세 가지 조절값이 있으며, 이들을 잘못 맞추는 것이 로그에는 오류가 없는데 에이전트가 망가진 것처럼 느껴지는 가장 흔한 원인입니다. 기본적으로 session.updated의 에코에는 이 값들이 나타나지 않으니, 가정하지 말고 문서를 확인하세요.

  • threshold(0.1~0.9, 기본 0.85): 어느 정도 커야 음성으로 간주할지. 시끄러운 환경에서는 높이고, 조용한 화자가 누락되면 낮추세요.

  • silence_duration_ms: 발신자가 얼마나 오래 조용해야 턴을 끝낼지. 너무 짧으면 생각 중인 말을 자르고, 너무 길면 굼떠 보입니다.

  • prefix_padding_ms(기본 333): 음성 감지 직전의 오디오 일부를 보존해 첫 음절이 잘리지 않게 합니다.

silence_duration_ms를 먼저 조정하세요. 발신자가 생각하려고 잠시 멈췄을 때 말이 자주 끊긴다면 이 값을 가장 먼저 만집니다.

응답 수신 및 재생

오디오는 response.output_audio.delta로 작은 조각으로 도착합니다. 스트리밍의 요지는, response.done을 기다리지 말고 도착 즉시 재생하는 것입니다.

async def play_response(ws):
    async for message in ws:
        event = json.loads(message)
        if event["type"] == "response.output_audio.delta":
            chunk = base64.b64decode(event["delta"])
            speaker.write(chunk)  # your playback call goes here
        elif event["type"] == "response.output_audio_transcript.delta":
            print(event["delta"], end="", flush=True)

프로덕션에서도 트랜스크립트를 유지하세요. 발신자가 "이상한 말을 했다"고 할 때 가장 저렴한 디버깅 도구입니다.

음성 에이전트에 도구 추가

말만 하는 음성 에이전트는 마이크 달린 챗봇에 불과합니다.

주문 도구 만들기

각 도구는 JSON 스키마와, 우리 측의 평범한 Python 함수로 이루어집니다. 모델은 데이터베이스에 직접 접근하지 않으며, 함수가 반환한 결과만 봅니다.

ORDER_TOOLS = [
    {
        "type": "function",
        "name": "check_order_status",
        "description": "Look up the status, ETA, and delivery instructions for an order.",
        "parameters": {
            "type": "object",
            "properties": {
                "order_number": {"type": "string", "description": "e.g. ORD-1042"},
            },
            "required": ["order_number"],
        },
    },
    # find_orders, update_delivery_instructions, cancel_order,
    # create_support_ticket, check_ticket_status and transfer_to_human
    # all follow the same shape
]

읽기 작업(check_order_status)은 타임아웃 시 재시도가 안전합니다. 쓰기 작업은 그렇지 않습니다. 예를 들어 update_delivery_instructions를 모호한 타임아웃 후 재시도하면 같은 변경이 두 번 적용될 수 있습니다. 프롬프트에 확인 멘트를 넣어도 이를 막지 못하므로, 쓰기에는 멱등성 키나 중복 검사 같은 장치를 두세요.

거절 로직도 함수에 넣으세요. cancel_order는 배송된 주문에 대해서는 취소 대신 사유와 대안을 반환합니다. "배송된 주문은 절대 취소하지 말라"는 프롬프트는 제안일 뿐이지만, 함수를 통한 거절은 확실합니다.

도구 호출 루프 처리

네 단계가 있으며, 순서가 생각보다 중요합니다. 모델이 response.function_call_arguments.done를 보냅니다. 그러면 코드가 함수를 실행하고, 결과를 function_call_output 아이템으로 다시 보내며, 그 다음에야 모델에 계속하라고 요청합니다.

async def handle_tool_call(ws, event):
    args = json.loads(event["arguments"])
    result = execute(event["name"], args)  # never raises; errors come back as {"error": ...}
    await ws.send(json.dumps({
        "type": "conversation.item.create",
        "item": {
            "type": "function_call_output",
            "call_id": event["call_id"],
            "output": json.dumps(result),
        },
    }))

요청에 두 개 이상의 도구가 필요하면, 오디오가 나오기 전에 function_call_arguments.done 이벤트가 여러 번 옵니다. 이들을 모두 처리해 결과를 전부 보낸 다음에야 단 하나의 response.create를 보내세요. 너무 일찍 보내면 아직 진행 중인 호출의 맥락 없이 모델이 답합니다.

여기에는 SpaceXAI가 문서에 적어둔, 그럼에도 불구하고 제가 첫 시도에서 겪은 함정이 있습니다. 도구 결과를 보낸 직후 곧바로 response.create를 보내면, 에이전트가 아직 도입 문장을 재생 중인 것과 겹칠 수 있습니다. 한 번은 "주문 ORD-1042 상태를 바로 확인해 드릴게요"라고 시작하며 문장 중간에 도구를 호출해, 즉시 응답을 보내면 자기 말을 덮어버릴 뻔했습니다.

현재 턴의 오디오가 끝날 때까지 기다리고, 그 사이 짧은 "생각 중" 상태를 보여주세요.

function_call_arguments.done에서 핸들러 실행, function_call_output 전송, 그 다음 response.create로 이어지는 흐름.

응답을 이어가기 전 도구 호출 흐름. 이미지: 필자.

중단과 대화 상태 관리

두 가지 별개의 문제입니다. 에이전트의 응답 도중 발신자가 말을 겹치는 경우, 그리고 WebSocket이 끊겨 다시 이어야 하는 경우입니다.

자연스러운 중단 지원

server_vad가 켜져 있으면 바지인은 서버에서 자동 처리됩니다. 발신자가 다시 말하기 시작했다고 감지하는 순간, 서버는 input_audio_buffer.speech_started를 신호로 보내고 기존 응답 생성을 중단합니다. 클라이언트 쪽은 핸드셰이크의 나머지 절반을 맡아, 대기열에 쌓인 오디오를 비워 에이전트가 아무도 원치 않는 문장을 끝까지 말하지 않게 해야 합니다.

if event["type"] == "input_audio_buffer.speech_started":
    playback_queue.clear()

수동(비 VAD) 세션에서는 요청으로 response.cancel이 같은 일을 합니다. 또한 conversation.item.truncate로 실제로 들린 부분까지만 어시스턴트 아이템을 잘라낼 수 있습니다. 문서에는 존재만 확인되고, 라이브 바지인 중 언제 호출해야 하는지는 적혀 있지 않으니 타이밍은 직접 테스트하세요.

중간에 배송 지시 변경을 넣고 테스트했습니다. 요청을 시작한 뒤, 에이전트의 확인 멘트 도중 다른 주소로 끼어드는 방식입니다. 중요한 건 오디오가 멈췄는지가 아니라, 에이전트가 조용히 이전 지시를 마무리하지 않고 수정된 지시를 적용했는지입니다. 침묵이 아니라 주문 기록을 기준으로 검증하세요. 이건 마지막의 브라우저 데모에서 직접 들을 수 있습니다.

끊긴 세션 재개

세션 재개는 옵트인 기능이며, 메모리와는 다릅니다. session.update에서 resumption.enabled: true로 설정하고, conversation.created 이벤트에서 ID를 가져오세요. 소켓이 끊기면, URL에 ?conversation_id=<id>를 붙여 재연결하고, 새 연결에서도 다시 옵트인합니다.

async def reconnect(conversation_id):
    url = f"wss://api.x.ai/v1/realtime?model={MODEL}&conversation_id={conversation_id}"
    ws = await websockets.connect(url, additional_headers=auth_header)
    await ws.send(json.dumps({"type": "session.update", "session": {"resumption": {"enabled": True}}}))
    return ws

캐시된 턴, 트랜스크립트, 도구 호출과 결과가 다음 질문 전에 재생되며, 비활성 30분 후 캐시는 사라집니다. 주문을 물어보고 연결을 끊은 뒤, 다시 연결해 되묻는 방식으로 테스트했을 때 ETA를 정확히 이어받았습니다.

문서에 없는 한 가지: 리플레이가 즉시 도착하지 않으므로, 소켓이 열리자마자 보낸 질문이 이를 앞질러, 이전 턴을 기억하지 못한 답변이 돌아올 수 있습니다. 재개 기능을 탓하기 전에 한 박자 기다리세요.

연결 끊김, conversation_id로 재연결, 그리고 올바른 후속 답변의 터미널 로그.

재개된 세션의 터미널 로그. 이미지: 필자.

이 기능을 자체 데이터베이스의 주문 상태 저장 대신 사용하지 마세요. 캐시가 만료되거나 다음 날 다시 전화가 오면 맥락은 0에서 시작하는 게 설계 의도입니다.

에이전트 보안 및 모니터링

영구 API 키를 브라우저나 모바일 코드에 넣지 마세요. 클라이언트가 서버를 거치지 않고 직접 연결해야 한다면, 수명이 짧은 토큰을 발급하세요.

from fastapi import FastAPI
import httpx, os

app = FastAPI()

@app.post("/session")
async def create_session():
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://api.x.ai/v1/realtime/client_secrets",
            headers={"Authorization": f"Bearer {os.environ['XAI_API_KEY']}"},
            json={"expires_after": {"seconds": 300}},
        )
    return response.json()  # {"value": "xai-realtime-client-secret-...", "expires_at": ...}

브라우저는 WebSocket 핸드셰이크에서 커스텀 Authorization 헤더를 설정할 수 없기 때문에, sec-websocket-protocol 헤더를 통해 토큰을 전달합니다. 이때 xai-client-secret. 접두사를 붙입니다.

서버가 브라우저에 단기 클라이언트 시크릿을 발급해 WebSocket을 여는 다이어그램.

서버가 토큰을 발급하고, 브라우저가 통화에 참여. 이미지: 필자.

과금은 두 가지 미터로 이뤄집니다. 오디오는 송수신 모두 앞서 언급한 분당 $0.08(시간당 $4.80)이며, 오디오가 아니고 function_call_output도 아닌 모든 conversation.item.create는 건당 $0.004의 고정 요금입니다. response.create는 과금되지 않습니다. 각 response.done에는 usage 객체가 포함되며, 제 테스트에서는 output_audio_seconds와 별도의 billable_audio_seconds가 보고되었습니다. 추정치가 아닌 이 값들을 기준으로 과금하세요.

Speech to Speech API의 문서화된 제한은 팀당 동시 10개 세션, 세션당 120분이며, 둘 다 us-east-1에 적용됩니다. 수용량 계획을 Voice Agent API의 숫자(서로 다름)에 기대지 마세요.

프라이버시에 관해선 정확히 고지하세요. SpaceXAI의 보안 FAQ에 따르면 API 요청/응답은 오남용 모니터링을 위해 30일간 암호화되어 보관되며, 허가 없이 학습에 사용되지 않습니다. 팀은 Zero Data Retention을 켤 수 있지만, ZDR을 켜면 보존되는 음성 에이전트 대화 기록이 없어지므로 재개 기능은 작동하지 않습니다.

통화가 녹음되거나 AI가 처리한다는 사실을 고지해야 한다면, 앞서 언급한 force_message 확장을 사용하세요. 모델이 임의로 바꾸지 않고, 작성한 문구 그대로 재생됩니다.

음성 에이전트 테스트

WebSocket 핸드셰이크에서 200이 떴다고 에이전트가 제대로 동작했다는 뜻은 아닙니다. 연결이 아니라 결과를 테스트하세요.

  • 정상적인 주문 조회: 응답이 왔는지 여부가 아니라, 발화 내용이 기록과 일치하는지 확인
  • 중단된 응답: 재생이 멈추고 에이전트가 새 요청을 처리하는지 확인
  • 확인이 필요한 배송 변경: 주문 기록을 기준으로 검증
  • 거절: 발송된 주문 취소처럼 규칙을 설명해야 하는 경우, 사과가 아니라 규칙을 설명하는지
  • 알 수 없는 주문 번호: 상태를 지어내지 않고 모른다고 말하는지
  • 오류를 반환하는 도구: 에이전트가 멈추지 않고 오류를 말하는지
  • 재연결과 재개: 앞서 언급한 리플레이 타이밍 포함
  • 시끄러운 오디오, 빠른 말하기, 숫자와 주소를 철자대로 말하는 발신자

이 글을 쓰면서 대부분을 실키로 돌려봤습니다. 흥미로운 실패는 에러가 아니라 동작상의 것이었습니다. 위의 재개 타이밍, 그리고 범위를 벗어난 VAD 임계값이 거부되지 않고 받아들여지는 문제처럼, 행복 경로만 테스트하면 조용히 깨진 채로 배포될 수 있는 유형입니다. 다국어 테스트도 추가하고, 언어 지정을 어떻게 하는지에 관한 자주 묻는 질문의 주의점을 보세요.

이 중 두 가지는 타이핑만으로는 테스트할 수 없습니다. app_streamlit.pyStreamlit 페이지로, 브라우저에서 실시간 통화를 제공합니다. 마이크는 WebRTC를 통해 같은 WebSocket으로 스트리밍되고, 에이전트의 음성도 스트리밍되어 돌아오며, 소켓은 내내 열려 있습니다.

streamlit run app_streamlit.py
에이전트의 말을 중간에 끊기. 영상: 필자.

에이전트 말 위로 말을 겹치면 멈춥니다. speech_started가 도착하고 페이지가 대기 중인 오디오를 비우기 때문입니다. 중단 섹션에서 설명한 핸드셰이크가 실제로 작동하는 모습입니다.

트랜스크립트보다 주문 기록을 보세요. 에이전트는 배송 변경을 읽어주고 완료했다고 말하지만, 기록이 실제로 바뀌었는지가 관건입니다. 헤드폰을 사용하세요. 오픈 스피커에서는 에이전트가 자기 목소리를 듣고 바지인으로 간주해 자신의 문장을 스스로 자르기도 합니다. 이는 스피커폰 발신자가 만들어내는 상황의 예고편이기도 합니다.

Grok Voice Think Fast 2.0의 한계와 배포 시 고려사항

다음 상황을 대비하세요. 턴 도중 실패하는 도구 호출, 행동이 성공했는지보다 확인 멘트를 더 자신감 있게 말하는 모델, 조용한 사무실에 맞춰 튜닝한 VAD가 전화선에서 무너지는 경우, 문장 도중 마음을 바꾸는 발신자 등.

결제, 계정 접근, 혹은 발신자가 혼란스럽거나 언짢게 들리는 경우에는 사람에게 라우팅하세요. 이를 위해 모델에 transfer_to_human 도구를 제공합니다. 없으면 사과로 임기응변할 뿐, 에스컬레이션하지 않습니다.

모듈식 음성-텍스트, 언어 모델, 텍스트-음성 스택도 여전히 자리가 있습니다. 각 구성요소를 개별적으로 제어하고, 추론 전에 결정적인 트랜스크립트를 확보할 수 있지만, 통합 작업이 더 필요합니다. 그리고 실시간 대화가 필요 없는 업무라면, 텍스트 챗봇이나 배치 전사는 말을 하지 않는 실시간 파이프라인보다 단순하고 저렴합니다.

결론

이 글의 테스트 전반에서 grok-voice-think-fast-2.0 은 대체로 문서에 적힌 대로 동작했습니다. 이벤트 생명주기는 유지되었고, 끊긴 연결은 이전 턴을 보존한 채 돌아왔으며, 모델은 오프닝 라인을 말하면서 도구를 호출했습니다.

conversation.item.added의 명명 불일치를 제외하면, 소켓의 우리 쪽에서 해야 할 일이 얼마나 많은지가 중요합니다. 재생 대기열, 언제 침묵해야 하는지, 언제 아직 다음 질문을 하지 말아야 하는지 등입니다.

오늘 프로젝트를 시작한다면, 별칭이 아닌 버전 고정 모델 문자열, 세 가지 중에서는 먼저 server_vadsilence_duration_ms를 조정하고, 바이너리가 필요한 명확한 이유가 생길 때까지 JSON 전송, 첫 session.update에서 resumption.enabled 켜기, 시작 시 session.model 로깅을 기본값으로 둘 것입니다.

어떤 음성 에이전트에도 적용할 습관은 다음과 같습니다. 발화 확인이 아니라 기록과 대조해 쓰기 작업을 검증할 것, 거절 로직은 프롬프트가 아니라 도구에 둘 것, 다음 response.create 전에 재생을 비울 것, 그리고 실제 악센트, 실제 소음, 실제 실패 양상을 테스트할 것.

분명한 확장은 전화 통신(SpaceXAI가 SIP 지원을 문서화), 에페메럴 토큰 기반 브라우저 클라이언트, 실제 CRM에 대한 MCP 연결, 그리고 제대로 된 다국어 버전입니다. 그리고 세션 제한을 비교했던 Voice Agent API가 필요에 더 가깝다면, Grok Voice Agent API 튜토리얼을 참고하세요.

FAQs

운영 환경에서 grok-voice-latest를 사용해도 안전한가요?

엄밀히 말해, 위의 버전 관리 섹션에서 언급했듯 안전하지 않습니다. 전환 시점은 SpaceXAI가 정하며, 청구도 함께 바뀝니다. 운영 환경에서는 grok-voice-think-fast-2.0을 고정하고, 예상치 못한 전환이 실사용 통화에 영향을 주지 않는 로컬 실험에만 별칭을 쓰세요.

Grok Voice Think Fast 2.0은 영어 외 다른 언어도 지원하나요?

네, 자동 감지와 함께 20개 이상이 문서화되어 있습니다. language_hint로 특정 언어에 치우치게 할 수도 있습니다. 스페인어와 포르투갈어는 es-MXpt-BR처럼 지역 코드를 요구합니다. espt 같은 포괄 코드는 허용되지 않으며, 인식되지 않는 코드는 조용히 무시되고 자동 감지로 폴백하므로, 오타가 비용을 초래하지는 않지만 아무 효과도 없습니다.

음성을 바꿀 수 있나요? 몇 가지가 있나요?

eve가 문서에 있는 기본 음성이며, ara, rex, sal, leo도 제공되고 커스텀 음성 ID도 가능합니다. 현재 목록은 GET /v1/tts/voices로 확인하세요. 말 속도가 마음에 들지 않으면 audio.output.speed에 0.7~1.5를 설정할 수 있습니다.

에이전트가 지금보다 더 빨리 대답하게 만들 수 있나요?

워크스루에서는 기본값이 보통 적절해 생략했지만, reasoning.effort를 시도해 보세요. 기본은 "high"이며 "none"도 가능합니다. 턴당 계획량을 줄여 더 빠르게 대답하지만, 단순 조회 흐름에나 적합합니다. 도구 사이에서 선택해야 하는 작업에는 손대지 않겠습니다.

이걸 만들려면 SpaceXAI 공식 SDK가 필요한가요?

아니요. 사전 준비 섹션에서 언급했듯 필수는 아닙니다. 순수 websockets 패키지나, api.x.ai 베이스 URL로 지정한 OpenAI 호환 클라이언트가 모두 잘 동작합니다. 알아둘 점 하나: 공식 xai-sdk는 이 WebSocket과 통신하지 않는 별도의 gRPC 클라이언트입니다. 그러니 그 SDK에서 실시간 메서드를 찾지 마세요. 제 예시 외의 출발점을 원한다면 xai-cookbook에 iOS, 웹, WebRTC, 텔레포니 샘플이 있습니다.

주제
인공지능

DataCamp와 함께 배우기

courses

인공 지능 이해하기

2
420.6K
머신 러닝, 딥러닝, NLP, 생성형 AI 등 인공 지능의 기본 개념을 학습합니다.
자세히 보기Right Arrow
강좌 시작
더 보기Right Arrow