Мой интернет-провайдер внедрил ИИ-агента, чтобы заменить телефонное меню. Я звонил ему несколько недель назад.
Он спросил, по какому вопросу я звоню. Затем последовало шесть секунд тишины — достаточно долго, чтобы я сказал «алло?» в пустоту, как будто на дворе 1998 год. Агент вернулся, сказал, что узнал мой номер, и спросил, звоню ли я по поводу этого аккаунта. Да. Затем он сообщил, что собирается провести аутентификацию, и зазвучал совершенно другой голос. Роботизированный. Каждые пятнадцать секунд: «MFA ОТПРАВЛЕН. ПОДТВЕРДИТЕ MFA». Я спросил, куда он что-то отправил. «НЕВЕРНЫЙ MFA».
В конце концов я нашел электронное письмо с шестизначным кодом и зачитал его. Дружелюбный голос вернулся, чтобы сказать, что ничем не может помочь, и перевел меня на оператора. Оператор спросил, по какому вопросу я звоню, и сказал, что ему нужно провести мою аутентификацию.
Вся ИИ-часть этого звонка была хуже, чем бесполезна. Она заставила меня делать всё дважды.
Вот что должно вас беспокоить: каждая отдельная часть работала. Модель поддерживала диалог. Транскрипция была точной. Голоса звучали нормально. Аутентификация аутентифицировала. Ничто в этом стеке не дало сбоя, а звонок всё равно был мусором.
Звонок был мусором из-за мест, где части соединялись друг с другом.
В этой статье
- Вы не можете устранить шов — границу, где соединяются две части вашего голосового приложения. Вы можете только переместить его. Каждый шов стоит вам задержки, связности или ответственности за сбой.
Вы не можете устранить шов — границу, где соединяются две части вашего голосового приложения. Вы можете только переместить его. Каждый шов стоит вам задержки, связности или ответственности за сбой.
- У речевого оборота есть около 1500 миллисекунд, прежде чем человек почувствует задержку. Размещение — это арифметика: каждый шов в этом обороте расходует один и тот же бюджет.
У речевого оборота есть около 1500 миллисекунд, прежде чем человек почувствует задержку. Размещение — это арифметика: каждый шов в этом обороте расходует один и тот же бюджет.
- Запихивание всего в один промпт не убирает ваши швы. Оно прячет их там, где вы не можете их контролировать.
Запихивание всего в один промпт не убирает ваши швы. Оно прячет их там, где вы не можете их контролировать.
Почему голосовые агенты терпят неудачу, когда каждый компонент работает?
Потому что сбои живут между компонентами, а не внутри них. Каждое приложение — это набор частей, соединенных вместе, и каждое место, где соединяются две части, я называю швом. Speech-to-text передает данные модели. Модель передает их синтезу речи. Агент обращается к вашей CRM. Он передает управление человеку. Ваше приложение — это куча таких границ с логикой, нанизанной между ними.
Можно ли спроектировать голосовой конвейер с меньшим количеством движущихся частей?
Вы можете перемещать части. Вы не можете избавиться от них, и именно в этом люди ошибаются. Если вы думаете, что устранили шов, вы его переместили, спрятали внутри библиотеки, протолкнули на уровень ниже или купили у поставщика, который теперь владеет им вместо вас. Он никуда не делся. Он сохранился. Единственное, что вы действительно контролируете, — это где он живет.
Я верил в версию этого утверждения в течение двадцати лет относительно единых точек отказа. Я начинал с создания крупномасштабных веб-приложений, и урок был тем же: вы никогда не устраняете единую точку отказа. Вы перемещаете ее туда, где можете управлять ею, где сбой стоит вам меньше всего. То же самое и здесь.
Что на самом деле идет не так на границах между компонентами?
Три вещи, и шов может стоить вам всех трех сразу.
Это стоит времени. Каждый переход — это миллисекунды, которые вы не вернете.
Это стоит связности. Состояние, контекст и идентификация должны пересекать границу, и когда они этого не делают, две стороны перестают «договариваться» о том, что происходит.
Это стоит поломок. Швы рвутся, и тот, кто держит шов, отвечает за то, что происходит, когда он рвется.
Каждый сбой в остальной части этой статьи — это одна из этих трех вещей или две сразу.
Почему мой голосовой агент замолкает на несколько секунд?
Обычно потому, что он ждет кругового запроса, который был бы незаметен в веб-приложении. В вебе шов почти бесплатен, и вас правильно приучили верить в это.
Двести миллисекунд незаметны при загрузке страницы. Вы подождете две секунды, пока отрисуется дашборд, и не заметите этого сознательно. Поэтому обычные паттерны подходят: вызвать сервис, дождаться ответа, отрисовать. Если запрос медленный, вы показываете спиннер, и пользователь ждет, в худшем случае слегка раздражаясь. В этом нет ничего плохого с точки зрения инженерии. Это правильный дизайн для среды, где пользователь смотрит на экран и понимает, что вещи загружаются.
Разговор — это не та среда. Там нет спиннера. Нет визуального сигнала, что работа идет. Есть только тишина, а тишина в разговоре означает для человека что-то конкретное: либо вы его не услышали, либо что-то сломалось. Двести миллисекунд, которые вы бы никогда не заметили на странице, — это момент колебания во время звонка. Две секунды — это мертвый эфир, и звонящий говорит «алло?»
Кстати, вы уже знаете об этом сбое из веба. Это N+1 запрос в пути рендеринга или «болтливый» API-вызов внутри цикла, где каждый отдельный запрос быстр, а совокупность — катастрофа. Разница в том, что в вебе вы узнаете об этом, когда страница кажется медленной. Во время звонка вы узнаете об этом, когда звонящий перебивает вашего агента или вешает трубку.
Голосовые агенты, построенные на веб-рефлексах, демонстрируют себя прекрасно. Один звонок, локальный стек, чистая сеть, несколько оборотов. Затем они сталкиваются с реальным разговором, который длится тридцать оборотов на реальном телефонном соединении, и то, что было нормально на трех оборотах, становится невыносимым на тридцати.
Какую задержку может выдержать оборот голосового ИИ?
Около полутора секунд. Это примерно бюджет, прежде чем человек на телефоне начнет чувствовать задержку, прежде чем он скажет «алло?», прежде чем решит, что линия оборвалась, прежде чем начнет перебивать вашего агента.
Пятнадцать сотен миллисекунд. Теперь посчитайте свои швы. Транскрипция, модель, синтез, возможно, поиск посередине. Каждый из них расходует один и тот же бюджет. Это перестает быть философией и превращается в арифметику. Восемь границ в обороте, и у каждой почти нет места. Добавьте один медленный запрос, и вы провалили всё дело, а это значит, что нужно отыгрывать время везде или выносить этот запрос с критического пути.
Вынос вещей с критического пути — это настоящий навык, потому что не каждый шов должен быть быстрым. Скажем, вам нужен поиск в CRM. Если вы можете запустить его в начале звонка, поместить запись в контекст и использовать ее минуту спустя, когда она понадобится, не имеет значения, где живет этот поиск и насколько он медленный. Он разрешился, пока никто не ждал.
Но если звонящий говорит что-то на третьем обороте, чего вы не могли предсказать, и вам нужна эта запись прямо сейчас, на полуслове, чтобы ответить ему. Этот запрос должен быть быстрым. Единственный способ сделать его быстрым — убить круговой запрос, а это значит, что он должен жить там, где живет звонок. Тот же поиск. Противоположное размещение. То, что это определяет, — не правило, которое мы можем вам дать. Это форма вашего конкретного сценария звонка и того, укладывается ли оборот в бюджет.
Впрочем, одна затянутая пауза — это еще не катастрофа. Звонок может пережить одну-две заминки. В этот момент это не будет ощущаться естественным, но люди прощают такое, точно так же, как они прощают собеседнику по телефону секундную заминку, чтобы подумать. Чего они не прощают, так это тридцати пауз, каждая из которых балансирует на грани допустимого бюджета времени. Ни одна из них технически не была «просрочкой», но весь разговор в итоге выматывает.
И когда вы знаете, что возникнет задержка, вы можете её прикрыть. Если поиск не может быть асинхронным и займет четыре секунды, агент может сделать то же, что и человек: сказать «подождите, я сейчас открою ваш аккаунт». Четыре секунды стоят столько же, но это уже не необъяснимая тишина. Вы «выкупили» это время, задав ожидание.
Это стоит заметить, потому что это третий способ разместить «шов». Вы можете переместить его ближе к началу звонка, вы можете убрать его с критического пути или оставить там, где он есть, и изменить ожидания звонящего, пока процесс выполняется.
Именно это делает мой звонок провайдеру таким наглядным примером. Я почти уверен, что эти шесть секунд ушли на поиск моего номера телефона, чтобы найти аккаунт, потому что первое, что было сказано после этого — что мой номер распознан. У этого поиска было как минимум три лучших места.
Они могли запустить его в тот же миг, как поступил звонок, еще до того, как агент заговорил, чтобы запись уже была в контексте к тому моменту, как я закончу фразу. Они могли бы «купить» это время еще дешевле, позволив телефону прозвонить на один цикл больше перед ответом. Никто не воспринимает четвертый гудок как задержку. Это единственное место в телефонном звонке, где секунда работы совершенно невидима, и у этого нет аналога в вебе, поэтому почти никто этим не пользуется.
А если ни то, ни другое было невозможно, если поиск действительно должен был произойти после приветствия, они могли бы так и сказать. «Позвольте мне открыть ваш аккаунт». Те же шесть секунд, но без «мертвого» эфира.
Проблема была не в шести секундах. Проблема была в шести секундах, которые никто не учел.
Почему мой ИИ-агент забывает то, что звонящий ему уже сказал?
Вернемся к моменту, когда голос изменился во время моего звонка провайдеру. Это была не задержка. Та часть, на самом деле, была быстрой.
Это была связность. Кто-то прикрутил этап аутентификации как отдельную систему, со своим сервисом, своим голосом, своим словарем и встроил это в поток, не передавая ничего через границу. Сервис аутентификации не знал моего имени, хотя агент, который меня перевел, уже его имел. Он не умел разговаривать так же, как остальная часть звонка. Поэтому стык между двумя системами просочился прямо в мой пользовательский опыт.
Я слышал этот шов. Большинство швов невидимы, и приходится доказывать, что они существуют. Тот же заявил о себе голосом незнакомца.
Передача звонка человеку была такой же ошибкой, даже хуже. Запись взаимодействия не пересекла границу. Все, что агент узнал обо мне, осталось на его стороне и там же умерло, поэтому я заплатил за это, повторяя одно и то же. Бюджет задержки не видит ничего из этого. Каждый переход в этом звонке может укладываться в бюджет, но вы все равно выпустите плохой продукт.
Что происходит с «живыми» звонками, когда ваш LLM-провайдер отключается?
У вас есть конвейер. AssemblyAI превращает речь в текст. Вы передаете текст в OpenAI. Вы передаете то, что вернулось, в Rime для озвучки. Три вендора, два шва, и в демо это работает великолепно.
Теперь OpenAI отключается. Или просто становится медленным, что хуже, потому что каждый звонок «хромает», вместо того чтобы чисто завершиться с ошибкой. Или они выводят вашу модель из эксплуатации во вторник. Любое из этих событий, и если вы сшили это самостоятельно, каждый активный звонок мгновенно обрывается.
Так собираетесь ли вы строить то, что вас спасет? Для одного агента? Вам нужно будет обнаруживать деградацию, переключаться на другого провайдера, переписывать промпты и вызовы, чтобы справиться с особенностями этого провайдера — а вы ведь знаете, что OpenAI, Groq, OpenRouter и Azure предоставляют «одну и ту же» модель со своими маленькими различиями, верно? — восстанавливать контекст для каждого звонка, который находится в середине фразы, когда вы переключаетесь, а затем возвращаться обратно, когда первый провайдер восстановится, и все это без сброса звонка.
Когда вы берете шов в свой собственный слой, вы владеете не только «счастливым путем». Вы владеете и поломкой. И у каждого шва есть поведение по умолчанию при поломке, определили вы его или нет. По умолчанию оно всегда плохое. По умолчанию это шесть секунд тишины. По умолчанию это уверенный неправильный ответ, потому что поиск выдал 500, а никто не сказал об этом модели. По умолчанию это бесконечный цикл «MFA SENT». Вы проектируете поведение при сбое, иначе поведение при сбое спроектирует ваш худший звонок.
Стоит ли мне поместить всю логику агента в один большой промпт?
Это заманчивый ход. Не выстраивать конвейер, полный границ, которыми нужно управлять. Отдать все одной модели. Написать агента как один гигантский промпт. Ноль швов для размещения. Чисто.
Закон сохранения говорит, что нельзя. Вы не убрали эти швы. Вы их закопали. Маршрутизация, защитные барьеры, поиск, бизнес-логика — все сплавлено внутри одного вызова модели, в том самом месте, где у вас меньше всего контроля.
Мы называем это «промпт-и-молись»: поместить правила в промпт и надеяться, что модель им последует, без детерминированной проверки вне модели для обеспечения чего-либо. Ошибка, которую замечают люди — это когда модель игнорирует инструкцию. Ошибка, которая стоит дороже — это то, что один гигантский промпт лишает вас рычагов управления. Больше не за что ухватиться.
Вы не можете передать модели новую информацию в середине звонка. Вы искали аккаунт, пока она говорила, и нет способа ей об этом сообщить. Вы не можете вырвать функцию из её рук: прием платежей относится к процессору, который ИИ никогда не видит, и теперь это невозможно. Вы не можете узнать, когда кто-то взломал ваш промпт, не говоря уже о сбросе контекста для восстановления.
Закопайте швы, и вы закопаете каждый рычаг, который не является самим промптом.
Как построить ИИ, который слушает звонок, не участвуя в нем?
Это та возможность, которая делает размещение швов конкретным, и большинство стеков вообще не могут вам её дать.
Агент, который слушает звонок и никогда в нем не говорит. «Тихая нога» на медиапотоке, слышащая всё, подпитывающая человека в реальном времени, улавливающая то, что он мог бы пропустить. Усиление человека вместо его замены.
Подумайте, что для этого нужно. Модель должна слышать одну сторону звонка, рассуждать о ней и возвращать что-то полезное в рамках того же хода, прежде чем момент будет упущен. Коуч, который приходит на две секунды позже, хуже, чем отсутствие коуча.
Если вы строите на агентском фреймворке, вы вообще не сможете этого сделать. Абстракция предполагает, что агент — это и есть звонок, поэтому на медиапотоке нет места для «молчаливого третьего». И вы не можете прикрутить это со стороны уровня приложения, потому что ваше приложение никогда не касается аудио. Оно видит только текст постфактум, на одной стороне кругового пути, за который вы уже заплатили.
В SignalWire это метод SignalWire Markup Language (SWML):
Вот и вся интеграция. Сложная часть никуда не делась. Переключение при сбоях, работа с задержками, перехват нужного сегмента медиапотока — всё это по-прежнему существует. Просто теперь это не часть вашего кода. Оно переместилось на уровень, созданный специально для таких задач, и в этом вся суть: стык сохраняется, поэтому вы размещаете его там, где он принесет меньше всего вреда. В данном случае это медиапуть, независимо от того, владеете ли вы им или используете стороннее решение.
Где живут стыки
Избежать стыков невозможно. Единственное, что вы можете решить — это где они будут находиться.
Разместите их ближе к медиапотоку, плотно привязав к уровню, предназначенному для их удержания, и вызывающая сторона их даже не почувствует. Если же разбросать их по краям только потому, что там их было проще всего внедрить, ваш пользователь заплатит за каждый из них: задержками, голосом незнакомца, зачитывающим код ошибки, или необходимостью дважды объяснять свою проблему двум системам, которые никогда не были связаны друг с другом.
Волна ИИ не отменила ничего из этого. Она лишь спрятала стыки внутри модели и убедила многих умных людей в том, что они исчезли.
Это не так. Найдите свои стыки и переместите их в более подходящее место.
Есть вопросы о создании более качественных голосовых ИИ-агентов? Присоединяйтесь к обсуждению в нашем сообществе разработчиков в Discord.








