12/21 は app層(dialog)の状態機械(Listening/Thinking/Speaking/Ending)を草案としてまとめました。
今日はそれを支える「運用の作法」―― タイムアウト / キャンセル / 失敗時の扱い を、SIP/RTP/AI それぞれの責務に沿って整理します。
音声ボットで一番つらいのは「無言」です。
なので本記事のゴールは、エラーや遅延が起きても 何かしらの反応を返し、必要なら切断まで持っていく ための方針を固定することです。
0. まず大原則:検知と判断を分ける
このプロジェクトではレイヤ分離を前提にしているので、失敗時も次の原則を守ります。
- 検知するレイヤ(sip/rtp/ai):事実を検知してイベントで上げる
- 判断するレイヤ(session/app):継続するか、謝罪するか、切るか決める
例:
- RTPが来ない → rtpが
RtpTimeoutを出す(検知)
→ session/app が「待つ/警告/切断」を決める(判断)
1. タイムアウトの種類(何が起きるか)
音声ボットで起きるタイムアウトは大きく3種類です。
-
SIP側(呼制御)
- トランザクションの再送や応答待ち
- セッションタイマ(将来)
-
RTP側(メディア)
- 音が来ない(無音ではなく“パケット無着信”)
- ジッタ過大(遅延が大きすぎる)
-
AI側(ASR/LLM/TTS)
- API遅延
- エラー(ネットワーク/上限/内部エラー)
- ストリーム中断
2. SIPの作法(MVP→次の段階でも壊れないために)
2.1 検知(sipの責務)
- パース不能 → ログ+(必要なら)400
- トランザクション期限切れ →
TransactionTimeoutを上位へ通知 - 再送 → 最後の応答キャッシュで返す(MVP)
2.2 判断(sessionの責務)
- early(200前)でタイムアウト → セッション破棄(相手は再INVITEする前提でOK)
- confirmed(通話中)でタイムアウト → 原則BYEで終了に寄せる
MVP段階では「厳密なRFC」より「変な状態で固まらない」を優先します。
3. RTPの作法(メディア無着信は“段階対応”)
3.1 検知(rtpの責務)
- 一定時間RTPが来ない →
RtpTimeout(call_id, elapsed)を上げる - 変なパケット(SSRC違い等) → ログ+(必要なら)ストリームエラーイベント
3.2 判断(session/appの責務)
RTP無着信は「一発で切る」より段階対応が安全です。
- 1回目:警告ログ+継続(回線揺れの可能性)
- 連続 or 一定回数超え:ボットが音声で「聞こえません」等を返す(可能なら)
- さらに継続:BYEで切断(無言通話を避ける)
MVPの現実解としては「連続N回で切断」でも十分です。
4. AIの作法(ASR/LLM/TTS:無言にならない最重要ポイント)
AI失敗は頻度が高いので、明確な方針が必要です。
4.1 検知(aiモジュールの責務)
- タイムアウト / HTTPエラー / 解析失敗などを
AsrError / LlmError / TtsErrorとしてappへ返す - リトライは ai 側で吸収して良い(回数と間隔を決める)
- “最終的に失敗した” だけを app に伝える(appを複雑にしない)
4.2 判断(appの責務)
基本方針(例):
-
初回失敗:謝罪定型を喋って継続
- ASR失敗:「すみません、もう一度お願いします」
- LLM失敗:「ただいま混み合っています。もう一度お願いします」
- TTS失敗:可能ならテキストでログ、次は短い別文でリトライ
-
同一フェーズで連続失敗(例:2回):切断判断へ
-
Endingに遷移 → 最後に謝罪 →SessionAction::Bye
-
「失敗回数カウント」は app の状態に持たせるのが管理しやすいです。
5. キャンセルの作法(barge-inや“遅いLLM”対策)
音声ボットはリアルタイムなので、キャンセルを設計しないと破綻します。
5.1 何をキャンセルするか
- LLM問い合わせ中にユーザが話し始めた(barge-in)
- TTSで喋っている途中にユーザが被せた
- 古い要求(前の発話)に対する結果が今さら返ってきた
5.2 最小実装の考え方
- app側で
turn_id(発話ターン番号)を持つ - ASR final を受けたら
turn_id += 1 - LLM/TTSの要求にも
turn_idを付ける - 返ってきた結果の
turn_idが古ければ捨てる(“遅延結果”対策)
さらに進めるなら:
- tokio task を abort して「本当にキャンセル」する(ただしまずは論理キャンセルでも十分)
6. “無言回避”の定型文(MVPのおすすめ)
失敗時に喋る定型は、長いと逆に邪魔なので短いのが良いです。
- ASR失敗:「すみません、もう一度お願いします。」
- LLM失敗:「ただいま混み合っています。もう一度お願いします。」
- RTP途絶:「音声が聞こえません。もう一度お願いします。」
- 連続失敗で終了:「申し訳ありません。通話を終了します。」
※ ずんだもん口調は後で統一(LLM側プロンプトでも可)
7. どこにポリシーを書くか(置き場所)
設計上はこの形が分かりやすいです。
- ai:リトライ方針(回数/間隔/タイムアウト)
- rtp/sip:検知イベントの定義(Timeout/Error)
- app:ユーザ向け振る舞い(謝罪/継続/終了)、turn_id、連続失敗の閾値
- session:通話維持/終了の最終操作(BYE、RTP停止など)
まとめ(12/22の固定点)
- 「検知(sip/rtp/ai)」と「判断(session/app)」を分ける
- RTP無着信は段階対応(警告→謝罪→切断)
- AI失敗は“無言回避”が最優先(初回は謝罪で継続、連続失敗で終了)
- キャンセルは
turn_idによる論理キャンセルから始めるのが現実的
次回(12/23)は、あとから困らないための 録音・メタ情報・可観測性(ログ相関、レイテンシ計測) をまとめる予定です。