本記事は AI(Claude)が生成した文章をもとにしています。
内容は人間が確認・校閲しています。
TL;DR
- Soniox(STT)→ GPT → TTS の電話応答パイプラインに、TypeSafe AI の Jev(テキストを生成せず typed な確率だけ返す "System One" モデル)を挟んだ
- Jev には STT の途中結果が更新されるたびに「用件は? 何をしたい? 言い終えた?」の 3 問を投げ、その確率で 予約 DB の先読み と GPT へのプロンプト切替 を話し終わる前に済ませる
- 実測(Soniox stt-rt-v5 / gpt-4.1-mini / Cartesia): 話し終わり → 音声先頭バイトが 2583ms → 1296ms(予約)、2570ms → 1852ms(キャンセル)
- 懸念だった「明日の 15 時の予約を…」を新規予約と誤判定する問題は、選択肢に
undeterminedを置いて指示すれば Jev は末尾まで undetermined(0.99)を維持し、「キャンセルしたいです」でcancel 0.98に切り替わった - Jev の実測レイテンシは 200〜550ms で、公称 0.1s より重い。Jev 単体で速いというより「GPT の仕事を前倒しする/tool call の往復を消す」ことで効いている
背景:なぜ電話応答は遅いのか
電話の AI 応答で体感を決めるのは「相手が話し終えてから返事が返ってくるまで」です。素直に組むとこうなります。
<end> が来てから GPT を呼ぶので、GPT の TTFT(650〜900ms)× 2 往復 + DB + TTS が全部直列に乗ります。実測で 2.5 秒前後でした。
Jev とは(この記事に必要な範囲で)
-
POST /v1/systemoneにstate(文字列 or JSON)とquestionsを渡す - 質問は 3 種類だけ
-
noul: yes/no → 0〜1 の確率 -
choice: 選択肢から 1 つ → 各選択肢の確率 + confidence -
score: 段階評価
-
- 文章は一切生成しない。だから速く(公称 0.1s)、選択肢の外の答えは返らない
- 状態を持たない。会話の文脈は毎回
stateに入れて渡す
「AI 版の if 文」と呼ばれているのが一番近い説明です。
設計:どのタイミングで Jev に投げているか
用語を先に。ドラフト=相手がまだ話している最中に、Jev の判定(意図+先読みデータ)を前提にして GPT に先に作らせておく返答文の下書きのこと。<end> が来たら「その前提が最後まで崩れていないか」を Jev で確認してから使います。崩れていれば捨てて作り直します。
投げるタイミング
Soniox は WebSocket で is_final: false のトークンを逐次返します。文字列が前回と変わるたびに Jev を呼びます(連打防止に最小間隔 150ms、実行中なら最新の 1 件だけ保留)。1 発話で 5〜8 回呼びました。
Jev に渡す state は途中テキストそのままです。「文は途中で途切れていることがある」と一言添えます。
{
"state": { "transcript_so_far": "明日の15時の予約を", "note": "通話中の文字起こし。文は途中で途切れていることがある。" },
"model": "jev-latest",
"questions": {
"topic": { "type": "choice", "instructions": "この電話の用件の大分類は?", "criteria": { "reservation": "予約に関する話(新規、空き確認、変更、キャンセルを含む)", "chat": "雑談・挨拶だけ", "other": "営業時間・場所・料金など" } },
"action": { "type": "choice", "instructions": "話者が求めている操作は?「予約」「○時」「明日」といった語があるだけでは新規予約と決めつけないこと。意思を示す語が出るまでは undetermined を選ぶ。", "criteria": { "new_booking": "新しく予約を入れたいと明言", "check_availability": "空いている日時を知りたい", "cancel": "既存予約を取り消したい", "change": "既存予約の日時を変えたい", "undetermined": "まだ意思を示す語が出ていない", "none": "予約と無関係" } },
"complete": { "type": "noul", "instructions": "話者は用件を言い終えており、この内容だけで受付として返答を作れるか?" }
}
}
返却値の何を見て GPT に投げているか
返ってくるのは確率だけです。
{ "answers": {
"topic": { "type": "choice", "choice": "reservation", "probabilities": { "reservation": 0.99, "chat": 0.0, "other": 0.01 }, "confidence": 0.98 },
"action": { "type": "choice", "choice": "undetermined", "probabilities": { "undetermined": 0.99, "cancel": 0.01, ... }, "confidence": 0.99 },
"complete": { "type": "noul", "noul": 0.06 }
} }
これを 3 段のゲートに使っています。ポイントは「先読み」と「ドラフト生成」で条件を分けていることです。
| 発火するもの | 条件 | 誤判定したときの被害 |
|---|---|---|
| 予約 DB 先読み | topic.probabilities.reservation ≥ 0.6 |
なし(read-only。新規でもキャンセルでも同じデータを取る) |
| GPT ドラフト生成 |
action.choice ≠ undetermined かつ action.confidence ≥ 0.7 かつ complete ≥ 0.7
|
間違った前提の返答文ができる → 差分判定で捨てて再生成(時間のロスのみ) |
<end> 後の採用判定 |
差分判定 ok ≥ 0.6
|
変な返答をしてしまう(ここが一番厳しく見るべき所) |
被害の小さいものほど緩い条件で早く発火させ、被害の大きいものほど厳しくする、という並べ方です。
差分判定は state を JSON にして「確定した発話」「ドラフト時に仮定した action」「ドラフト文」を並べ、noul 1 問で聞きます。
{ "state": { "final_utterance": "明日の15時の予約をキャンセルしたいんですが", "assumed_action": "new_booking", "draft": "明日15時で承りました。" },
"questions": { "ok": { "type": "noul", "instructions": "`draft` は `final_utterance` への返答として適切か?特に `assumed_action` が発話の意図と一致しているか。" } } }
実測
Windows + Chrome、マイクから Soniox に直結。A(Jev なし)と B(Jev あり)に同じ音声を同時に流しています。
| 発話 | A: EOS→音声 | B: EOS→音声 | 短縮 |
|---|---|---|---|
| 「では、来週の火曜日14:00に予約をしたいです。」 | 2583ms | 1296ms | −50% |
| 「来週の火曜日の14:00の予約なんですけれども、こちらはキャンセルしたいです。」 | 2570ms | 1852ms | −28% |
| 「おはようございます。」(雑談) | 2065ms | 2064ms | 0% |
内訳(A): <end> → GPT#1 TTFT 651ms → tool call → DB 318ms → GPT#2 TTFT 621ms → 1 文目 → TTS 先頭 273ms。
内訳(B): 先読みは発話中に完了済み → <end> → Jev 最終判定 225ms → GPT(データ同梱、tool なし)TTFT 458ms → TTS 272ms。
雑談は tool call がないので差が出ません。ドラフト先行生成は今回の発話では 一度も発火しませんでした(後述)。
誤判定チェック:「明日の 15 時の予約を」で新規予約に振れないか
これが一番の懸念でした。途中結果ごとの Jev の返答(キャンセル発話):
| 途中テキスト | action | conf | complete |
|---|---|---|---|
| 来 | undetermined | 0.95 | 0.02 |
| 来週の火 | undetermined | 0.99 | 0.06 |
| 来週の火曜日の | undetermined | 1.00 | 0.03 |
| 来週の火曜日の14:00の予約なんですけれども | undetermined | 0.99 | 0.27 |
| 来週の火曜日の14:00の予約なんですけれども、こちらは | undetermined | 0.83 | 0.05 |
| (確定)…こちらはキャンセルしたいです。 | cancel | 0.98 | 0.35 |
criteria に undetermined を置き、instructions で「語があるだけで決めつけるな」と書いただけで、末尾まで保留してくれました。新規予約の発話でも …14:00に予約をした まで undetermined、予約をしたいです で new_booking 0.93 です。
一方で complete は言い終えた後でも 0.35〜0.67 と低く、ドラフト生成のゲート(≥0.7)を通りませんでした。「言い終えたか」は Jev には難しい問いのようで、ここは Soniox の <end> に任せ、Jev には意図判定だけを頼むのが正しそうです。
Jev の速さについて
公称 0.1s に対し、実測は 205〜556ms(日本からの呼び出し、途中テキスト 10〜40 文字、3 問同時)。GPT の TTFT(450〜900ms)より速いのは確かですが「2 桁速い」わけではありません。効いているのは Jev 自体の速さより、話している 3〜5 秒の間に DB 先読みを終わらせておけることです。
Q. 「明日の予約を」で止まったら「ご予約でしょうか、キャンセルでしょうか」と聞き返せるか
できます。しかも Jev が一番向いているパターンです。
Soniox の <end> が来た時点で最終判定が action=undetermined(高 confidence)なら、GPT を呼ばずにあらかじめ合成しておいた定型文の音声を再生します。GPT も TTS も通らないので、<end> + Jev 1 回(〜250ms)で返せます。
注意点が 2 つ。
- Soniox のセマンティック終端検出は「言いかけ」では
<end>を出しにくいので、そもそも「明日の予約を」で止まるケースはmax_endpoint_delay_ms(既定 2000ms)の上限で切られたときが中心になる。つまり聞き返しが返るのは相手が 1.5〜2 秒黙った後で、体感としては自然。 -
<end>の直後に相手が続きを話し始める(「…をキャンセルで」)場合がある。聞き返し音声の再生中にマイク側で音声を検出したら再生を止める(バージイン)を入れておくべき。今回のベンチには未実装。
Q. Jev は前後を知らないので、次の発話「予約したい」がいつの予約かは GPT 頼りになるか
半分正解です。
Jev が状態を持たないのはその通りですが、state は JSON で渡せるので、直前のやり取りを毎回 state に含めれば文脈は使えます。
{ "state": {
"history": [ { "role": "user", "text": "明日の予約を" }, { "role": "assistant", "text": "ご予約でしょうか、キャンセルでしょうか" } ],
"current": "予約したい" } }
これで action は new_booking に振れます。ただし 「いつの予約か」を文字列として取り出すのは Jev にはできません。Jev は生成しないので、自由記述の値(日付、時刻、名前)の抽出は GPT(または正規表現などのルール)の仕事です。
ただし「閉じた集合から選ぶ」なら Jev でできます。キャンセルや変更で「どの予約のことか」は、既存予約の一覧を criteria に並べた choice にすれば Jev が選べます。
{ "state": { "history": [...], "current": "明日の15時のやつです" },
"questions": { "target": { "type": "choice", "instructions": "話者が指している予約はどれか", "criteria": { "B-1001": "明日 15:00 カット", "B-1002": "明日 11:00 カラー", "none": "どれでもない / 特定できない" } } } }
整理すると:
| やりたいこと | 担当 |
|---|---|
| 用件の分類(新規 / 確認 / キャンセル / 変更 / 雑談) | Jev |
| 言い終えたか | Soniox の <end>(Jev の complete は当てにならなかった) |
| 既存予約のうちどれを指しているか | Jev(closed set の choice) |
| 新規予約の日時・名前など自由記述の抽出 | GPT |
| 返答文の生成 | GPT |
| 定型の聞き返し | Jev 判定 + 事前合成音声(GPT 不要) |
まとめ
- Jev は「返答を作る」モデルではなく「今どの分岐にいるか」を返すモデル。電話応答では STT の途中結果を流し込む判定器として置くと、話している間に先読みとプロンプト切替ができる
- 効果は tool call のある用件(予約・キャンセル)で大きく、雑談では出ない
- 誤判定対策は「
undeterminedを選択肢に入れる」「被害の小さい処理ほど緩い閾値で先に発火させる」「<end>後に差分判定を挟む」の 3 点 - Jev の弱点: 自由記述の抽出はできない、「言い終えたか」は苦手、実測レイテンシは公称より重い