はじめに
「なんであの人は売れるんだろう」。営業を見ていて一度はそう思ったこと、ないでしょうか。私はあります。そしてその答えはたいてい「人柄」とか「経験」で片付けられて、再現できないまま終わる。
この記事は、その「なんとなく売れる」を機械で評価できないか試した記録です。商談をZoomのScribe APIで文字起こしし、まずSQLで表層の指標を出し、そのうえで文脈の要る評価はClaude(エージェント)に丸ごと読ませて採点させました。結果として、SQLで測れる部分とエージェントでしか測れない部分が、きれいに線引きできました。
この記事で使う商談データは、私が台本を書いて音声合成した完全な架空データです。実在の商談ではありません。なので「トップ営業はこうだ」という事実を主張する記事ではなく、「この手法で、仕込んだトークの差をちゃんと検出できるか」という手法検証の記事です。数字はその文脈で読んでください。
何を作ったか
やったことはシンプルです。
- 架空の商談3本(トーク巧者2本/一方的に喋るダメ商談1本)の音声を合成
- Zoom Scribe で話者ごとに文字起こし
- PostgreSQL に発話を貯める
- SQLで「表層の指標」を出す(発話比率、被せ)
- Claudeエージェントに会話を丸ごと読ませて「質」を採点させる
最後のステップが本題です。完成イメージがこれ。
青がトーク巧者の商談、赤が一方的な商談。Claudeが文脈を踏まえて5観点で採点した結果です。仕込んだ差が素直に出ました。ここに至るまでを順に書いていきます。
設計:話者分離を使わずに「誰の発話か」を確定する
商談はセールスと顧客の2人が話します。分析には「どの発話が誰か」が要る。普通はScribeの話者分離(diarization)を使うところです。
最初に普通に diarization: true で叩きました。そうしたら、これ。
{
"code": 400,
"reason": "INVALID_INPUT",
"message": "Diarization is not supported for locale 'ja-JP'"
}
話者分離、日本語に対応していませんでした。記事の根幹が崩れるやつで、けっこう焦りました。
回避策はZoomの「参加者ごとに別の音声ファイルで録画」機能です。録画時点でトラックが分かれていれば、話者分離なんていらない。各トラックを別々にScribeへ通せば、ファイル名で誰の発話かが確定します。
各トラックは「その人が話す区間だけ音を入れ、相手が話す間は無音」にしておきます。こうすると2トラックが同じタイムラインに乗るので、時刻順に並べれば会話as-isに戻せる。被せも測れます。
検証用の商談をダミーで作る
実商談は使えないので、台本を書いてmacOSの say で音声合成しました。録音すらしていません。話者ごとに別ボイスを当てて喋らせます。
def say_to_wav(voice: str, text: str, out_wav: Path) -> float:
aiff = out_wav.with_suffix(".aiff")
run(["say", "-v", voice, "-o", str(aiff), text]) # 例: voice="Kyoko"
run(["ffmpeg", "-y", "-i", str(aiff), "-ar", "16000", "-ac", "1",
str(out_wav), "-loglevel", "error"])
return probe_duration(out_wav) # ffprobeで尺(秒)を返すヘルパ(実装は割愛)
発話の長さから「何秒地点から喋るか」を計算し、ffmpeg の adelay で配置します。台本が「——」で終わる行は、次の発話を食い込ませて被せを再現しました。作った商談はトーク巧者2本(別の顧客で同じ型を再現)と、機能を一方的に説明するダメ商談1本です。
Scribeで文字起こしする
Scribe Fast modeは音声を公開URLで渡す仕様です(base64やローカルアップロードは不可)。ここでもハマって、最初に使おうとした一時アップロードサービスが軒並み死んでいました。transfer.sh は閉鎖、0x0.st は「AIボットのスパムが多すぎて停止」とのこと。時代ですね。結局 catbox.moe が生きていました。
呼び出しはシンプルです。今回は単一話者トラックなので、diarization=False を渡して話者分離は明示的にオフにします(そもそもja-JPでは使えないので)。
def scribe_transcribe(file_url, language="ja-JP", diarization=True, timestamps=True):
return _post("/scribe/transcribe", {
"file": file_url,
"config": {"language": language, "diarization": diarization, "timestamps": timestamps},
})
# 呼び出し側(単一話者なので話者分離オフ)
resp = scribe_transcribe(url, language="ja-JP", diarization=False, timestamps=True)
返ってくるテキストは、今回の合成音声では「Excel」「3拠点」「法改正」あたりの主要語をちゃんと拾えていました。一方で「打刻」を取りこぼすなど誤認識もあって、実運用なら辞書や後処理での校正は要りそうです。
まずSQLで「表層」を出す。ただし限界がある
PostgreSQLに発話を貯めれば、表層の指標は安く出せます。たとえばヒアリング比率(顧客が話した割合)。
SELECT c.call_id, c.is_top,
round(100.0 * sum(s.end_sec - s.start_sec) FILTER (WHERE s.role='customer')
/ sum(s.end_sec - s.start_sec), 1) AS hearing_pct
FROM segments s JOIN calls c USING (call_id)
GROUP BY c.call_id, c.is_top;
被せ(割り込み)も、ウィンドウ関数の LAG で直前の発話の終了時刻を引けば出せます。
WITH ordered AS (
SELECT call_id, role, start_sec,
lag(end_sec) OVER (PARTITION BY call_id ORDER BY start_sec) AS prev_end,
lag(role) OVER (PARTITION BY call_id ORDER BY start_sec) AS prev_role
FROM segments
)
SELECT call_id,
count(*) FILTER (WHERE prev_role='customer' AND role='seller'
AND start_sec - prev_end < -0.3) AS interrupts
FROM ordered GROUP BY call_id;
巧者は被せ0回、ダメ商談は2回。表層はこれで十分つかめます。
ただ、ここで頭打ちになるんです。SQLは「質問を何回したか」は数えられても、「その質問が良い質問だったか」は判断できない。「なるほど」を何回言ったかは数えられても、それが的確な共感だったか、ただの口癖かは分からない。文脈が要る評価は、正規表現では届かないんですね。
本題:Claudeに会話を丸ごと読ませて採点させる
そこでClaudeの出番です。claude CLIのヘッドレスモード(-p)で叩けば、APIキーなしで(ログイン済みのClaude Codeの認証で)文脈評価を回せます。
会話全文を渡して、表層では測れない5観点でJSON採点させました。
PROMPT = """あなたは営業トレーニングのプロです。次の商談トークを読み、
セールス側のトークを評価してください。回数ではなく文脈で判断すること。
# 商談トーク
{transcript}
# 出力(JSONのみ。前後に文章やコードフェンスを付けない)
{{
"needs_uncovering": 1-5, // 顧客の本当の課題を引き出せたか
"question_quality": 1-5, // 質問が深掘り・オープンか
"proposal_fit": 1-5, // 提案が課題に紐づくか(機能羅列は低い)
"empathy_authenticity": 1-5, // 共感が的確か(形式的な相槌は低い)
"next_action_quality": 1-5,
"techniques": ["盗める型を最大3つ"],
"missed": "最大の取りこぼしを40字以内で"
}}"""
def call_claude(prompt: str) -> dict:
proc = subprocess.run(
["claude", "-p", prompt, "--output-format", "json", "--model", "sonnet"],
check=True, capture_output=True, text=True)
envelope = json.loads(proc.stdout) # claude CLI の外側JSON
raw = envelope["result"].strip() # モデルの返答本体
if raw.startswith("```"): # フェンスで囲んで返すことがあるので剥がす
raw = raw[raw.find("{"):raw.rfind("}") + 1]
return json.loads(raw)
プロンプトで「コードフェンスを付けるな」と指示しても、モデルが ```json で囲んで返してくることがあります。なので外側の json.loads でCLIのラッパーを剥がし、内側でフェンスを除去してからもう一度パースする、という二段構えにしています。ここを素朴に一発 json.loads すると、たまに落ちて泣きます(実際に泣きました)。
これを3商談ぶん回した結果が、冒頭のレーダーチャートです。スコアはダメ商談が1〜2、巧者が概ね4〜5(質問の質だけはA社が3点、トップ平均で3.5)で、きれいに割れました。
ただ、スコアより面白かったのはコメントの精度です。たとえばトーク巧者の商談A、これは私が「良い商談」として書いたものなんですが、Claudeの総評はこうでした。
{
"one_line": "課題発掘と次アクションは秀逸。予算・決裁者確認ゼロで商談化リスクあり",
"missed": "予算・決裁者・導入タイミングを全く確認せず進んだ点"
}
これ、唸りました。私はヒアリングと次アポを完璧に書いたつもりだったんですが、「予算と決裁者を聞いていない」という抜けを見抜かれた。SQLなら「質問4回・次アポあり=満点」で終わっていた商談です。文脈を読むエージェントだからこそ、「表面上は良いが、実は重要な確認が抜けている」を指摘できる。ここがSQLとの決定的な差でした。
一方、ダメ商談C(機能を一方的に説明する台本)への指摘は容赦なし。
{
"one_line": "機能説明に終始し顧客課題を掘れなかった一方通行商談",
"missed": "店舗数・パート比率・紙運用の何が辛いかを一切聞かずに提案を進めた"
}
しかも面白いことに、ダメ商談からも「移行コストを定量化して示す(1週間で運用開始)」みたいな盗める型は拾ってくる。全否定せず、文脈で良し悪しを切り分けてくる。このへんの解像度は、SQLの集計では絶対に出せません。
抽出した型をプレイブックにする
エージェントが各商談から抜き出した techniques を並べれば、それがそのまま新人向けのプレイブックになります。実際に出てきた型がこれ。
エージェントが抽出した「型」(全文)
トップ商談A:
- 仮定質問で理想の未来を描かせ変化意欲を高める型
- 課題優先順位を顧客に確認し合意を取る型
- 顧客状況を前提にした次回提案で継続動機を作る型
トップ商談B:
- 実際のヒヤリ体験を引き出して痛みを具体化する
- 仮定質問で解決後の状態を顧客自身に語らせる
- 課題を要約→自社強みへ橋渡しして提案を接続する
平均商談C(反面教師+拾える型):
- 顧客の懸念に複数の代替手段をセットで即提示する(ICカード+タブレット共有)
- 移行コストを定量化して示す(「1週間で運用開始」の具体数字)
- 価格+無料期間をセットで提示し初期障壁を下げる
「トップを見て学べ」と言われても何を見ればいいか分からない。でも「仮定質問で解決後の状態を顧客に語らせる」なら、明日マネできます。暗黙知が形式知になった手応えがありました。
やってみて分かったこと
正直な所感を残します。
- 役割分担が大事。表層の定量(発話比率・被せ)はSQLが速くて安い。文脈の質(質問の良し悪し、抜け)はエージェントが圧倒的。どちらか一方ではなく、安いSQLで足切りして高いエージェントで深掘る、の二段構えが現実的でした。
-
Scribeの日本語文字起こしは、主要語はよく拾える。専門用語や数字も思ったより取れた。ただ誤認識もゼロではなく(「打刻」を取りこぼす、など)、実運用では校正・辞書・後処理が前提です。あと
diarizationがja-JP非対応なのは要注意で、日本語で話者分離前提の設計をすると詰みます。 - claude CLIのヘッドレスが便利。APIキーの管理なしに、スクリプトから生成AIの判断を1ステップで挟める。今回みたいな「文脈評価をパイプラインに噛ませる」用途にハマりました。
最初は「文字起こしして要約するだけ」になりかけていました。でもエージェントに文脈ごと読ませた瞬間、SQLでは絶対に出ない「表面上は良いが抜けている」という指摘が出てきて、ここが一番ゾクッとしたところです。
次は、実際のチームの商談(同意を取って)でこれを回してみたい。あと今回はトラックを分ける前提でしたが、ミックス音源しかない場合に外部のdiarizationを噛ませる版も試したいです。やったらまた書きます。似たことを試した人がいたら、どんな観点で採点させているか教えてください。

