Руководство по API Grok Voice Think Fast 2.0: создаём голосового агента реального времени на Python
Узнайте, как использовать Grok Voice Think Fast 2.0 для создания голосового агента реального времени, который ведёт разговоры, вызывает инструменты, обрабатывает перебивания и возобновляет оборванные сессии.
Grok Voice Think Fast 2.0 от SpaceXAI — это модель «речь-в-речь». Вы отправляете ей аудио по WebSocket, а она возвращает аудио; при этом модель может рассуждать и продолжать говорить, пока уже запущен выбранный ею вызов функции. Никаких отдельных шагов распознавания речи и синтеза речи.
SpaceXAI объявила о Think Fast 2.0 29 июля 2026 года: более быстрое первое аудио, более стабильный полнодуплексный режим (слушает, пока говорит, а не строго по очереди) и ранние вызовы инструментов в рамках реплики. На бенчмарках долго не задерживаюсь: для практикума важнее, что меняется в вашем коде.
Мы построим голосового агента службы поддержки интернет‑магазина. Звонящий может спросить о заказе, изменить инструкции по доставке, отменить, перебить агента посреди фразы и продолжить диалог после обрыва соединения. Это путь через API, а не конструктор без кода, который разбирает наш учебник по Grok Voice Agent Builder. Для версии через консоль начните оттуда.
Что такое Grok Voice Think Fast 2.0?
Grok Voice Think Fast 2.0 — новейшая модель SpaceXAI для Speech to Speech API — продуктового названия того, что большинство называет просто Grok Voice. Если вы всё ещё думаете о компании как о xAI — это та же команда: 6 июля 2026 года она была интегрирована в SpaceX и переименована в SpaceXAI. API названия не менял, поэтому все идентификаторы ниже по‑прежнему содержатxai — от переменной XAI_API_KEY до хоста api.x.ai.
Классический голосовой стек — это цепочка из трёх сервисов: распознавание речи, языковая модель, затем синтез речи. Каждый переход добавляет задержку и риск потери контекста. Think Fast 2.0 сводит всё к одной модели, которая принимает на вход аудио или текст и по тому же соединению выдаёт аудио или текст.
WebSocket «речь‑в‑речь» против трёхсервисной архитектуры. Иллюстрация автора.
Для агента, который действует, а не только говорит, важно, что рассуждение и речь идут параллельно. По словам SpaceXAI, вызовы инструментов «обычно» стартуют до того, как агент закончит первую фразу — и это «обычно» тут не случайно.
На бенчмарках, на которые ссылается SpaceXAI — Artificial Analysis — Think Fast 2.0 набирает 82,9% в Speech to Speech Index против 75,7% у 1.0 и сокращает время до первого аудио с 1,25 секунды до 0,70 секунды. Показатели вендора на общем бенчмарке — это гипотеза о вашем звонке, а не план тестирования.
Есть три строковых названия модели: grok-voice-latest, grok-voice-think-fast-2.0 и grok-voice-think-fast-1.0. Псевдоним удобен на этапе прототипирования и недостаточно надёжен для чего‑то большего.
Когда я тестировал 4 августа 2026 года, grok-voice-latest всё ещё разрешался в grok-voice-think-fast-1.0, а в заметках о релизах SpaceXAI перенос на Think Fast 2.0 был намечен на следующий день. Этот переключатель — смена не только модели, но и цены: $0,08 за минуту аудио против $0,05 у 1.0, так что незакреплённый псевдоним подорожает, хотя вы не меняли ни строки кода. В развёртывании закрепляйте версию явной строкой.
Что мы построим
Агент покрывает типичные запросы линии поддержки: найти заказ, найти его по email, если у звонящего нет номера, изменить инструкции по доставке, отменить, открыть или проверить тикет и передать звонок оператору. По пути будут перебивания и обрыв соединения.
Это несколько небольших файлов, а не один скрипт: у каждого своя задача, и вы захотите тестировать их по отдельности. Структура такая:
config.py загружает ключ API и хранит строку модели, частоту дискретизации и URL конечных точек
voice_client.pyинкапсулирует WebSocket, ведёт учёт биллинга и предоставляет помощники отправки/приёма
tools.py определяет функции для заказов и небольшой in-memory хранилище заказов вместо реальной базы
assistant.pyсодержит системный промпт, конфигурацию сессии и цикл обработки событий, который всё связывает
token_server.py — небольшой эндпоинт FastAPI, который выпускает эфемерные токены
app_streamlit.py даёт тот же клиент в браузерном звонке в реальном времени; к нему вернусь после раздела о тестировании
Обучающий путь — из терминала. Демонстрация добавляет микрофон.
Предварительные требования
Вам нужна учётная запись SpaceXAI с ключом API, пополненный платёжный метод (постоянного бесплатного тарифа нет, промокредиты для новых аккаунтов не спасут) и достаточный уровень владения asyncio и WebSocket, чтобы следовать без построчного разбора await.
В примерах быстрого старта SpaceXAI используется чистый пакет websockets, а не отдельный SDK — так поступим и мы. В документации нет требований к версии 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:
Это почти вся подготовка. Интересное — в соединении.
Понимание Grok Voice Realtime API
Grok Voice — это продуктовое имя. Фактически вы работаете с WebSocket-эндпоинтом по адресу wss://api.x.ai/v1/realtime, и весь диалог — это поток JSON‑событий по этому одному сокету.
Жизненный цикл событий
Соединение следует фиксированному шаблону: сервер сразу после подключения отправляетsession.createdи conversation.created, вы отправляете session.updateдля настройки голоса и инструментов, сервер подтверждает session.updated, и дальше вы создаёте элементы беседы и запрашиваете ответы. Я прогонял это с живым ключом — порядок точно совпал с документацией.
session.update(клиент) настраивает голос, инструкции, инструменты и формат аудио
conversation.item.create(клиент) добавляет сообщение пользователя, ассистента или результат инструмента
response.create(клиент) просит модель говорить; серверный VAD отправляет это за вас автоматически
response.output_audio.delta и response.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 Гц), audio/pcmu илиaudio/pcma(G.711 на 8 кГц, для телефонии), илиaudio/opus (24 кГц). Транспорт — это способ передачи байтов по сети:
json (по умолчанию) отправляет аудио как base64‑текст внутри input_audio_buffer.appendи response.output_audio.delta, удобно для логирования и отладки
binaryшлёт «сырые» байты кодека как бинарные кадры WebSocket, исключая накладные расходы base64 ценой усложнения цикла приёма (ветвление по типу сообщения)
Начните с JSON. Все примеры в документации используют его, он тривиально инспектируется, а накладные расходы base64 — не то, что станет узким местом для агента поддержки. Переходите на binary, только если замеры дадут повод.
Совместимость с OpenAI Realtime API
Пропустите, если вы никогда не работали с Realtime API OpenAI. Для остальных: Speech to Speech API достаточно близко следует OpenAI Realtime API, так что большую часть клиентского кода можно перенести, сменив базовый URL и ключ, но это не идеальная замена один‑к‑одному.
Транскрипты приходят как conversation.item.input_audio_transcription.updated вместо openai-шногоdelta, некоторые события OpenAI не поддерживаются, а SpaceXAI добавляет свои расширения: force_message для обязательной фразы раскрытия,resumption для переподключений и replaceдля исправления произношения брендов до синтеза речи.
Создание голосового агента в реальном времени
Хватит о протоколе. Вот клиент, который с ним говорит.
Подключение и настройка сессии
Соединение открывается с bearer‑токеном и параметром модели в запросе, а первое ваше сообщение задаёт всё поведение агента:
import asyncio
import json
import os
import websockets
MODEL ="grok-voice-think-fast-2.0"# pin the version, not grok-voice-latestasyncdefconnect():
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.model из session.created один раз при старте и проверяйте, что получили то, что просили.
Вывод терминала с session.created после подключения. Иллюстрация автора.
Стриминг пользовательского аудио
Сturn_detection.type, установленным в server_vad, вам достаточно просто добавлять аудио. Сервер сам решает, когда звонящий закончил говорить, и инициирует ответ. Поставьте null — и решение будет на вас, вы явно фиксируете буфер, когда считаете, что реплика окончена.
asyncdefsend_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.
asyncdefplay_response(ws):asyncfor 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 hereelif 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 после неоднозначного таймаута может применить одно и то же изменение дважды. Фраза подтверждения в промпте это не предотвратит, поэтому давайте на записи idempotency‑ключ или проверку на дубликаты.
Отказы тоже закладывайте в функцию. cancel_order возвращает причину и альтернативу вместо отмены отправленного заказа, потому что «никогда не отменять отправленные заказы» в промпте — это пожелание, а отказ в функции — факт.
Обработка цикла вызова инструментов
Четыре шага, и порядок важнее, чем кажется. Модель присылает response.function_call_arguments.done, ваш код выполняет функцию, вы отправляете результат назад как элемент function_call_output— и только затем просите модель продолжить.
asyncdefhandle_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» и вызвал инструмент посреди фразы, так что мгновенный ответ заговорил бы поверх вступления.
Дождитесь завершения текущего аудио и покажите короткое состояние «думает» между ними.
Поток вызова инструмента перед продолжением ответа. Иллюстрация автора.
Управление перебиваниями и состоянием беседы
Здесь две разные задачи. Звонящий перебивает агента в середине ответа, и 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для обрезки элемента ассистента до реально услышанного. В документации подтверждается его наличие, но не указано, когда именно его вызывать при живом барж‑ине — подберите тайминг опытным путём.
Я проверял это на изменении инструкции по доставке в середине ответа: начать запрос, перебить другой деталью адреса во время подтверждения агента. Важно, чтобы агент применил скорректированную инструкцию, а не тихо довёл старую, а не то, остановилось ли аудио. Сверяйтесь с записью заказа, а не с тишиной. В конце есть браузерная демо, где это можно услышать.
Возобновление оборванной сессии
Возобновление сессии — по выбору и это не память. Поставьтеresumption.enabled: true в session.update, возьмите ID из события conversation.created, и если сокет оборвётся — переподключайтесь с ?conversation_id=<id> в URL и снова включайте опцию на новом соединении.
asyncdefreconnect(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.
Есть недокументированная особенность: повтор не прилетает мгновенно, поэтому вопрос, отправленный в момент открытия сокета, может «обогнать» воспроизведение и вернуться без памяти о предыдущей реплике. Дайте секунду, прежде чем винить механизм возобновления.
Журнал терминала с возобновлённой сессией. Иллюстрация автора.
Не используйте это вместо сохранения состояния заказа в собственной базе. Если кэш истечёт или звонящий наберёт завтра, вы начнёте с нуля — так задумано.
Безопасность и мониторинг агента
Никогда не размещайте постоянный ключ API в браузере или мобильном коде. Если клиент подключается напрямую, а не через ваш сервер, выпускайте краткоживущий токен:
from fastapi import FastAPI
import httpx, os
app = FastAPI()@app.post("/session")asyncdefcreate_session():asyncwith 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": ...}Работает на
Браузер не может задать пользовательский заголовок Authorizationв рукопожатии WebSocket, поэтому он передаёт токен через заголовок sec-websocket-protocol с префиксом xai-client-secret..
Сервер выпускает токен, браузер присоединяется к звонку. Иллюстрация автора.
Биллинг ведётся по двум счётчикам. Аудио, отправленное или полученное, тарифицируется по $0,08 за минуту, о чём я упоминал выше (то есть $4,80 в час), а каждый conversation.item.create, который не аудио и не function_call_output, стоит фиксированные $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 — они другие.
Что до приватности — будьте точны. В FAQ по безопасности SpaceXAI говорится, что запросы и ответы API хранятся в зашифрованном виде 30 дней для мониторинга злоупотреблений и не используются для обучения без разрешения, а также что команды могут включить Zero Data Retention, но ZDR отключает историю разговоров голосового агента и потому несовместим с возобновлением.
Если вы сообщаете, что звонок записывается или обрабатывается ИИ, для этого и служит расширение force_message, которое я упоминал. Фраза проигрывается ровно как написана, а не как её перефразирует модель.
Тестирование голосового агента
Статус 200 на рукопожатии WebSocket ничего не говорит о том, корректно ли агент выполнил задачу. Тестируйте результат, а не только соединение.
Чистый поиск заказа — сверяйте озвученный ответ с записью, а не просто факт ответа
Прерванный ответ — подтвердите, что воспроизведение остановилось и агент переключился на новый запрос
Обновление доставки с подтверждением — сверяйте с записью заказа
Отказ — например, отмена отправленного заказа: агент должен объяснить правило, а не извиняться
Неизвестный номер заказа — убедитесь, что агент так и говорит, а не выдумывает статус
Инструмент, вернувший ошибку — проверьте, что агент её озвучивает, а не «зависает»
Переподключение и возобновление — включая окно воспроизведения, с которым я столкнулся
Шумное аудио, быстрая речь и звонящий, диктующий номера и адреса по буквам
Большинство из этого я прогнал на живом ключе, пока писал статью. Интересные сбои — поведенческие, не ошибки: описанная выше задержка при возобновлении и выход за пределы допустимого порога VAD, принятый вместо отклонения — то, что тихо уедет в прод, если тестировать только «счастливый путь». Добавьте многоязычный тест и посмотрите раздел FAQ — там нюанс с указанием языка.
Два сценария вы не проверите печатая.app_streamlit.py — это страница на Streamlit, которая даёт живой звонок в браузере: микрофон стримит в тот же WebSocket по WebRTC, голос агента стримится обратно, и сокет открыт всё время.
Говорите поверх агента — и он остановится, потому что приходит speech_started, а страница очищает очередь аудио. Это то самое «рукопожатие» из раздела о перебиваниях — в бою.
Смотрите на запись заказа, а не на транскрипт: агент зачитывает изменение доставки и говорит, что применил его — и запись либо изменилась, либо нет. Наденьте наушники. На открытых колонках агент слышит себя, считает это барж‑ином и обрывает свою фразу — так же поступит звонок по громкой связи.
Ограничения Grok Voice Think Fast 2.0 и вопросы развёртывания
Планируйте следующее: вызовы инструментов, которые падают посреди реплики; модель, которая уверенно произносит подтверждение, хотя действие не удалось; VAD, настроенный под тихий офис и «ломающийся» на телефонной линии; и звонящего, который меняет мнение на полуслове.
Для платежей, доступа к аккаунту или если звонящий звучит растерянно или расстроен — переводите к человеку. Дайте модели инструмент transfer_to_human: без него она придумает извинение вместо эскалации.
Модульный стек распознавание‑модель‑синтез всё ещё уместен: отдельный контроль над каждым компонентом и детерминированный транскрипт до всяких рассуждений — ценой большей интеграционной работы. А если вашему сценарию вовсе не нужно живое общение, текстовый чат-бот или пакетная транскрипция проще и дешевле, чем конвейер реального времени, к которому никто не говорит.
Заключение
Во всех тестах в этой статьеgrok-voice-think-fast-2.0в основном делал то, что описано в документации. Жизненный цикл событий выдержан, оборванное соединение восстановилось с сохранением предыдущих реплик, а модель вызвала инструмент, ещё произнося вступление.
Помимо несовпадения по названию conversation.item.added, стоит отметить, как много оставшейся работы на вашей стороне сокета: очереди воспроизведения, когда молчать, когда не задавать следующий вопрос.
Если начинать проект сегодня, мои дефолты: фиксированная версия модели, а не псевдоним; server_vad с настройкой silence_duration_ms раньше других регуляторов; транспорт JSON, пока измеримо не понадобится binary; resumption.enabled в первом session.update; и логирование session.model при старте.
Привычки, которые я бы перенёс в любой голосовой агент: проверять записи по факту, а не по устному подтверждению; закладывать отказы в инструмент, а не в промпт; давать воспроизведению завершиться перед следующим response.create; и тестировать на реальных акцентах, реальном шуме и реальных сбоях инструментов.
Очевидные продолжения: телефония (SpaceXAI документирует поддержку SIP напрямую), браузерный клиент на эфемерных токенах, подключение MCP к реальному CRM и полноценная многоязычность. А если Voice Agent API, с которым я сравнивал лимиты сессий, ближе к вашей задаче — наш учебник по Grok Voice Agent API описывает этот путь.
FAQs
Безопасно ли использовать grok-voice-latest в продакшене?⌄
Не совсем, как я упоминал в разделе о версиях. Он меняется в дату, которую выбирает SpaceXAI, а не вы, и цена меняется вместе с ним. Закрепите grok-voice-think-fast-2.0 и оставьте псевдоним для локальных экспериментов, где неожиданный переключатель не придётся на звонок с живым клиентом.
Поддерживает ли Grok Voice Think Fast 2.0 языки, кроме английского?⌄
Да, документировано более двадцати с автоопределением, и вы можете сместить транскрипцию в сторону конкретного языка через language_hint. Учтите, что для испанского и португальского нужен региональный код, например es-MX или pt-BR. Оголённые es или pt не принимаются, а нераспознанные коды молча игнорируются с возвратом к автоопределению, так что опечатка ничего не стоит, но и ничего не даёт.
Можно ли сменить голос и сколько их?⌄
eve — та, что в документации и которую я использовал; также доступны ara, rex, sal и leo, плюс настраиваемые ID голосов. Текущий список вернёт GET /v1/tts/voices. Если вас смущает темп, audio.output.speed принимает значения от 0.7 до 1.5.
Можно ли заставить агента отвечать быстрее?⌄
Попробуйте reasoning.effort, который я пропустил в разборе, потому что дефолт обычно верный. По умолчанию это "high", также принимает "none", что снижает объём планирования на реплику. Подходит для простых сценариев поиска. Я бы не трогал на процессах, где нужно выбирать между инструментами.
Нужен ли официальный SDK SpaceXAI, чтобы всё это построить?⌄
Нет, как сказано в разделе с требованиями. Подойдёт обычный пакет websockets или совместимый с OpenAI клиент, направленный на базовый URL api.x.ai. Важно: официальный xai-sdk — это отдельный gRPC‑клиент, который не работает с этим WebSocket, так что не ищите в нём методы для real‑time. В качестве альтернативной отправной точки посмотрите xai-cookbook — там есть примеры для iOS, web, WebRTC и телефонии.
Grok Voice Think Fast 2.0 от SpaceXAI — это модель «речь-в-речь». Вы отправляете ей аудио по WebSocket, а она возвращает аудио; при этом модель может рассуждать и продолжать говорить, пока уже запущен выбранный ею вызов функции. Никаких отдельных шагов распознавания речи и синтеза речи.
SpaceXAI объявила о Think Fast 2.0 29 июля 2026 года: более быстрое первое аудио, более стабильный полнодуплексный режим (слушает, пока говорит, а не строго по очереди) и ранние вызовы инструментов в рамках реплики. На бенчмарках долго не задерживаюсь: для практикума важнее, что меняется в вашем коде.
Мы построим голосового агента службы поддержки интернет‑магазина. Звонящий может спросить о заказе, изменить инструкции по доставке, отменить, перебить агента посреди фразы и продолжить диалог после обрыва соединения. Это путь через API, а не конструктор без кода, который разбирает наш учебник по Grok Voice Agent Builder. Для версии через консоль начните оттуда.
Что такое Grok Voice Think Fast 2.0?
Grok Voice Think Fast 2.0 — новейшая модель SpaceXAI для Speech to Speech API — продуктового названия того, что большинство называет просто Grok Voice. Если вы всё ещё думаете о компании как о xAI — это та же команда: 6 июля 2026 года она была интегрирована в SpaceX и переименована в SpaceXAI. API названия не менял, поэтому все идентификаторы ниже по‑прежнему содержат
xai— от переменнойXAI_API_KEYдо хостаapi.x.ai.Классический голосовой стек — это цепочка из трёх сервисов: распознавание речи, языковая модель, затем синтез речи. Каждый переход добавляет задержку и риск потери контекста. Think Fast 2.0 сводит всё к одной модели, которая принимает на вход аудио или текст и по тому же соединению выдаёт аудио или текст.
WebSocket «речь‑в‑речь» против трёхсервисной архитектуры. Иллюстрация автора.
Для агента, который действует, а не только говорит, важно, что рассуждение и речь идут параллельно. По словам SpaceXAI, вызовы инструментов «обычно» стартуют до того, как агент закончит первую фразу — и это «обычно» тут не случайно.
На бенчмарках, на которые ссылается SpaceXAI — Artificial Analysis — Think Fast 2.0 набирает 82,9% в Speech to Speech Index против 75,7% у 1.0 и сокращает время до первого аудио с 1,25 секунды до 0,70 секунды. Показатели вендора на общем бенчмарке — это гипотеза о вашем звонке, а не план тестирования.
Есть три строковых названия модели:
grok-voice-latest,grok-voice-think-fast-2.0иgrok-voice-think-fast-1.0. Псевдоним удобен на этапе прототипирования и недостаточно надёжен для чего‑то большего.Когда я тестировал 4 августа 2026 года,
grok-voice-latestвсё ещё разрешался вgrok-voice-think-fast-1.0, а в заметках о релизах SpaceXAI перенос на Think Fast 2.0 был намечен на следующий день. Этот переключатель — смена не только модели, но и цены: $0,08 за минуту аудио против $0,05 у 1.0, так что незакреплённый псевдоним подорожает, хотя вы не меняли ни строки кода. В развёртывании закрепляйте версию явной строкой.Что мы построим
Агент покрывает типичные запросы линии поддержки: найти заказ, найти его по email, если у звонящего нет номера, изменить инструкции по доставке, отменить, открыть или проверить тикет и передать звонок оператору. По пути будут перебивания и обрыв соединения.
Это несколько небольших файлов, а не один скрипт: у каждого своя задача, и вы захотите тестировать их по отдельности. Структура такая:
config.pyзагружает ключ API и хранит строку модели, частоту дискретизации и URL конечных точекvoice_client.pyинкапсулирует WebSocket, ведёт учёт биллинга и предоставляет помощники отправки/приёмаtools.pyопределяет функции для заказов и небольшой in-memory хранилище заказов вместо реальной базыassistant.pyсодержит системный промпт, конфигурацию сессии и цикл обработки событий, который всё связываетtoken_server.py— небольшой эндпоинт FastAPI, который выпускает эфемерные токеныapp_streamlit.pyдаёт тот же клиент в браузерном звонке в реальном времени; к нему вернусь после раздела о тестированииОбучающий путь — из терминала. Демонстрация добавляет микрофон.
Предварительные требования
Вам нужна учётная запись SpaceXAI с ключом API, пополненный платёжный метод (постоянного бесплатного тарифа нет, промокредиты для новых аккаунтов не спасут) и достаточный уровень владения
asyncioи WebSocket, чтобы следовать без построчного разбораawait.В примерах быстрого старта SpaceXAI используется чистый пакет
websockets, а не отдельный SDK — так поступим и мы. В документации нет требований к версии Python. Я тестировал на 3.11.Храните ключ API на сервере. Если браузер или мобильное приложение обращается к Voice API напрямую, используйте эфемерный токен вместо реального ключа — это мы разберём в разделе безопасности ниже.
Настройка проекта
Все файлы ниже есть в репозитории проекта, так что можно клонировать его вместо копирования фрагментов:
websocketsобеспечивает соединение в реальном времени,python-dotenvчитает ваш ключ. Остальное — для эндпоинта токенов и браузерной демонстрации. Поместите ключ в.env:Это почти вся подготовка. Интересное — в соединении.
Понимание Grok Voice Realtime API
Grok Voice — это продуктовое имя. Фактически вы работаете с WebSocket-эндпоинтом по адресу
wss://api.x.ai/v1/realtime, и весь диалог — это поток JSON‑событий по этому одному сокету.Жизненный цикл событий
Соединение следует фиксированному шаблону: сервер сразу после подключения отправляет
session.createdиconversation.created, вы отправляетеsession.updateдля настройки голоса и инструментов, сервер подтверждаетsession.updated, и дальше вы создаёте элементы беседы и запрашиваете ответы. Я прогонял это с живым ключом — порядок точно совпал с документацией.session.update(клиент) настраивает голос, инструкции, инструменты и формат аудиоconversation.item.create(клиент) добавляет сообщение пользователя, ассистента или результат инструментаresponse.create(клиент) просит модель говорить; серверный VAD отправляет это за вас автоматическиresponse.output_audio.deltaиresponse.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 Гц),audio/pcmuилиaudio/pcma(G.711 на 8 кГц, для телефонии), илиaudio/opus(24 кГц). Транспорт — это способ передачи байтов по сети:json(по умолчанию) отправляет аудио как base64‑текст внутриinput_audio_buffer.appendиresponse.output_audio.delta, удобно для логирования и отладкиbinaryшлёт «сырые» байты кодека как бинарные кадры WebSocket, исключая накладные расходы base64 ценой усложнения цикла приёма (ветвление по типу сообщения)Начните с JSON. Все примеры в документации используют его, он тривиально инспектируется, а накладные расходы base64 — не то, что станет узким местом для агента поддержки. Переходите на binary, только если замеры дадут повод.
Совместимость с OpenAI Realtime API
Пропустите, если вы никогда не работали с Realtime API OpenAI. Для остальных: Speech to Speech API достаточно близко следует OpenAI Realtime API, так что большую часть клиентского кода можно перенести, сменив базовый URL и ключ, но это не идеальная замена один‑к‑одному.
Транскрипты приходят как
conversation.item.input_audio_transcription.updatedвместо openai-шногоdelta, некоторые события OpenAI не поддерживаются, а SpaceXAI добавляет свои расширения:force_messageдля обязательной фразы раскрытия,resumptionдля переподключений иreplaceдля исправления произношения брендов до синтеза речи.Создание голосового агента в реальном времени
Хватит о протоколе. Вот клиент, который с ним говорит.
Подключение и настройка сессии
Соединение открывается с bearer‑токеном и параметром модели в запросе, а первое ваше сообщение задаёт всё поведение агента:
instructions— это системный промпт, и этой модели нужны короткие. В заметках по миграции SpaceXAI советует упрощать промпты, написанные для старых голосовых моделей эпохи GPT, а не переносить их дословно. В моём агенте — короткие ответы, один вопрос за раз и обязательное зачитывание записи перед действием. Устное подтверждение — деталь UX, а не контроль безопасности. Авторизацию записи всё равно обеспечивает ваше приложение.Что меня удивило: нераспознанная строка модели не даёт ошибку при подключении, а молча откатывается к
grok-voice-think-fast-1.0. Платный запрос, «пониженный» из‑за опечатки без уведомления — странный дефолт. Логируйте полеsession.modelизsession.createdодин раз при старте и проверяйте, что получили то, что просили.Вывод терминала с session.created после подключения. Иллюстрация автора.
Стриминг пользовательского аудио
С
turn_detection.type, установленным вserver_vad, вам достаточно просто добавлять аудио. Сервер сам решает, когда звонящий закончил говорить, и инициирует ответ. Поставьтеnull— и решение будет на вас, вы явно фиксируете буфер, когда считаете, что реплика окончена.У серверного 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.Храните транскрипт и в продакшне. Это самый дешёвый способ отладки, когда звонящий говорит, что агент «сказал что‑то странное».
Добавление инструментов в голосового агента
Голосовой агент, который только говорит, — это чат-бот с микрофоном.
Создание инструментов для заказов
Каждый инструмент — это JSON‑схема плюс простая Python‑функция на нашей стороне. Модель никогда не трогает базу, она видит только то, что возвращает наша функция.
Операции чтения вроде
check_order_statusбезопасно ретраить при таймауте. Операции записи — нет: повторupdate_delivery_instructionsпосле неоднозначного таймаута может применить одно и то же изменение дважды. Фраза подтверждения в промпте это не предотвратит, поэтому давайте на записи idempotency‑ключ или проверку на дубликаты.Отказы тоже закладывайте в функцию.
cancel_orderвозвращает причину и альтернативу вместо отмены отправленного заказа, потому что «никогда не отменять отправленные заказы» в промпте — это пожелание, а отказ в функции — факт.Обработка цикла вызова инструментов
Четыре шага, и порядок важнее, чем кажется. Модель присылает
response.function_call_arguments.done, ваш код выполняет функцию, вы отправляете результат назад как элементfunction_call_output— и только затем просите модель продолжить.Если модели нужно несколько инструментов для запроса, она инициирует несколько событий
function_call_arguments.doneдо начала воспроизведения аудио. Разрешите их все и отправьте каждый результат прежде, чем посылать один‑единственныйresponse.create. Если отправить его слишком рано, модель ответит без контекста незавершённых вызовов.Есть подводный камень, о котором SpaceXAI пишет, и который я всё равно поймал с первого раза: отправка
response.createсразу после результата инструмента может наложиться на ещё проигрываемое вступительное предложение агента. В одном прогона он начал с «Сейчас проверю статус заказа ORD‑1042» и вызвал инструмент посреди фразы, так что мгновенный ответ заговорил бы поверх вступления.Дождитесь завершения текущего аудио и покажите короткое состояние «думает» между ними.
Поток вызова инструмента перед продолжением ответа. Иллюстрация автора.
Управление перебиваниями и состоянием беседы
Здесь две разные задачи. Звонящий перебивает агента в середине ответа, и WebSocket рвётся и требует возобновления.
Поддержка естественных перебиваний
С включённым
server_vadбарж‑ин обрабатывается на стороне сервера автоматически: как только он обнаруживает, что звонящий снова говорит, он сигнализируетinput_audio_buffer.speech_startedи прекращает генерацию старого ответа. Ваша задача — клиентская часть рукопожатия: очистить уже поставленное в очередь аудио, чтобы агент замолчал, а не закончил ненужную фразу.Для ручных сессий без VAD ту же задачу решает
response.cancelпо запросу. Есть ещёconversation.item.truncateдля обрезки элемента ассистента до реально услышанного. В документации подтверждается его наличие, но не указано, когда именно его вызывать при живом барж‑ине — подберите тайминг опытным путём.Я проверял это на изменении инструкции по доставке в середине ответа: начать запрос, перебить другой деталью адреса во время подтверждения агента. Важно, чтобы агент применил скорректированную инструкцию, а не тихо довёл старую, а не то, остановилось ли аудио. Сверяйтесь с записью заказа, а не с тишиной. В конце есть браузерная демо, где это можно услышать.
Возобновление оборванной сессии
Возобновление сессии — по выбору и это не память. Поставьте
resumption.enabled: trueвsession.update, возьмите ID из событияconversation.created, и если сокет оборвётся — переподключайтесь с?conversation_id=<id>в URL и снова включайте опцию на новом соединении.Кэшированные реплики, транскрипты, вызовы инструментов и их результаты воспроизводятся до вашего следующего вопроса, а кэш исчезает после 30 минут бездействия. Я тестировал так: спросил о заказе, оборвал соединение и переподключился для уточнения без повторов — агент корректно вспомнил ETA.
Есть недокументированная особенность: повтор не прилетает мгновенно, поэтому вопрос, отправленный в момент открытия сокета, может «обогнать» воспроизведение и вернуться без памяти о предыдущей реплике. Дайте секунду, прежде чем винить механизм возобновления.
Журнал терминала с возобновлённой сессией. Иллюстрация автора.
Не используйте это вместо сохранения состояния заказа в собственной базе. Если кэш истечёт или звонящий наберёт завтра, вы начнёте с нуля — так задумано.
Безопасность и мониторинг агента
Никогда не размещайте постоянный ключ API в браузере или мобильном коде. Если клиент подключается напрямую, а не через ваш сервер, выпускайте краткоживущий токен:
Браузер не может задать пользовательский заголовок
Authorizationв рукопожатии WebSocket, поэтому он передаёт токен через заголовокsec-websocket-protocolс префиксомxai-client-secret..Сервер выпускает токен, браузер присоединяется к звонку. Иллюстрация автора.
Биллинг ведётся по двум счётчикам. Аудио, отправленное или полученное, тарифицируется по $0,08 за минуту, о чём я упоминал выше (то есть $4,80 в час), а каждый
conversation.item.create, который не аудио и неfunction_call_output, стоит фиксированные $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 — они другие.Что до приватности — будьте точны. В FAQ по безопасности SpaceXAI говорится, что запросы и ответы API хранятся в зашифрованном виде 30 дней для мониторинга злоупотреблений и не используются для обучения без разрешения, а также что команды могут включить Zero Data Retention, но ZDR отключает историю разговоров голосового агента и потому несовместим с возобновлением.
Если вы сообщаете, что звонок записывается или обрабатывается ИИ, для этого и служит расширение
force_message, которое я упоминал. Фраза проигрывается ровно как написана, а не как её перефразирует модель.Тестирование голосового агента
Статус 200 на рукопожатии WebSocket ничего не говорит о том, корректно ли агент выполнил задачу. Тестируйте результат, а не только соединение.
Большинство из этого я прогнал на живом ключе, пока писал статью. Интересные сбои — поведенческие, не ошибки: описанная выше задержка при возобновлении и выход за пределы допустимого порога VAD, принятый вместо отклонения — то, что тихо уедет в прод, если тестировать только «счастливый путь». Добавьте многоязычный тест и посмотрите раздел FAQ — там нюанс с указанием языка.
Два сценария вы не проверите печатая.
app_streamlit.py— это страница на Streamlit, которая даёт живой звонок в браузере: микрофон стримит в тот же WebSocket по WebRTC, голос агента стримится обратно, и сокет открыт всё время.