Ты собираешь голосового агента. Собираешь долго. Наконец задеплоил. И тут начинается ад — ручное тестирование. Микрофон, «Алиса, включи музыку», пауза, слушаешь ответ, снова говоришь. 20 сценариев — час времени. А если нужно проверить регрессию после каждого коммита? Руки опускаются.
Я перепробовал всё: запись реальных разговоров, эмуляцию аудиопотоков через PulseAudio, виртуальные микрофоны. Всё это — костыли. Они не масштабируются, падают в CI, требуют физического присутствия железа. А потом я наткнулся на подход, который называется Automated Voice Agent Testing Without a Microphone. И да, это работает. А главное — Amazon Nova Sonic делает это нативным.
Почему ручное тестирование голоса — это пуля в колено
Давай честно: голосовые агенты тестируют так же, как GUI в 2005 году — вручную. Открываешь демо, говоришь в микрофон, смотришь на логи. Один сценарий — 2-3 минуты. 50 сценариев — полтора часа. А если агент начал галлюцинировать на третьем вопросе? Начинаем сначала.
Проблема номер один: человеческий голос невоспроизводим. Ты не сможешь сказать одинаково два раза подряд. Воспроизводимость тестов — святая корова QA — летит в бездну. Проблема номер два: нет триггера для CI. Никто не садится за микрофон при каждом пуше. Итог — регрессии живут в проде, а пользователи слышат странные ответы.
Внимание: если ваш voice agent уже в production и у вас нет автоматизированных тестов — вы играете в русскую рулетку. Один неудачный промпт — и агент начинает отвечать по-китайски. Это не шутка.
Amazon Nova Sonic: единая модель вместо каскада
Если ты читал мою предыдущую статью про Amazon Nova Sonic, ты знаешь главное: эта модель — не просто ASR + LLM + TTS, слепленные скотчем. Она принимает сырые аудиоданные (PCM или Opus в WebSocket) и выдаёт аудиопоток. А значит, мы можем подавать на вход не микрофон, а файл или синтезированную речь. И это меняет всё.
Автоматизация тестирования без микрофона строится на простой идее: берём текстовые сценарии, превращаем их в аудио (любой TTS), отправляем в Nova Sonic через Bedrock Runtime API, получаем аудиоответ, транскрибируем его и сравниваем с эталоном. Всё это — в Python-скрипте, который запускается в GitHub Actions.
Никаких виртуальных аудиоустройств. Никаких Xvfb. Только API. Только код.
Шаг за шагом: как собрать тестовый пайплайн
1 Готовим сценарии и синтезируем речь
Пишем YAML-файл с тестами. Каждый тест — пара (user_input, expected_response). User_input — это фраза пользователя. Мы её синтезируем в аудио с помощью любого TTS. Я использую Amazon Polly (он уже в AWS) или NeuTTS Nano — лёгкий on-device синтезатор.
# scenarios.yaml
tests:
- id: "greeting"
user_input: "Привет, какой сегодня курс биткоина?"
expected_partial: "Биткоин" # проверяем, что в ответе есть это слово
- id: "fallback"
user_input: "Расскажи анекдот про хлеб"
expected_partial: "извините" # если агент не знает — он должен извиниться
Дальше генерируем WAV-файлы. Это можно сделать прямо в тестовом скрипте:
import boto3
from io import BytesIO
polly = boto3.client('polly')
def synthesize(text: str) -> bytes:
response = polly.synthesize_speech(
Text=text,
OutputFormat='pcm',
VoiceId='Maxim', # русский голос
SampleRate='16000'
)
return response['AudioStream'].read()
2 Отправляем аудио в Nova Sonic и получаем ответ
Используем Converse API с поддержкой аудио-контента. В Bedrock Nova Sonic умеет принимать и возвращать аудиоблоки. Главное — правильно сформировать сообщение:
import boto3
import json
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')
def invoke_nova_sonic(audio_bytes: bytes, sample_rate=16000):
response = bedrock.converse(
modelId='amazon.nova-sonic-v1:0',
messages=[
{
'role': 'user',
'content': [
{
'audio': {
'source': {
'bytes': audio_bytes,
'mediaType': 'audio/pcm'
},
'sampleRate': sample_rate
}
}
]
}
]
)
# Ответ может содержать аудио и/или текст
assistant_content = response['output']['message']['content']
# Проверяем, есть ли text
for block in assistant_content:
if 'text' in block:
return block['text']
elif 'audio' in block:
# Если вернулось аудио — транскрибируем (или сразу сравниваем по семантике)
return block['audio']['transcript']
return ""
Осторожно: Nova Sonic может возвращать аудио даже если ты просишь текст. В документации AWS это описано как особенность мультимодальных моделей. Поэтому всегда проверяй оба поля.
3 Сравниваем ответ с эталоном
Тут есть тонкость: сравнивать посимвольно — глупо. Агент может сказать "Курс биткоина сейчас 59 000 долларов" вместо "Курс биткоина составляет 59000 USD". Нужна семантическая близость или хотя бы частичное совпадение.
Я использую комбинацию: - Partial match — проверяем, что ключевое слово есть в ответе. - Cosine similarity через embeddings (Amazon Nova Embeddings).
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
def semantic_similarity(expected, actual, threshold=0.8):
emb1 = model.encode(expected)
emb2 = model.encode(actual)
sim = cosine_similarity([emb1], [emb2])[0][0]
return sim >= threshold, sim
Для быстрой проверки в CI достаточно частичного совпадения — это уже отсечёт 90% регрессий.
4 Запускаем в CI/CD
Всё упаковываем в Python-скрипт и кладём в репу. Для CI я использую pytest с параметризацией:
# test_voice_agent.py
import pytest
import yaml
with open('scenarios.yaml') as f:
scenarios = yaml.safe_load(f)['tests']
@pytest.mark.parametrize('scenario', scenarios, ids=lambda s: s['id'])
def test_voice_agent(scenario):
audio = synthesize(scenario['user_input'])
response = invoke_nova_sonic(audio)
assert scenario['expected_partial'].lower() in response.lower(), \
f"Ожидали '{scenario['expected_partial']}' в ответе, получили '{response}'"
В .github/workflows/test.yml просто устанавливаем зависимости и запускаем pytest. Вся магия — в работе с Converse API. Никаких микрофонов.
Грабли, на которые я наступил (и ты наступишь)
1. Nova Sonic не всегда возвращает текст
По умолчанию модель может ответить аудио. Если тебе нужен именно текст — добавь в промпт системную инструкцию "Отвечай только текстом". Но даже так иногда приходит аудио. Лови оба случая.
2. Аудио нужно ресемплировать
Polly выдаёт 16 кГц — норм. Но если берёшь синтез из другого источника (например, Vosk), может быть 8 кГц. Nova Sonic ожидает минимум 16 кГц. Используй librosa.resample или sox.
3. Задержки в CI — это боль
Каждый вызов модели занимает 2-5 секунд. 50 тестов — 4 минуты. Не смертельно, но можно ускорить параллелизацией. pytest-xdist спасает.
4. Не все сценарии покрыть текстом
Некоторые сценарии зависят от контекста разговора (несколько реплик). Для них используй историю сообщений (messages с несколькими чередованиями). Я описал это в статье про создание streaming-приложения с Nova Sonic и WebRTC.
Куда копать дальше: регрессионные наборы и метрики
Подход, который я описал, — база. Этого достаточно, чтобы покрыть 80% тестовых сценариев. Но если хочешь серьёзное enterprise-тестирование — посмотри на EVA-Bench 2.0. Там есть готовые бенчмарки с размеченными ответами и метрики вроде Voice Agent Quality (VAQ). Я интегрировал EVA-фреймворк в свой пайплайн — и сразу увидел, что мой агент плохо обрабатывает сложные вопросы с двойным отрицанием.
Для глубокого тестирования AI-агентов в production рекомендую прочитать статью про LangSmith + pytest + Bedrock. Там показано, как логировать каждый вызов и вычислять метрики качества.
Автоматизация тестов без микрофона — не фантастика. Это уже рабочее решение, которое я внедрил в нескольких продакшенах. Amazon Nova Sonic даёт API, который позволяет обойтись без звуковой карты. А значит — вписать тесты голосовых агентов в CI/CD стало реально.
Не повторяй моих граблей: не пытайся эмулировать устройство через ALSA, не заставляй QA сидеть с микрофоном. Просто отправляй PCM в Nova Sonic и проверяй ответ. Быстро, надёжно, без микрофона.
Через два года ни один voice-агент не будет выпущен в прод без pipeline, который гоняет 500+ сценариев автоматически. Те, кто не автоматизирует тестирование сейчас, утонут в багах. А ты — нет.