Оценка голосовых агентов по результату: трёхуровневый тестовый стенд
Метрики по транскрипту не замечают, когда голосовой агент обновил не тот заказ или оставил устаревшую запись. Собираем стенд из трёх уровней, шести осей и релизного шлюза.
Представьте три телефонных звонка, которые на ревью выглядят идеально. В первом клиент посреди фразы исправляет номер заказа, и агент зачитывает ему уже исправленный номер. Во втором клиент перебивает агента, и тот аккуратно замолкает, никто ни на кого не наговаривает. В третьем агент сообщает, что заказ обновлён.
А теперь загляните в бэкенд. Первый звонок обновил не тот заказ, а именно тот, от которого клиент только что отказался. Во втором вызов инструмента, который выполнялся в момент перебивания, так и продолжил работать. В третьем обновление вообще не дошло, и в системе по-прежнему лежит старое значение.
Ревью транскриптов ни одну из этих ситуаций не поймает. Проверка WER или рубрика качества диалога тоже. При этом все три звонка провалили то, что действительно важно. Дальше разберём, как построить тестовый стенд, который ловит весь этот класс ошибок раньше, чем на него наткнётся клиент.
Почему метрик по транскрипту мало
Голосовых агентов обычно меряют по word error rate, задержке, скорости реакции на перебивания, произношению и естественности речи. Всё это стоит отслеживать, и ни одна из метрик не ошибочна. Но они отвечают на более узкий вопрос, чем тот, что ставит прод. Они говорят, правильно ли агент звучал. И ничего не говорят о том, произошло ли что-то на самом деле.
У стенда, который имеет смысл запускать, три уровня:
- Диалог: агент правильно услышал и правильно ответил?
- Исполнение: вызваны нужные инструменты с нужными аргументами?
- Состояние: задуманное изменение действительно применилось во внешней системе?
Приложим это к трём звонкам. Первый прошёл уровень диалога и провалил исполнение. Второй тоже провалил исполнение, но по другой причине: устаревший вызов так и не отменили. Третий прошёл и диалог, и исполнение, но провалился на состоянии, потому что запись в бэкенд не сохранилась. Три провала на разных уровнях, и у всех один и тот же симптом: транскрипт, который выглядит нормально.
Сначала опишите, что может пойти не так
Ничего не стоит оценивать, пока нет матрицы режимов отказа. Четыре категории покрывают большую часть случаев.
Перебивания: клиент вклинивается до того, как агент договорил; пока собирается вызов инструмента; сразу после того, как вызов начал выполняться; а также случайные перебивания из-за фонового шума, которые вообще не должны были останавливать агента.
Исправления: изменённый номер заказа, новая дата доставки, другое количество и отрицание, например «отправляйте», а следом «нет, не надо».
Сбои инструментов: от таймаутов и пустых ответов до инструмента, который достал не ту запись, 500-х от бэкенда, запоздавшего ответа об успехе и дублирующихся запросов из-за неидемпотентного ретрая.
Конфликты состояния возникают, когда одновременно существуют три версии правды: в транскрипте одно, в живом состоянии другое, а в payload, который реально ушёл в инструмент, третье.
На практике категории пересекаются. Исправление, пришедшее во время ретрая после сбоя инструмента, это отдельный сценарий. И всё же такая группировка позволяет сначала проверить каждую категорию по отдельности.
Как выглядит тест-кейс
Каждому кейсу нужны входной сценарий, описание того, что правильная система должна сделать на всех четырёх уровнях, и структура достаточно жёсткая, чтобы оценка шла автоматически и никому не приходилось читать и судить вручную. Минимальная схема выглядит так:
{
"id": "corr-02",
"category": "correction",
"setup": {
"field": "order_number",
"initial_value": "5590",
"corrected_value": "5599",
"interrupt_stage": "during_tool_prep"
},
"expected": {
"captured": true,
"active_state_correct": true,
"tool_args_correct": true,
"stale_write_prevented": true,
"backend_correct": true,
"confirmation_matches_backend": true,
"should_execute": true
}
}
Блок setup намеренно свободный: перебиваниям и сбоям инструментов нужны другие поля, чем исправлениям. Блок expected строгий. Каждый кейс оценивается по одним и тем же шести осям, и именно поэтому 30 совершенно разных сценариев сводятся в одну таблицу результатов.
Шесть осей
- Событие зафиксировано. Система вообще заметила исправление или перебивание?
- Корректное активное состояние. После события живое рабочее состояние обновилось соответственно?
- Корректные аргументы инструмента. Значения в вызове инструмента актуальны и согласованы?
- Устаревшая запись предотвращена. Заблокировано ли каждое действие, основанное на устаревшем состоянии?
- Бэкенд корректен. Сохранённый результат соответствует тому, чего клиент на самом деле хотел?
- Подтверждение совпадает с бэкендом. То, что агент сказал клиенту, совпадает с тем, что реально сохранено?
Одно правило важнее остальных: эти оси никогда не усредняются в одно число. Система, правая по пяти осям и неправая по backend_correct, выдала неверный результат, и пять зелёных галочек рядом этого не смягчают. Кейс засчитывается как настоящий проход, End-to-End Task Integrity, только когда все шесть осей истинны одновременно.
Как выглядит успешный трейс
Возьмём сценарий, где исправление приходит рано: оно появляется раньше, чем возник какой-либо вызов инструмента, так что отменять нечего.
Customer says 4821 → state_version = 4
Correction: 4281 → state_version = 5
Tool call built using version 5
Backend verified = 4281
Agent confirms 4281
Каждый переход состояния несёт метку версии, а вызов инструмента собирается только тогда, когда версия стабильна. Ничего никогда не строится на версии 4, поэтому потом нечего выбрасывать. Вот так выглядит успешный кейс. Это не «исправления не было». Это «исправление полностью распространилось до того, как что-то ниже по цепочке зафиксировалось».
Прогон на заведомо кривом моке
Чтобы было предметно, возьмём рабочий стенд (фикстуры, схема и скорер) и направим его на мок-пайплайн на правилах. Мок точно распознаёт речь и хорошо держит очерёдность реплик, но никогда не инвалидирует вызов инструмента, если тот уже собран или отправлен, и никогда не перепроверяет результат инструмента, если не распознал его как битый. Это ровно та система, что стоит за провалами выше, только описанная явно, чтобы у стенда была цель.
В прогоне было 30 синтетических звонков, поровну разделённых на сценарии исправлений, перебиваний и сбоев инструментов.
- Событие зафиксировано: 24 из 30
- Корректное активное состояние: 24 из 30
- Корректные аргументы инструмента: 11 из 30
- Устаревшая запись предотвращена: 8 из 30
- Бэкенд корректен: 8 из 30
- Подтверждение совпало с бэкендом: 7 из 30
- Сквозная целостность задачи: 7 из 30
Уровень диалога выглядит крепким: в 24 звонках из 30 событие поймано и состояние обновлено. Дальше исполнение и состояние проседают. Только в 8 звонках удалось не допустить устаревшую запись, и лишь в 7 озвученное подтверждение совпало с тем, что в итоге оказалось в бэкенде. Если судить только по качеству транскрипта, система выглядела бы готовой к проду. Но это не так.
Как превратить провалы в список исправлений
Красный результат полезен, только если он указывает, какой уровень чинить. Если разложить 23 непрошедших кейса по причинам, получится так:
- 10 кейсов: аргументы инструмента собраны из устаревших значений
- 4 кейса: битый результат инструмента принят без всякой проверки
- 3 кейса: уже запущенные вызовы так и не отменили
- 2 кейса: фоновый шум ошибочно принят за перебивание клиента
- 2 кейса: ошибка бэкенда плюс ретрай, который нельзя было безопасно повторять
- 1 кейс: подтверждение озвучено до того, как вернулся асинхронный результат
- 1 кейс: у действия нет ключа идемпотентности
Десять из 23 промахов вызваны одной причиной. Вызов инструмента уже был собран или отправлен, когда пришло исправление или перебивание, и никакого механизма ниже по цепочке, чтобы его отменить, не было. Это одна архитектурная дыра, которая проявилась в десяти сценариях, а не десять багов, и увидеть её как раз и есть смысл прогона стенда. Голое «звонок провалился» команде ничего не даёт, а вот «сборка аргументов сломалась в 10 из 23 кейсов» задаёт приоритеты на следующий спринт.
Релизный шлюз для значимых действий
Прежде чем выкатывать изменение голосового агента, который совершает значимые действия, планки «качество диалога выросло» недостаточно. Шлюз должен звучать так:
- Ни одного провала с устаревшей записью в наборах исправлений и перебиваний
- Ни одного расхождения между подтверждением и бэкендом
- Идемпотентные ретраи инструментов
- Критичные действия проверяются по бэкенду после выполнения, а не принимаются на веру по ответу инструмента
Голосовой агент готов к проду только тогда, когда диалог, трейс инструментов и состояние бэкенда сходятся в том, что произошло. Большинство систем сегодня этим похвастаться не могут, и чтобы это выяснить, нужно перестать оценивать транскрипт и начать оценивать результат.