Oathra

オープンソース · Apache-2.0 · TypeScript

AIが電話で値切って、予約を取る。「確定」に✓が付くのは、相手がそう言った時だけ。

下は、GPT-4o mini が定価23,500円のホテルと交渉した実際の通話です。保存されたイベントをそのまま3倍速で描き直しています。録画ではありません。

無理難題ホテル · openai:gpt-4o-mini再生
00:00 / 01:26
0 / 10,000意地悪な店員シミュレーション1万通りでの誤完了
約0.4秒実電話での最初の返答(GPT-Live)
1コマンドclone から AI 同士の通話まで
3 + N電話会社: Twilio · Plivo · SIP · 自分で追加

作った理由

AIに電話をさせる仕組みは、作ろうと思えば一晩で動きます。厄介なのはその先で、席が取れていないのに「予約できました」と言い切る。留守番電話に向かって予約を依頼し続ける。値段が予算を超えているのに、話の流れで受けてしまう。どれも実際に起きました。

そこで、完了の判定をモデルから取り上げてコードに移しました。日時、人数、金額、そして「確定したか」は、相手の発言から取り出した証拠が揃ったときだけ埋まります。AI 側がいくら「確定しました」と言っても、それは記録されるだけで証拠にはなりません。

同じランタイムをシミュレータでも本物の電話でも動かせるようにしたので、まずはブラウザで AI 同士に交渉させて遊び、納得したら電話会社を繋いでください。

Arena の画面。GPT-4o mini がホテルと交渉中。ミッションの金額は未確定のまま 21,100 と表示
交渉の途中。ホテルが 21,100 円を出した直後で、「金額 ≤ 20,000」はまだ埋まらない。証拠は相手の発話ごとに右側へ積まれていく。
Arena の画面。ホテルの「ご予約承りました」で確定に✓が付き、証拠に confirmed = はい(相手)が並ぶ
ホテルが「1泊19,900円でご予約承りました」と言った瞬間。「確定」に✓が付き、証拠の先頭に confirmed = はい(相手)が入る。AI 側の発言では、この行は絶対に埋まらない。

まず遊ぶ。AI同士か、自分が店員になるか。

シナリオは6本。19時が満席のレストラン(日本語版と英語版)、底値を隠しているホテル、まとめ買いの値引き交渉、シリアル番号の復唱、そして全時間帯が満席で「取れませんでした」と言うのが唯一の正解になる罠。モデル同士を同じ条件で戦わせて、カードにして共有できます。

$ npx oathra battle impossible-hotel \
    --agent scripted --agent openai --agent gemini \
    --png card.png --json run.json

Impossible Hotel
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
scripted                    ████████████████████    6540
openai:gpt-4o-mini          ███████████████████▋    6444
gemini:gemini-flash-latest  ███████████████████▌    6410

                            SUCCESS  TIME   COST  SCORE
scripted                    ✓         69s  $0.00   95.4
openai:gpt-4o-mini          ✓         95s  $0.00   89.5
gemini:gemini-flash-latest  ✓         79s  $0.00   90.1

False completions: 0

上の動画の元になった実行です。定価23,500円のホテルを、組み込みAI・GPT-4o mini・Gemini Flash がそれぞれ2万円以下まで交渉し、成立価格は 19,900 / 19,900 / 20,000 円。得点の差は価格と所要時間の差です。ホテル側の「ご予約承りました」が出た通話だけが成立と数えられます。勝ち負けより、その一行のほうが大事だと思っています。

この実行で、最初は誤完了が1件出ました。ホテルが値段を言い直しただけの発話を「はい」と数えていたからです。証拠エンジンを直し、同じ3通話を流し直して0にしました。そういう見つけ方をするための対戦機能です。

63秒 · 画面録画 · 音声あり

3つのAIが同時に電話した記録(63秒・音声あり)。3つのウィンドウは Arena の再生画面そのまま、声は記録された発話を読み上げたもの。

同じ仕組みで、本物の電話。電話会社とモデルは別々に選ぶ。

Twilio はメディアストリーム直結の最短経路をそのまま使います。Plivo や手持ちの SIP 回線は SIP ゲートウェイ(初期実装は LiveKit、Cloud でもセルフホストでも可)を経由します。音声モデルは別軸で、テスト通話で一番自然だった GPT-Live を既定にし、Deepgram + LLM + TTS のパイプラインも残しています。

$ npx oathra setup phone

音声エンジンを選択
❯ GPT-Live        推奨 · 会話が最も自然
  OpenAI Realtime speech-to-speech
  Pipeline        Deepgram + 任意のLLM + OpenAI TTS

電話会社を選択
❯ Twilio          直結 · 実通話で検証済み
  Plivo           SIP経由
  Custom SIP

Twilio を設定中
   認証情報を確認             account · Full
   音声発信できる番号を検出    +1 947 ···
   日本向け発信の許可を確認
   Media Streams              通話ごとに設定
✓ Twilio の準備ができました。

$ npx oathra phone doctor --to +81…
Carrier         認証 · 有効 · 発信元番号 · 日本向け発信 OK
Media          · 実通話が必要 — oathra phone test
Voice Engine    GPT-Live (gpt-live-1)
Latency        Oathra → api.twilio.com   244 ms
Cost           日本の携帯 · twilio · $0.185/分
READY ✓
  • 自動化できない所は、案内する発信元番号の本人確認や海外発信の許可など、人の操作が必要な手順はリンク付きの一手順として表示し、終わるまで待ちます。「ワンクリック」と言い張るより、正直なほうが使いやすいと考えました。
  • 通話料を払わずにテストするoathra phone test は Local(¥0、合成音声でエンジンを往復)→ Gateway(¥0)→ PSTN(有料)の3段階。開発中のバグ1つごとに1分¥28を払わずに済みます。
  • どの層が壊れたかを doctor が言う電話会社 · SIPゲートウェイ · 音声 · 音声モデル · 遅延 · 料金を段ごとに確認し、失敗した項目には直し方を添えます。

実際に電話してみた記録

1回目Deepgram + GPT-4o-mini + TTS の三段構成。会話は成立したが最初の返答まで 11.8秒。「聞こえますか?」と何度も聞き返された。店員役が「大丈夫です」としか言わなかったので、結果は正しく INCOMPLETE。AIは「確定です」と言ったが、証拠にはならなかった。
2回目TTS のストリーミング化で応答は 2.2秒に。ただし最初の2分間は留守番電話に予約を頼み続けていた。雑音を割り込みと誤認して13回も話を止めた。留守電検出と、相槌を割り込みにしない判定を追加。
3回目GPT-Live(全二重)に切り替え。「もしもし」から返答まで 約0.4秒。6分25秒、112ターン、エラーなしで雑談が成立。「バイバイ」で切れない問題が残ったので、終話ツールと無音25秒の安全装置を入れた。

通話料は Twilio で日本の携帯宛 ¥28.78/分、GPT-Live はセッション $0.05/分。この記録の総額は数百円です。

「予約できました」は、証拠ではない

返ってくるのは VerifiedResult です。各項目は相手側の発話(実電話では音声区間)に紐づき、完了はコードが判定します。意地悪な店員を1万通り生成して回すと、誤完了はゼロ。ワンコマンドで再現できます。

{
  "field": "time",
  "value": "19:30",
  "source": "callee",
  "transcript": "19時はいっぱいですが、19時半でしたら空いております。",
  "span": "19時半",
  "explicit": true,
  "verified": true,
  "note": "accepted with value restated by caller",
  "audio": { "startMs": 14210, "endMs": 18960 }
}

完了 = 接続済み
     ∧ 日付.検証済み ∧ 時刻.検証済み
     ∧ 人数.検証済み ∧ 確定.検証済み
     ∧ 制約を満たす

$ npx oathra eval --adversarial 10000
never-confirm · wrong-restate · negate-then-offer
silent-hangup · caller-echo-trap
False Completion: 0 / 10000 adversarial runs  6.2s
  • 断りは提示にならない「19時はいっぱいですが」から 19:00 の証拠は作られない。
  • 言い直しは上書き「13日、いや14日」なら14日が残り、上書きの履歴が付く。
  • 曖昧な返事は確定にならない「たぶん大丈夫」は同意でも確定でもない。
  • 確定できるのは相手だけAI が「予約しました」と言っても記録されるだけ。
  • 確定は古くなる「承りました」の後に金額が変われば、確定し直しが必要。
  • 引き下がりは受諾ではない「わかりました、他を探します」で何も検証されない。

組み合わせは自由

電話会社
Twilio direct実通話で検証済み
Plivo sipゲートウェイ経由 · PSTN 未検証
Custom SIP sip任意のトランク
Telnyx · Wavix · Sinch · didlogicv0.2
自分で追加oathra provider create phone <id>
音声モデル
GPT-Live 推奨全二重 · $0.05/分
OpenAI Realtimespeech-to-speech
PipelineDeepgram + 任意のLLM + TTS
LLMOpenAI · Gemini · Ollama · 組み込み
ローカル実験段階

使い方

  1. ブラウザで見るnpx oathra demo。シナリオを選ぶと AI 同士が電話を始めます。「自分が電話に出る」を選べば、あなたが店員として AI の交渉を受けられます。
  2. ターミナルで回すoathra play impossible-hotel --fast で一瞬、oathra eval で全シナリオと誤完了の数、oathra eval --callee openai で店員役を GPT-4o mini に任せた評価(数円)、oathra replay <id> --at 00:18 でその時点の状態に戻れます。
  3. 電話会社を繋ぐoathra setup phone で質問に2〜3個答えるだけ。oathra phone doctor で各層を確認し、oathra phone test --level local は通話料ゼロです。
  4. 本物の番号に電話するoathra call --to +81… --scenario restaurant-reservation。録音と全イベントが .oathra/calls/ に残り、結果は相手が実際に確定したときだけ completed になります。

何が検証済みで何が未検証か(v0.1 リリース候補の現状)は README にまとめています。このページの数字はすべて自分で計測したもので、書いてあるコマンドで再現できます。

まず、AI同士に電話させてみてください。

github.com/FORIFOR/oathra Star