Перейти к содержимому
Экосистема AI Vibe
← Назад к AI Vibe News

Редакция 9 августа 2026 г.

Разборы

Двухфазная граница для голосового агента: модель предлагает, приложение коммитит

Двухфазная граница для голосового агента: модель предлагает, приложение коммитит

Голосовой агент с инструментами внешнего эффекта без явного согласия — та же боль, что у MCP-сервера или IDE-агента, которому вы открыли «опубликовать» или «удалить». В туториале Susie Wang разбирает TypeScript-паттерн для Tencent RTC Voice Companion: модель может подготовить действие, но закоммитить его может только свежее явное «да» пользователя — с state machine, короткоживущими tickets и drills перед релизом. Для соло-разработчика, который собирает voice- или agent-стек без отдельного backend-отдела, это переносимый приём human-in-the-loop на границе tool-calling.

Почему «гладкий голос» не заменяет границу authority

Сценарий из вводного примера: пользователь говорит «Tell the room I'll leave at eight—actually, don't send that». Модель может успеть сформировать аргументы tool до перебивания; без transaction boundary сообщение уходит в комнату, пока ассистент ещё озвучивает «Sure».

Автор формулирует тезис прямо: «A smooth voice is a presentation layer. Trust comes from the state machine behind it.» Разработческая ценность смещается от prompt polishing к authority, окну валидности, expiry и UX, когда certainty недостижима.

Для агентных архитектур — voice, Dify-workflow или MCP-tool с внешним эффектом — это не косметика: LLM и агент-платформа не должны получать прямое право коммитить consequential action.

Фаза prepare и фаза commit: путь без прямого commit из модели

Инвариант паттерна: вывод модели может подготовить действие, но только явное подтверждение пользователя может его закоммитить.

Фаза 1 (prepare / preview): распознанная речь → координатор хода приложения → LLM или Dify → валидированное предложение (MessageProposal) → серверный prepared ticket (PreparedAction) → озвученный preview («Post this message to the room: … Say yes to confirm or no to cancel.»).

Фаза 2 (confirm / commit): узкий классификатор подтверждения (не LLM) → повторная проверка прав (canPublish) → идемпотентный commit через ActionBroker.commit(ticket, context).

RTC audio → speech recognition → application turn coordinator → LLM or Dify
→ validated action proposal → server-side prepared ticket → spoken preview
→ explicit user confirmation → permission recheck → idempotent commit

Модель и Dify не получают credential для commit; переход commit принадлежит координатору приложения. Для OpenAI-compatible стека автор подчёркивает: commit(ticket) не должен быть tool, доступным модели.

Consequential tools: одно демо-действие и таблица эффектов

В туториале демонстрируется одно application-defined действие — publishRoomMessage (kind: 'publishRoomMessage', поля roomId, text). Конкретный сценарий в коде и тестах: сообщение «I will leave at eight.» в комнату room-1.

Таблица классификации эффектов задаёт политику по умолчанию:

Эффект Пример из статьи Политика
Read-only Search a public catalog Allow with normal validation
Local and reversible Change companion's temporary voice style Allow or Undo
Shared and reversible Add a track to a room queue Confirm when social impact is meaningful
External or audience-visible Publish a room message Require explicit confirmation
Sensitive or difficult to reverse Purchase, delete, invite, disclose private data Strong confirmation or do not expose

Публикация в комнату попадает в класс «external or audience-visible» — именно здесь паттерн двух фаз становится обязательным, а не опциональным UX-улучшением.

State machine, tickets и idempotency

Состояние описано discriminated union VoiceActionState: idle | requesting-model | previewing | awaiting-confirmation | executing | completed | recovery (причины recovery: expired, denied, provider-error, commit-uncertain).

Ключевые механизмы:

  • TicketrandomUUID(), одноразовый идентификатор prepared action, не reusable credential для модели.
  • Срок жизниexpiresAt = now + 30_000 (30 секунд в коде примера).
  • Idempotency — ticket передаётся в RoomPublisher.publish(..., { idempotencyKey: ticket }).
  • Повторный commit уже закоммиченного ticket — ранний return без дублирования эффекта.

При ошибке publish после executing записи переходят в uncertain; контроллер озвучивает, что результат не верифицирован и автоматический retry не выполняется.

Валидация proposal жёстко привязана к сессии: roomId сверяется с доверенным session context; текст сообщения после trim — от 1 до 280 символов; JSON от модели парсится как untrusted input.

Перебивание, ambiguous и граница Dify/OpenAI

Если пользователь начинает говорить во время previewing, speech.stop() останавливает озвучку, состояние → awaiting-confirmation; commit не происходит.

Подтверждение — только после транскрипта с классификацией yes / no / ambiguous. Фразы вроде yes, yes post it, confirm, send it против no, cancel, dont send it, do not send it. ambiguous не считается согласием — публикация не выполняется.

После старта внешнего commit прерывание не откатывает операцию; компенсация — отдельное авторизованное действие.

LlmAdapter.propose возвращает только proposal с allowedActions: readonly ['publishRoomMessage']. Один application-generated request ID на voice turn — для логов и маршрутизации. Для Dify: workflow может дать proposal, но не credential публикации. Инструкция модели: «Never claim an action completed» — приложение валидирует, подтверждает и исполняет.

Восемь failure drills перед релизом consequential tool

Раздел «Failure drills to run before release» перечисляет сценарии, которые автор считает обязательными до включения side-effect tool:

  1. LLM returns a different room ID — отклонить на prepare; scope только из аутентифицированной RTC-сессии.
  2. User loses permission after preview — recheck authorization at commit time.
  3. Speech recognition produces “yes” from background audio — узкая фраза подтверждения, корреляция с активным ticket, button fallback; для high-risk voice-only может быть недостаточно.
  4. Ticket expires while user is thinking — cancel; новый preview при сохранении намерения; не продлевать старый ticket молча.
  5. Model provider times out — recoverable conversational state; ticket не создаётся; retry с тем же turn correlation без дублирования prepared actions.
  6. Publish request times out after reaching server — uncertain; query by idempotency key если downstream поддерживает; иначе visible recovery choice, не автоповтор publish.
  7. RTC connection drops during confirmation — ticket истекает; reconnect не трактует старый transcript как новое согласие.
  8. Prompt injection asks model to skip confirmation — модель имеет только proposal authority; commit capability в коде недоступна модели.

Юнит-тесты (controller.test.ts) покрывают: interruption + «no» → published пуст; ambiguous speech → остаётся awaiting-confirmation; double «Yes, post it» → одна публикация, completed.

Tencent RTC: orchestration layer отдельно от media и model

Сценарий Tencent RTC Conversational AI описан как real-time voice interaction с подключением к нескольким LLM-провайдерам. Документация LLM-конфигурации покрывает OpenAI-compatible модели и агент-платформы вроде Dify, а также request identifiers для маршрутизации и observability.

Слои media, speech, model и application authorization остаются раздельными; код паттерна живёт в application orchestration layer и не подменяет RTC/messaging API. Интерфейсы (SpeechOutput, LlmAdapter, события onUserSpeechStarted, onTranscript) обозначены как integration seams — точная SDK-wiring зависит от клиентской платформы.

Автор указывает relationship disclosure: материал написан в связи с Tencent RTC; официальная документация Tencent RTC использована как implementation reference.

Чеклист и перенос на MCP и IDE-агенты

Перед релизом consequential tool автор предлагает двенадцать пунктов: parse proposal as untrusted; trusted session scope; model propose / app commit; single-purpose short-lived user-bound tickets; preview before confirm; ambiguous ≠ consent; barge-in без commit; permission recheck before execute; duplicate callbacks не дублируют эффект; timeouts → uncertain без выдуманного success/failure; коррелированные логи без лишнего хранения приватного аудио/текста; visible stop/cancel/confirm control.

Прямых сравнений с Cursor или MCP в первоисточнике нет — но логика «proposal vs commit» переносима на любой агентный стек, где LLM или workflow предлагает side-effect, а приложение держит state machine, tickets и human confirmation. Тот же принцип, что при проектировании MCP-tool boundary: модель видит инструмент, но commit с внешним эффектом остаётся на стороне приложения с явным consent.

Если вы собираете voice- или coding-agent workflow в одиночку, начните с drills №3, №4 и №8 — фоновое «yes», истёкший ticket и prompt injection — до полировки промпта.

Источники