音声 AI エージェント — 聞いて、調べて、話すパイプライン
声で動かすツール — ステートマシン・確認の質問・リトライ・人への引き継ぎ
一言でいうと
音声エージェントの事故は、たいていツールで起きます。「はい」という返事がないまま予約が作られ、タイムアウトを再送して予約が2つになり、聞き取れないまま同じ質問を繰り返します。そのため、何ができるかはステートマシンが決め、モデルはユーザーの発言を項目(日付・時刻・名前)に変換する仕事だけを行います。元に戻せないツールは確認の質問のあとにだけ呼び、タイムアウトを再送しません。2回聞き取れなければ人に引き継ぎ、引き継ぐときにすでに聞いた内容を要約して渡します。
なぜ必要なのか
電話予約アシスタントをLLM 1つで作ると、会話全体をモデルに渡し、「次に何をするか」も任せることになります。このイメージの0.5Bモデルに、意図分類をjson_schemaで行わせてみたところ、形式は完璧に守りましたが、8文のうち1つしか当てられませんでした。ほとんどすべての文を、enumの最初の値である「book」に分類したのです(このモジュールのラボのステップ2で、自分で測ります)。形式を文法で強制しても、内容は強制されません。このようなモデルに「もう予約してよいか」を任せると、「いいえ」でも予約が作られる可能性があります。
どう動くのか
項目の穴埋めと状態。予約に必要な項目は、日付・時刻・名前の3つです。状態は、LISTEN → ASK_DAY → ASK_TIME → ASK_NAME → CONFIRM → DONEと流れ、どの状態からでもHANDOFF(人へ)に移れます。ユーザーが一度に「火曜日の午前10時、名前はJamie」と言えば、空いている項目がないので、すぐCONFIRMに進みます。状態が実行できることを決めるので、bookはCONFIRMで「はい」を聞いたときにだけ呼ばれます。モデルが何を言っても、構造的にそうなります。
確認の質問への「いいえ」。「いいえ、2時半でお願いします」は拒否ではなく修正です。新しい時刻が含まれていれば、その項目だけを変更してもう一度確認し、何も情報がなければ、何が違うのかわからないので、時刻から聞き直します。音声は文字より誤認識が多いので、元に戻せない行動の直前の読み返しは、選択ではありません。
リトライとバックオフ。ツールのエラーは2種類に分けます。タイムアウト(ToolTimeout)は、同じリクエストを再送すればうまくいくことがあります。拒否(ToolError、例: その枠がたった今埋まった)は、再送しても同じ答えが返ります。読み取り専用のツール(空き時間の検索)のタイムアウトは、0.5秒、1.0秒のようにだんだん長く待ちながら2回まで呼び直し、それでもだめなら、抱え込まずに引き継ぎます。電話では、待つ数秒がそのまま沈黙です。
元に戻せないツール。予約(book)がタイムアウトで終わったなら、それは「できなかった」ではなく「わからない」です。サーバーは予約を作ったのに、応答だけが遅れた可能性があります。再送すると予約が2つになります。そのため、元に戻せないツールのタイムアウトは、リトライせずに、人が確認できるよう引き継ぎます(冪等キーのあるAPIなら、同じキーで再送して安全に確認できます。そうしたAPIでなければ行いません)。拒否(空きなし)は、空き時間を探し直して、改めて尋ねます。
人に引き継ぐ。引き継ぎでは、3つのことを守ります。ユーザーが人を求めたら、いつでもすぐに引き継ぎます。2回続けて聞き取れなければ、3回目を試みません。そしてすでに聞いた項目を要約して一緒に渡します。受け取る人に最初から聞き直させることが、最悪の引き継ぎです。
テストは録音ではなく記録で。エージェントをテストするとき、発言の文言を比較すると、表現を変えるたびにテストが壊れます。失敗計画を入れたダミーのツールとダミーのsleepを差し込み、状態の流れとツール呼び出しの記録を比較します。リトライのバックオフも、実際には待たずに、「どれだけ待とうとしたか」で確認します。
現場での姿
LangGraphコースが同じ考えをグラフで扱います。ノードが状態を変え、条件付きエッジが次を決め、人の承認が必要な場所で止まります。MCPサーバーのコースは、ツール側で同じ境界を守ります。ツールが自分で権限を絞り、破壊的な操作に確認を求めます。音声エージェントは、その2つの間で、人が画面なしに耳だけで確認するという制約を加えます。
ルールパーサーも完璧ではありません。このコースのパーサーは、「I need to see the doctor on Thursday afternoon」を、予約(book)ではなく情報提供(inform)として読みます。「予約」を意味する単語がリストにないからです。それでも、ステートマシンの中では大きな事故は起きません。日付が埋まったので次の空き項目(時刻)を尋ね、結局は確認の質問を通ります。理解モジュールが間違えても、元に戻せないことは確認で止まります。これがステートマシンを使う理由です。理解モジュールをLLMに替えても、この構造はそのまま残します。変わるのは、項目を埋める部品だけです。
次のラボですること
ダミーの診療予約APIに失敗を入れてみて、ツールの性質を書き出します。LLMとルールパーサーの意図の正解率を測ります。予約エージェントをステートマシンとして作り、ステップごとに機能を足します。順調な流れ、確認の「いいえ」、読み取りツールのリトライ、人への引き継ぎ、元に戻せないツールの失敗です。採点ツールは、自分のagent.pyで9つのシナリオを直接動かします。