TL;DR
- 文章を生成しない判断特化AI Jev に、架空の営業電話ログ60本(正解ラベル付き)を判定させた
- 「通話の結果はどれか(5択)」は、意地悪な通話24本を含めて 60/60 正解。5質問まとめて 中央値250ms、60本で 約0.6円
- Noul(真偽の確率)は 0.7以上と答えたものが全部 true、Brier score 0.012。確率がちゃんと当てになる
- ただし 「営業は6回以上しゃべったか」は 37/60 でコイン投げ並み。数えるのは本当にできない
- 結論: 「読めばわかる判断」はJev、「数えればわかること」はコード。この線引きがすべて
やろうとしたこと
Qiitaの公式イベントで判断特化AI「Jev」を知り、TypeSafe AI の waitlist に登録したら 3時間ほどでAPIキーが届きました。
が、正直に言うと、キーが来た時点で 何に使えるのかよくわかっていませんでした。迷路を解かせる? シフト表の最適化? ゲームのデバッグ? いろいろ考えたんですが、ドキュメントを読んで全部ボツに。Jevはこういうものなんですね。
- 渡すのは
state(判断したい対象のテキスト)と、型つきの質問 - 返ってくるのは3種類だけ。Choice(選択肢から1つ)、Score(段階評価)、Noul(真偽の確率 0〜1)
- 文章は一切生成しない。計算もしない。公式が「電卓ではない」と明言している
考えて解くAIではなく、読んで即答する「直感」の部品。探索や計算はコードの仕事。
となると向いているのは「同じ形の判断を大量にさばく」仕事で、思いついたのが 営業電話ログの仕分け でした。入口で断られたのか、担当不在だったのか、金額で断られたのか、アポは取れたのか。人が1本ずつ聞き直すのはつらいやつです。
構成
本物の通話ログは使えないので、完全フィクション で作りました。
- 架電元: 株式会社開運堂本舗
- 商材: 金運があがる置物 「黄金の福ガエル」(純金メッキ・高さ30cm・1体29万8千円)
- ゴール: 社長との対面アポ
通話ログは Claude に仕様書を渡して 6カテゴリ×10本=60本 生成させ、正解ラベルも同時に付けさせています。各カテゴリ4本は hard(字面だけ追うと間違えそうな通話) です。
| カテゴリ | 例 |
|---|---|
| 門前払い | 「今忙しい」「間に合ってます」「営業はすべてお断り」 |
| 担当不在 | 外出・会議・居留守 |
| 金額で断り | 「その値段じゃ無理だなあ」 |
| 金額以外で断り | 怪しい、招き猫がもうある、税理士に反対された |
| 一度断ってからアポ | 「5分だけだぞ。買わないからな」 |
| すんなりアポ | 「最近ツイてなくてさ、見てみたい」 |
hard の例はこんな感じです。
- 「社長は今ちょっと忙しくて…」→ 実は 会社方針で営業電話は一切お断り(不在ではない)
- 受話器の向こうから「いないって言っといて」と聞こえる 居留守
- ずっと「ぼったくりじゃないの」と文句を言いながら「あさって3時に来てよ」と 約束する 社長
1通話に対して、次の5質問を 1リクエストにまとめて 投げます。公式の設計指針が「1質問=1つの狭い判断に分解せよ」なので、それに従いました。
from typesafe_sdk import Choice, Noul, Score
QUESTIONS = {
"outcome": Choice(
instructions="この営業電話は最終的にどういう結果で終わったか。",
criteria={
"gatekeeper_reject": "商品説明をほぼ聞いてもらえないまま、電話口で門前払いされた(...)",
"absent": "社長や担当者が不在であることを理由に通話が終わった",
"price_reject": "商品説明を聞いた上で、金額の高さを主な理由に断られた",
"other_reject": "商品説明を聞いた上で、金額以外の理由で断られた",
"appointment": "対面で訪問する約束(アポ)が取れた。途中で断られかけていても...",
},
),
"appointment": Noul(instructions="営業担当は、相手先を訪問する具体的な約束を取り付けたか。"),
"initial_refusal": Noul(instructions="相手は通話中に一度でも、断る・電話を切ろうとする意思を示したか。..."),
"reached_decision_maker": Noul(instructions="営業担当は、決裁者本人と直接会話できたか。"),
"recall_priority": Score(
instructions="この相手にもう一度営業電話をかける優先度はどのくらいか。",
criteria=["再架電してはいけない...", "望み薄...", "再架電の価値がある...", "アポ獲得済み..."],
),
}
result = await client.system_one(transcript, QUESTIONS, model="jev-latest")
環境は uv add typesafe-sdk python-dotenv だけ。指示文も選択肢の説明も 全部日本語 で書いています。
やってみた結果
正答率
| 質問 | 型 | 全体 | easy | hard |
|---|---|---|---|---|
| 通話の結果(5択) | Choice | 60/60 | 36/36 | 24/24 |
| アポが取れたか | Noul | 60/60 | 36/36 | 24/24 |
| 一度でも断られたか | Noul | 58/60 | 36/36 | 22/24 |
| 決裁者と話せたか | Noul | 60/60 | 36/36 | 24/24 |
| 再架電の優先度(0〜3) | Score | 55/60 | 33/36 | 22/24 |
正直、もっと間違えると思っていました。 居留守も、文句を言いながらのアポも、「忙しい」が口実の門前払いも、全部正しく仕分けています。
たとえばこの不機嫌な運送会社の社長。
社長: カエルだあ? くだらねえ。そんなもんで事故が減りゃ苦労しねえよ。
(中略)
社長: ちっ……。水曜の朝8時半。トラック出し終わったあとなら事務所にいる。5分だけだぞ。買わないからな。
"outcome": { "choice": "appointment", "confidence": 1.0 },
"appointment": { "noul": 0.98 },
"initial_refusal": { "noul": 0.83 },
"recall_priority": { "score": 2.99, "confidence": 0.99 }
「買わないからな」と言われてもアポはアポ。パースも正規表現もなしで、この形のJSONがそのまま返ってきます。
60本ぶんの通話ログ・正解ラベル・Jevの回答は、1通話ずつ並べて見られるようにしてあります。「誤答のある通話だけ」で絞ると、どこで間違えたかがすぐわかります。
速度とコスト
| 項目 | 実測 |
|---|---|
| レイテンシ(5質問/リクエスト) | 中央値 250ms、p95 511ms |
| 60本の処理時間(4並列) | 4.2秒 |
| 入力トークン | 平均1,517/通話、合計91,001 |
| 料金($0.042/Mtok、出力無料) | 60本で約 $0.004(0.6円)。1万通話でも100円くらい |
確率は当てになるのか
Noul の3問ぶん(180判断)をまとめて、「Jevが言った確率」と「実際に true だった割合」を並べました。
| Noul の値 | 件数 | 実際に true だった割合 |
|---|---|---|
| 0.0–0.1 | 75 | 0.00 |
| 0.1–0.3 | 6 | 0.17 |
| 0.3–0.5 | 2 | 0.00 |
| 0.5–0.7 | 3 | 0.67 |
| 0.7–0.9 | 8 | 1.00 |
| 0.9–1.0 | 86 | 1.00 |
Brier score は 0.012。161/180 が両端に寄っていて、間違えた2件はどちらも 0.26 と 0.68 という「自信なさげ」な値 でした。つまり「0.2〜0.8 のものだけ人が見る」というルールにすれば、180判断のうち8件をチェックするだけ で誤判定を全部拾えます。confidence でルーティングせよ、という公式の推しパターンはちゃんと機能しそうです。
間違えたところ
「今週は忙しい」を断りと読んだ
院長: うーん、今週は忙しいんですよ。学会の準備もあって、ほんとに時間が取れなくて。
院長: だから来週にしてください。来週の木曜は午後休診なんで、その日なら。
これは日程調整であって断りではないんですが、「一度でも断られたか」に 0.68 と回答。公式の弱点リストにある 「字義通りに読む。含みは読まない」 がそのまま出ました。
正解ラベルのほうが怪しかった
もう1件の誤答は「ぼったくりじゃないの」と文句を言い続けてアポに応じた居酒屋の社長。正解ラベルは「断られた=true」、Jevは 0.26。読み返すと 社長は一度も断っていない んですよね。ラベルを付けた側(Claude)のミスで、Jevのほうが正しい気がします。後出しでラベルは直していないので、数字は 58/60 のままにしてあります。
Score は悲観的に寄る
再架電優先度の誤答5件のうち 4件は「1: 望み薄」を「0: 再架電NG」と判定 したものでした。「霊感商法みたいで怪しい」「信仰と合わないので」とはっきり断られた通話を、「二度とかけるな」と同格に読んでいます。これはルーブリックの書き方で直せる余地がありそうです。なお confidence 0.7 以上に絞ると 49/50 正解でした。
わざと苦手なことを聞いてみた
結果が良すぎて不安になったので、公式が苦手と明言している カウント と 時刻の比較 を同じログで聞いてみました。どちらも正解はコードや目視で機械的に決まります。
| 質問 | 結果 | 全部同じ答えにした場合 |
|---|---|---|
| 営業担当の発話は6回以上あるか | 37/60(Brier 0.275) | 36/60 |
| 約束した訪問時刻は14時より遅いか | 20/20(Brier 0.001) | 13/20 |
カウントは完全に崩壊 しました。Brier 0.275 は「常に0.5と答える」(0.25) より悪く、6〜7回の通話をほぼ全部「6回未満」と答えています。transcript.count("営業:") の1行で済む話なので、これは聞くほうが悪い。
一方で時刻比較は、「3時に本店に来てよ」「朝8時半」「午後2時」と表記がバラバラなのに 全問正解。弱点リストに載っていても、この程度なら普通にいけました。苦手の出方には濃淡があるので、自分のデータで試すのが一番 ですね。
学んだこと
-
Jevに渡すのは「読めばわかる判断」だけにする。 数える・計算する・並べ替えるはコードでやる。今回なら、Jevが出した
recall_priorityとappointmentを使って架電リストをソートするのはPythonの仕事 - 質問は分解するほど強い。 「この通話を分析して」ではなく、5つの狭い質問にしたから検証もできたし、どこで間違えたかも特定できた
- 確率つきで返ってくるのが想像以上に便利。 LLMに「自信度も出して」と頼むのとは違って、0.7以上は実際に全部当たっていた。人に回す線を数字で引ける
- 日本語の指示文でそのまま動く。 英語に直す必要は今回感じなかった
- ⚠️ ただし今回のデータは LLMが書いた「きれいな」通話ログ です。実際の音声認識テキストは言い間違いや誤変換だらけのはずで、公式も「ノイズが多いと精度が落ちる」と書いています。100%という数字は割り引いて読んでください
おわりに
「何に使えるのかわからない」から始まりましたが、触ってみての感想はこれです。
LLMの代わりではなく、
if文の条件式に「文章を読む力」を足す部品。
250ms・1通話0.01円で確率つきの判断が返ってくるなら、「全件LLMに読ませるのは高いし遅いから諦めていた仕分け」が現実的になります。逆に、迷路や最適化みたいな「考える・計算する」仕事を渡すと、発話カウントの実験のようにあっさり崩れます。
検証コードと通話ログ60本(福ガエルの営業トーク入り)は GitHub に置いてあります。APIキーがあれば uv run だけで再現できます。
「うちのログではこうだった」「こういう質問は崩れた」などあれば、ぜひコメントで教えてください。