3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

商談を文字起こししてClaudeに丸ごと採点させたら、SQLでは測れない「トークの質」まで見えた

3
Posted at

はじめに

「なんであの人は売れるんだろう」。営業を見ていて一度はそう思ったこと、ないでしょうか。私はあります。そしてその答えはたいてい「人柄」とか「経験」で片付けられて、再現できないまま終わる。

この記事は、その「なんとなく売れる」を機械で評価できないか試した記録です。商談をZoomのScribe APIで文字起こしし、まずSQLで表層の指標を出し、そのうえで文脈の要る評価はClaude(エージェント)に丸ごと読ませて採点させました。結果として、SQLで測れる部分とエージェントでしか測れない部分が、きれいに線引きできました。

この記事で使う商談データは、私が台本を書いて音声合成した完全な架空データです。実在の商談ではありません。なので「トップ営業はこうだ」という事実を主張する記事ではなく、「この手法で、仕込んだトークの差をちゃんと検出できるか」という手法検証の記事です。数字はその文脈で読んでください。

何を作ったか

やったことはシンプルです。

  1. 架空の商談3本(トーク巧者2本/一方的に喋るダメ商談1本)の音声を合成
  2. Zoom Scribe で話者ごとに文字起こし
  3. PostgreSQL に発話を貯める
  4. SQLで「表層の指標」を出す(発話比率、被せ)
  5. Claudeエージェントに会話を丸ごと読ませて「質」を採点させる

最後のステップが本題です。完成イメージがこれ。

image.png

青がトーク巧者の商談、赤が一方的な商談。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 で音声合成しました。録音すらしていません。話者ごとに別ボイスを当てて喋らせます。

make_audio.py(抜粋)
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で尺(秒)を返すヘルパ(実装は割愛)

発話の長さから「何秒地点から喋るか」を計算し、ffmpegadelay で配置します。台本が「——」で終わる行は、次の発話を食い込ませて被せを再現しました。作った商談はトーク巧者2本(別の顧客で同じ型を再現)と、機能を一方的に説明するダメ商談1本です。

Scribeで文字起こしする

Scribe Fast modeは音声を公開URLで渡す仕様です(base64やローカルアップロードは不可)。ここでもハマって、最初に使おうとした一時アップロードサービスが軒並み死んでいました。transfer.sh は閉鎖、0x0.st は「AIボットのスパムが多すぎて停止」とのこと。時代ですね。結局 catbox.moe が生きていました。

呼び出しはシンプルです。今回は単一話者トラックなので、diarization=False を渡して話者分離は明示的にオフにします(そもそもja-JPでは使えないので)。

zoom_ai.py(抜粋)
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;

image.png

被せ(割り込み)も、ウィンドウ関数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採点させました。

agent_analysis.py(抜粋)
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を噛ませる版も試したいです。やったらまた書きます。似たことを試した人がいたら、どんな観点で採点させているか教えてください。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?