Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions docs/launch/real-call-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# 実電話の検証計画(2026-10-19 作成)

目的: 企業向けに「実電話 N 件、誤完了 0、成功率 XX%、p95 応答 X.X 秒、¥XX/通話」と、測った数字だけで言えるようにする。

方針: **電話なしで確かめられることは電話なしで全部済ませ、実電話は電話でしか確かめられないことに絞る。** 実電話は「直す作業」ではなく「数字を取る作業」にする。

前回の計画([READINESS.md](../READINESS.md)「実電話 100 件の計画」、2026-09-13)は、100 件すべてを実電話で行う前提だった。今回はその区分を 3 つの層に振り分け直した。

## 現状(根拠)

| 項目 | 状態 | 根拠 |
|--|--|--|
| 実電話の記録 | Twilio/GPT-Live で十数件。会話・同意・相手情報の取得まで成功した件は集計なし | [real-calls.md](real-calls.md) |
| 無音バグの修正(9/15) | 修正後の再発信は 2 件とも留守電で、未評価 | [real-calls.md](real-calls.md) |
| 取消の競合で `runtime_error`(9/20) | 修正済み。本物の OpenAI API では再現と修正を確認。実回線での再発信は BLOCKED | [realtime-recovery.md](../quality/realtime-recovery.md) |
| UI からの実発信(9/20) | 32 秒、7 発話で完了。ただし Twilio の price が null で、精算は BLOCKED | [user-ui-call.md](../quality/user-ui-call.md) |
| シミュレーター | 敵対的 1 万件で誤完了 0(14 種) | [ENTERPRISE_READINESS.md](../ENTERPRISE_READINESS.md) |
| Plivo / SIP / LiveKit / Gemini Live / 英語 | 実回線は未検証 | README |

## 3 つの層

| 層 | 何を使うか | 費用 | 依頼者の go |
|--|--|--|--|
| A. 判定とガードレール | シミュレーター、ゲートウェイのテスト | ¥0 | 不要 |
| B. 音声の通し試験 | `phone test --level conversation`(電話会社なし、本物の音声 API) | 1 回 $0.05〜0.2 程度 | 不要(API 代のみ) |
| C. 実電話 | ゲートウェイのアプリから Twilio 直結で発信 | 1 件 3 分 ≈ ¥110 | **必要** |

### 前回の 100 件の区分は、どの層で確かめるか

| 区分 | A 判定 | B 音声 | C 実電話 | 実電話が必要な理由 |
|--|--|--|--|--|
| 成功 / 代案から成功 | 済 | ○ | ○ | 人の声での聞き取りと料金 |
| 部分成功 / 満席 / 仮押さえ / 拒否 | 済 | ○ | 少数 | 判定は A と B で十分 |
| 聞き返し / 転送 / 保留 | 済 | ○ | 少数 | 実際の無音・保留音での挙動 |
| 途中切断 | 済 | ○ | ○ | 電話会社の切断通知の扱い |
| 留守電 | 済 | ― | ○ | 携帯会社ごとに応答文と鳴り方が違う |
| 騒音 / 早口 / 小声 | ― | ○(合成) | ○ | 合成音は人の声の代わりにならない |
| 方言 | 済 | ○(合成) | ○ | 同上 |
| 営業: 受ける / 曖昧 / 断る / 連絡しないで / 取次ぎ | 一部済 | ○ | ○ | 名乗り・目的告知が実際に聞こえるか |
| 再発信停止・時間帯・上限・二重発信 | 済 | ― | ― | 電話をかける前に止まるので不要 |

## 層 A — 判定とガードレール(¥0、すでにある)

新しい作業は、下のコマンドを検証の起点として再実行し、結果を記録することだけ。

- `pnpm oathra eval --adversarial 10000` → `False Completion: 0 / 10000`
- `pnpm test`(`packages/evidence/src/appointment-fuzz.test.ts` の営業アポ 1 万件を含む)
- `pnpm test:gateway` に、電話をかける前に止める仕組みのテストがある:
- 連絡停止の番号は承認できない。発信直前にも確認し直す(`apps/gateway/test/gateway.node.mjs:49`, `:82`)
- 二重発信の防止(`gateway.node.mjs:39-41`)
- 日次上限(`gateway.node.mjs:55`)
- 発信時間帯(`schedules.node.mjs:222` ほか。営業電話の既定 9〜20 時を直接確かめるテストは未確認。無ければ追加する)
- 営業電話は連絡停止の番号・許可外の相手を拒否する(`agent-sales-inbound.node.mjs:90`)

2026-10-19 時点: `pnpm test` 4310 件、`pnpm test:gateway` 637 件、すべて成功。

## 層 B — 音声の通し試験(電話会社なし)

### 今の `--level conversation` でできること / できないこと

`packages/cli/src/phone.ts:611` と `packages/cli/src/local-conversation.ts` を読んだ結果。

| 項目 | 今 | 根拠 |
|--|--|--|
| 本物の処理の流れ(依頼 → 契約 → ランタイム → 音声エンジン) | ○ | `phone.ts:617-628` |
| 電話と同じ音質(μ-law 8kHz) | ○ | `local-conversation.ts:88` |
| 相手が話している途中の割り込み 1 回 | ○ | `local-conversation.ts:21`, `:188-193` |
| 応答時間の計測(聞き取り・考える・話し始め) | ○ | `local-conversation.ts:195-199` |
| 依頼の種類 | **雑談の 1 種類だけ**(予約・営業アポは不可) | `phone.ts:617` |
| 相手の台本 | **固定の 5 行だけ**(CLI から変えられない) | `local-conversation.ts:17-23` |
| 騒音・早口・小声 | **なし** | ― |
| 完了判定(`evaluate()` の結果) | **出ない**(終わり方と応答時間のみ) | `local-conversation.ts:205` |
| 複数回の実行と集計 | **なし**(1 回ずつ) | ― |

### B を使うために足す機能(実装が必要)

1. **依頼を選べるようにする**: シナリオ(`restaurant-reservation`、営業アポ)を指定でき、その契約で `evaluate()` の判定を report に出す。
2. **相手の台本をファイルで渡す**: 区分ごとの台本(上の表の区分)を `scenarios/` か `tests/` に置く。
3. **音を悪くする**: 相手の音声に、騒音(テレビ・水音・街中)を指定の音量差で混ぜる。話す速さ(早口)と音量(小声)を変える。μ-law に落とす前に処理する。
4. **まとめて回す**: 区分 × 回数を一度に実行し、誤完了・期待どおりの判定・応答時間(p50/p95)・費用を集計する。1 回ずつと合計の費用上限を守る。

方言は合成音声では再現が粗い。B では言い回しだけ確かめ、発音は C で確かめる。

### B の合格条件

- 予約・営業アポの各区分を 5 回ずつ(20 区分前後 × 5 = 約 100 回)。費用の目安は合計 $20 以内(実行前に試算し直す)。
- 誤完了 0。
- 期待どおりの判定 95% 以上。外れた場合は区分ごとに原因を記録する(聞き取り / 判定 / 会話の進め方)。
- `runtime_error` 0。割り込みの後に古い応答の音声が流れない。
- 騒音・早口・小声で、日時・人数・名前の誤認識の件数を記録する(ここは目標を置かず、実電話との比較に使う)。

## 層 C — 実電話(要 go)

B が合格してから行う。CLAUDE.md の順で、`--level local` → `--level gateway` → PSTN(実際の電話網)。

### 原則

- 実在の店舗や企業にはかけない。**相手役は人間の協力者が自分の番号で受ける**。協力者には事前に、記録と目的を説明して同意を取る。
- 失敗・つながらなかった・留守電も、すべて分母に残す。過去の記録は分母に足さない。
- 音声と会話本文は公開しない。公開するのは集計表と、協力者が許可した書き起こしだけ。
- シミュレーターと層 B の結果を、実電話の件数・遅延・料金として載せない。

### C0 — 準備(¥0)

- **Twilio の料金データを取れるようにする。** 9/20 の通話の price を Twilio API で今取り直し、後から値が入るのか確かめる。入らない場合は、請求明細から集計する手順に切り替える。これが決まらないと、1 通話あたりの料金と精算を測れない。
- `pnpm oathra phone doctor --to <協力者の番号>`、`phone test --level local`、`--level gateway` がすべて成功すること。

### C1 — 疎通確認 10 件(約 ¥1,100)

ゲートウェイのアプリの画面から、協力者 1 人に 10 件。

| 確認すること | 合格条件 |
|--|--|
| 声が届くか(9/15 の無音バグ) | 10/10 で AI の声が相手に聞こえる |
| 冒頭の告知 | 10/10 で「この通話は記録されています。」が最初に流れる |
| 割り込み | 相手が割り込んでも `runtime_error` 0 |
| 完了判定 | 未確定の 4 件がすべて `incomplete` |
| 精算 | 10/10 で料金が確定し、確保していたクレジットが解放される |

1 つでも不合格なら止め、原因を `oathra replay` で特定して直す。直した点は層 B で再確認してから C1 をやり直す。

### C2 — 電話でしか測れないもの 30 件(約 ¥3,300)

| 区分 | 件数 | 相手役がやること |
|--|--:|--|
| 予約の成功(代案からの成功を含む) | 8 | 普通に受ける |
| 予約の未確定(満席・仮押さえ) | 4 | 断る / 「一応お取りしておきます」 |
| 留守電 | 4 | 実際の留守電につなぐ(携帯会社を 2 社以上) |
| 騒音・早口・小声 | 6 | テレビ音の中で / 早口 / 小声 |
| 方言 | 3 | 関西・東北など |
| 途中切断 | 2 | 会話の途中で切る |
| 営業(受ける / 断る / 連絡しないで) | 3 | 名乗り・目的告知が聞き取れるか相手役が記録 |

30 件で誤完了 0 なら、言えるのは「誤完了率は約 10% 以下(95% 信頼区間の上限)」まで。応答時間、誤認識、料金の実測値はここで出る。

### C3 — 企業向けの数字を作る 100 件(任意、約 ¥11,000)

「実電話 100 件で誤完了 0(約 3% 以下)」と言いたいときだけ行う。C2 の 30 件はこの 100 件には数えず、別に集計する。区分の配分は、C2 で問題が出た区分を厚くして決める。

### 1 件ごとに記録すること

記録先は `docs/launch/real-calls.md`(番号・音声・本文は載せない)。

試行 ID / 日時 / 層と段階(C1〜C3) / 区分 / 相手役 / commit / キャリア・音声エンジン / Twilio の最終状態 / 通話秒数 / 最終判定 / 期待どおりか / 誤完了 / `runtime_error` / 最初の応答までの時間 / 日時・人数・名前の誤認識 / 人が後で直した点 / Twilio 料金 / モデル料金 / 備考

### KPI(C2・C3)

| KPI | 定義 | 目標 |
|--|--|--|
| 誤完了 | 相手が確定していないのに `completed` になった件数 | **0**(1 件出たら即停止) |
| タスク成功率 | 期待どおりの結果になった割合 | 90% 以上 |
| 接続後の正常終了率 | 意図しない切断・`runtime_error` がない割合 | 95% 以上 |
| 人による修正率 | 人が後で結果を直す必要があった割合 | 5% 以下 |
| 最初の応答時間 | 相手が話し終えてから AI が話し始めるまで | p50 / p95 を実測し、層 B と比べる |
| 誤認識 | 日時・人数・名前の誤り | 件数を実測し、層 B と比べる |
| 料金 | Twilio + モデル(請求の実額) | 1 通話あたりの実測値 |

### 中止する条件

- 誤完了が 1 件 → 即停止。原因を直し、層 B で再確認してから、その区分をやり直す。
- `runtime_error` か無音が 2 件続く → 停止して原因を調べる。
- 実費が予算上限の 80% に達する → 停止して依頼者に報告する。
- 協力者が中止を求めたとき。

## 日程(目安)

| 段階 | 内容 | 人手 |
|--|--|--|
| 1 | 層 A を再実行して記録。営業の時間帯テストが無ければ追加 | 実装者のみ |
| 2〜4 | 層 B の機能を足し(上の 4 点)、約 100 回を回して直す | 実装者のみ |
| 5 | C0・C1(10 件) | 協力者 1 人、約 40 分 |
| 6〜7 | C2(30 件) | 協力者 2〜3 人、各 1 時間 |
| 任意 | C3(100 件) | 協力者 2〜3 人、4〜5 日 |
| 最後 | 集計し、README・サイト・READINESS.md を更新(未達は未達のまま載せる) | 実装者のみ |

## 決めてほしいこと

1. 層 B の機能追加を進めてよいか(API 代は合計 $20 程度まで)。
2. 実電話に進む go(C1、C2、C3 で別々に出す)。
3. 実電話の予算上限(案: C1 + C2 で ¥5,000、C3 を行うなら追加で ¥12,000)。
4. 相手役の協力者 2〜3 人と、その番号。方言を話せる人、携帯会社が違う人がいるか。
5. 結果をどこまで公開するか(集計のみ / 書き起こしの一部も)。

## この計画で検証しないこと

Plivo / SIP / LiveKit / Gemini Live、英語、同時に複数かける場合の負荷、実在の店舗・企業への発信、障害時の切り替え。これらは別の計画で扱う。
58 changes: 58 additions & 0 deletions docs/quality/conversation-batch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# 層 B: 音声の通し試験(電話会社なし)の実装と初回試験(2026-10-20)

計画: [real-call-plan.md](../launch/real-call-plan.md) の層 B。実電話ではない。相手の声は合成音声(OpenAI `tts-1`)で、回線はローカルの μ-law 8kHz ループ。

## 実装

| 変更 | ファイル |
|--|--|
| 相手役に台本キャラクター(シミュレーターの店員、敵対的な変異つき)を使えるようにした。AI の発話の文字起こしを聞いて返事する。通話後に `evaluate()` の判定、キャラクターが実際に約束した内容(`truth`)、誤完了かどうかを report に出す | `packages/cli/src/local-conversation.ts` |
| 相手側の音を悪くする: 小声(−18 dB)、騒音(指定の SNR、ピンクノイズまたは WAV)、早口(TTS の速度 1.5) | `packages/cli/src/audio-degrade.ts` |
| 変異 × 条件 × 回数をまとめて回し、誤完了・期待どおりの率・応答時間 p50/p95・費用を集計する。合計の費用上限を超える前に止まる | `packages/cli/src/conversation-batch.ts`, `packages/cli/src/phone.ts`, `packages/cli/src/commands.ts` |
| `OpenAITTS` に `speed` を追加(任意。未指定なら従来どおり) | `providers/openai/src/tts.ts` |
| 台本キャラクターが「お忙しいところ失礼いたします」を別れの挨拶と誤認して電話を切っていたのを修正 | `providers/simulator/src/character.ts` |

使い方:

```sh
pnpm oathra phone test --level conversation --engine gpt-live --scenario restaurant-reservation \
--mutation none,tentative-hold,ask-back --condition clean,quiet,fast,noise-10db \
--runs 5 --max-usd 0.3 --max-usd-total 20
```

結果は `--out`(既定は `.oathra/local-conversations/batch-*`)に、1 回ごとの `report.json` と `call-mixed-8k.wav`、全体の `summary.json` として保存される。誤完了があるか、上限で途中停止すると exit 1。

## 検証

| 判定 | 内容 | 結果 |
|--|--|--|
| PASS | `pnpm build` | 成功 |
| PASS | `pnpm test` | 73 ファイル、4,321 件成功 / 1 skip(追加 11 件: 音の加工 5、集計 3、キャラクター相手の通し 2、別れの挨拶 1) |
| PASS | `pnpm test:gateway` | 637 件成功 |
| PASS | `pnpm lint:deps` | dependency direction ok |
| PASS | `pnpm oathra eval` | False Completion 0(7/8 完了。未完了は意図した `false-completion-trap`) |
| PASS | `pnpm oathra eval --adversarial 10000` | False Completion: 0 / 10000 |
| 観測 | 有料の試験 2 回(gpt-live、予約、通常と騒音 10 dB)。合計の推定 $0.078 | 下記 |

## 初回試験の所見(2 回のみ。率としては扱わない)

| 回 | 条件 | 相手が実際に言ったこと | AI 側の音声認識 | 判定 |
|--|--|--|--|--|
| 1 | clean | 「…田中様でご予約**承りました**」 | 「…田中様でご予約**のおりました**」 | incomplete(誤完了ではない) |
| 2 | noise-10db | 「…田中様でご予約**承りました**」 | 2 つの発話がつながり「…ご予約**でよろしいでしょうか?**」 | incomplete(誤完了ではない) |

- 会話の流れ(日時・人数・名前の提示、満席からの代案、名前の確認)は 2 回とも成立した。
- 2 回とも相手は予約を確定したのに、**音声認識が「承りました」を聞き違え、判定エンジンが完了にしなかった**。安全側の失敗(取りこぼし)で、誤完了ではない。
- 前の試行では、「9月12日**いち**」のように余計な語が入っただけで、日付・時刻・人数がすべて未確認になった(同じ文から「いち」を除くと completed になることを、判定エンジン単体で確認した)。
- 騒音 10 dB では、聞き取り確定までの時間が 7 秒を超えた発話があった。
- 応答時間(相手が話し終えてから AI の声が出るまで): p50 1,208 ms、p95 1,841 ms(n=13)。ローカル回線なので、実回線ではキャリアの遅延が加わる。

実電話(層 C)に進む前に、約 100 回の本番実行で「確定の言葉の聞き違い」による取りこぼしの率を測る。取りこぼしが多ければ、判定を緩めるのではなく、AI が確定内容を復唱して相手に確認させる進め方(聞き違いを会話で直す)を検討する。

## 制約

- 相手の声は合成音声で、人の声・電話回線の代わりにならない。
- 台本キャラクターは決まった言い回しで返す。人間の店員の多様さはない。
- 営業アポ(ゲートウェイの `SALES_CALL_POLICY` と `evaluateSales`)はまだ対象外。
- 試験中、OpenAI の `gpt-4o-mini-tts` が 20〜50 秒応答しない時間帯があった(`tts-1` は 1〜2 秒)。そのため相手役の声は `tts-1` に固定した。
- 証拠ファイルは `artifacts/quality/conversation-batch/smoke-20261020/`(git 管理外)。
Loading
Loading