LabHub
はじめる
学ぶ 学習パス コース

音声 AI エージェント — 聞いて、調べて、話すパイプライン

予約エージェントをステートマシンで — 確認・リトライ・引き継ぎ

LabHubで続きを見る

目標

電話予約エージェントをステートマシンとして作り、確認の質問なしには予約せず、読み取りツールのタイムアウトだけをリトライし、聞き取れない場合や人を求められた場合には、要約とともに引き継ぐようにします。

なぜ重要なのか

音声エージェントの事故は、ツールで起きます。元に戻せない行動が確認なしに起きたり、タイムアウトを再送して予約が2つになったりします。このラボのツールはvoicekit.clinic.ClinicAPI、つまり失敗を計画表として入れられるダミーの予約APIで、ユーザーの発言はvoicekit.nlu.parseが項目に変換します。採点ツールは、/root/voice/agent/agent.pyからVoiceAgentを読み込んで9つのシナリオ(失敗計画 + ユーザーの発言)を直接動かし、発言の文言ではなく状態とツール呼び出しの記録で判定します。

ステップ

  1. ClinicAPIに失敗を入れてみて、ツールごとに、元に戻せないかどうかと、やり直す価値のあるエラーを/root/voice/agent/tools.jsonに書いてください。
  2. 8つの文の意図を、LLM(json_schema)とルールパーサーで取り出し、/root/voice/agent/intents.jsonlと正解数/root/voice/agent/intent_acc.jsonを作ってください。
  3. VoiceAgent(api, sleep)とhandle(text)を持つ/root/voice/agent/agent.pyを作り、確認で「はい」を受け取ったあとにだけ予約するようにしてください。
  4. 確認の質問への「いいえ」(新しい時刻があれば、その時刻で再確認)を処理してください。
  5. 読み取りツール(find_slots)のタイムアウトを、0.5・1.0秒待ちながら2回まで呼び直し、それでもだめならtool_failureとして引き継いでください。
  6. 人を求められたらuser_request、2回続けて聞き取れなければnot_understoodとして引き継ぎ、引き継ぐときに埋まった項目を要約してください。
  7. 予約ツールの拒否(空きなし)は、空き時間を探し直して尋ね、タイムアウトは再送せずに引き継いでください。
  8. 9つのシナリオを動かしたターンの記録を、/root/voice/agent/traces.jsonlに残してください。

参考

ツールの性質を書き出す

voicekit.clinicのTOOLSとClinicAPI(plan={"find_slots": ["timeout", "ok"], "book": ["taken"]})で失敗を実際に起こしてみて、/root/voice/agent/tools.jsonにtools(ツール名ごとに{"irreversible": true/false})とretryable(やり直す価値のあるエラー名のリスト)を書いてください。

TOOLSの値は(元に戻せないか、説明)です。ToolTimeoutは「応答がなかった」、ToolErrorは「拒否された」です。どちらが再送する価値がありますか。

LLMとルール、どちらが意図を当てるか

8つの文、「I'd like to book an appointment for Tuesday」(book)、「CAN I CANCEL MY VISIT ON FRIDAY」(cancel)、「YEAH THAT WORKS」(yes)、「NO THAT'S NOT RIGHT」(no)、「CAN I TALK TO A REAL PERSON」(human)、「WHAT TIME DO YOU CLOSE TODAY」(hours)、「TEN THIRTY IN THE MORNING」(inform)、「MY DOG ATE THE REMOTE」(unknown)を、この順序で、intentを8つの値のenumに縛ったjson_schemaでLLMに尋ねた結果(llm)と、voicekit.nlu.parseの結果(rule)を/root/voice/agent/intents.jsonlに{"text", "llm", "rule"}の形で書き、括弧内の正解と合っていた数を/root/voice/agent/intent_acc.jsonに{"llm": n, "rule": m}の形で書いてください。

voice-llm upのあと、voicekit.llm.chat(messages, max_tokens=40, json_schema=schema)です。形式は文法が強制しますが、内容はモデル次第です。結果がおかしければ、それがこのステップで見たいものです。

確認を受けたあとにだけ予約する

/root/voice/agent/agent.pyにVoiceAgent(api, sleep=time.sleep, max_retries=2, backoff=(0.5, 1.0))とhandle(text)を作ってください。parseで項目(day・time・name)を埋め、空き項目を、日付 → (find_slotsで空き時間を探して提示)時刻 → 名前の順に尋ねたあと、CONFIRMで読み返し、「はい」(intent yes)を聞いたときにだけapi.book(day, time, name)を呼んでDONEにしてください。採点: 5ターンの会話と、一度にすべて話す2ターンの会話で、bookが最後のターンにちょうど1回です。

「次に尋ねること」を決める関数を1つ(_next)置くと、一度に複数の項目を話した場合も同じコードで処理できます。提示した空き時間(offered)にない時刻なら、もう一度提示します。名前の番でパーサーが名前を取り出せなければ、発言の最後の単語を名前として使います。

確認の「いいえ」は修正

CONFIRMでintentがnoのとき、発言に新しい時刻・日付があれば、その項目だけを変えてもう一度確認(CONFIRM)し、何も情報がなければ、時刻を空にして空き時間を再提示してください。どちらの場合も、bookは呼びません。採点: 「…ten a m…」→「no, make it two thirty」→「yes」で、bookが14:30で最後のターンに1回です。

parse('no, make it two thirty')は、intentのnoとtimeの14:30を一緒に返します。noだからといって無条件に最初に戻ると、ユーザーがたった今言った情報を捨ててしまいます。

読み取りツールのタイムアウトはやり直す

find_slotsがToolTimeoutを出したら、self.sleep(backoff[시도 번호])で0.5秒、1.0秒待ちながら、最大max_retries(2)回呼び直してください(プレースホルダーは試行番号です)。3回とも失敗したら、状態をHANDOFFに、handoff.reasonをtool_failureにしてください。採点: 最初の呼び出しだけタイムアウトなら2回呼び、待った時間[0.5]でASK_TIME、3回とも失敗なら3回呼び、待った時間[0.5, 1.0]でHANDOFFです。

sleepをコンストラクターの引数として受け取っておくと、採点ツールが待たずに「どれだけ待とうとしたか」を記録できます。テストできるように作ることも設計です。

人への引き継ぎ: 要約とともに

どの状態でも、intentがhumanなら、すぐにHANDOFF(user_request)にしてください。intentがunknownなら、1回は言い直しを頼み、続けて2回目ならHANDOFF(not_understood)にします。聞き取れたら回数を0に戻します。引き継ぐとき、handoff.summaryに、ここまでに埋めたday・time・nameを入れてください。採点: 「hmm」→「blah blah」で2ターン目に引き継ぎ、「Can I book on Friday」→「can I talk to a person」で、要約にfridayが入っています。

名前の番(ASK_NAME)では、パーサーがunknownを出しても、人が名前だけを話した可能性があります。そのときは、聞き取れたものとして扱います。

元に戻せないツールが失敗したとき

bookがToolError(枠が埋まった)を出したら、再送せずに時刻を空にして、find_slotsで空き時間を探し直して提示してください(ASK_TIME)。bookがToolTimeoutを出したら、再送せずにHANDOFF(tool_failure)で引き継いでください。採点: 空きなしのあと、新しい時刻14:30で再確認を受けて、bookが2回(10:00は失敗、14:30は成功)、予約のタイムアウトで、bookが1回のあとHANDOFFです。

予約のタイムアウトは、「できなかった」ではなく「わからない」です。サーバーは予約を作ったのに、応答だけが遅れた可能性があります。再送すると、予約が2つになります。

9つのシナリオのターン記録

9つのシナリオ(happy・one_shot・confirm_no・retry_ok・retry_fail・not_understood・wants_human・taken・book_timeout。失敗計画とユーザーの発言は、各ステップの課題の採点説明と同じです)を自分のagent.pyで動かし、ターンごとに{"scenario", "turn", "user", "state", "say", "tools"(이번 턴에 부른 도구 이름), "outcomes"}を/root/voice/agent/traces.jsonlに書いてください(プレースホルダーは、そのターンで呼んだツール名です)。採点ツールが同じシナリオをもう一度動かして照合します。

ツールの記録は、api.callsに(名前、引数、結果)として積まれます。ターンの前後の長さの差で、そのターンで呼んだものだけを取り出します。この記録がそのままエージェントの回帰テストの材料です。