お問い合わせフォームから届く内容を人手で分類・振り分けしていませんか?
本記事では、OpenAI APIを活用してお問い合わせの自動分類・ルーティングを実現する方法を紹介します。
カテゴリ分類、緊急度判定、感情分析、LTV予測、エスカレーション判定まで、
AIが自動で処理することで、対応の初動を大幅に短縮できます。
注意点
・ APIコストが発生します(1件あたり約0.01〜0.05円程度)
・ 個人情報をAPIに送信する場合はプライバシーポリシーの確認が必要です
・ AI判定は100%正確ではないため、重要な判断は人間が確認してください
実装の概要
今回のコードでは、以下の流れで処理を行います。
2. フォーム完了画面のPHPでOpenAI APIを呼び出し、内容を分析
3. 分類結果に基づいて担当者を自動決定(負荷分散対応)
4. エスカレーション判定を実行
事前準備
1. お問い合わせDB、担当者マスタDBを作成します
2. 担当者マスタにデータを登録します
3. フォームの完了画面にPHPコードを設置します
4. OpenAI APIキーを取得・設定します
お問い合わせDB
| フィールド名 | 識別名 | 型 | 説明 |
|---|---|---|---|
| ID | contactId | 数字・記号・アルファベット(32 bytes) 自動採番 | 主キー |
| お問い合わせ日時 | regist_date | 登録日時 | 日付・時刻 |
| お名前 | name | テキスト | 問い合わせ者名 |
| 会社名 | company | テキスト | 会社名 |
| メールアドレス | メールアドレス | 連絡先 | |
| お問い合わせ内容 | content | テキストエリア | 本文 |
| カテゴリ | category | セレクト | 技術/営業/クレーム/その他 |
| 緊急度 | urgency | セレクト | 高/中/低 |
| 感情 | sentiment | セレクト | ポジティブ/ニュートラル/ネガティブ |
| LTVスコア | ltv_score | 整数 | 0-100 |
| 担当部署 | department | テキスト | ルーティング先 |
| 担当者 | assignee | テキスト | 担当者名 |
| エスカレーション | escalation | セレクト | 要/不要 |
| AI解析結果 | ai_analysis | テキストエリア | JSON形式 |
| ステータス | status | セレクト | 未対応/対応中/完了 |
担当者マスタDB
| フィールド名 | 識別名 | 型 | 説明 |
|---|---|---|---|
| 担当者ID | staff_id | 数字・記号・アルファベット(32 bytes) | 主キー |
| 担当者名 | staff_name | テキスト | 氏名 |
| メールアドレス | メールアドレス | 通知先 | |
| 部署 | department | セレクト | 技術/営業/カスタマーサポート |
| 対応カテゴリ | categories | マルチセレクト | 技術/営業/クレーム/その他 |
| 役職 | role | セレクト | 一般/リーダー/マネージャー |
本番用コード(完全版)
以下のコードをフォームの完了画面のPHPにそのまま設置してください。
冒頭の設定部分(APIキー、DB名等)を環境に合わせて変更するだけで動作します。
登録されたデータは$SPIRAL->getParam()で取得します。
<?//<!-- SMP_DYNAMIC_PAGE DISPLAY_ERRORS=ON NAME=XXX -->?>
<?php
/**
* AI問い合わせ自動分類・ルーティングシステム
*
* お問い合わせフォームから登録されたデータをAIで分析し、
* カテゴリ分類、緊急度判定、感情分析、LTV予測、エスカレーション判定を行い、
* 適切な担当者に自動ルーティングするシステム
*
* SPIRAL ver.1用 - フォーム完了画面のPHPとして設置
*/
// ============================================================
// 設定
// ============================================================
define('OPENAI_API_KEY', 'sk-xxxxxxxxxxxxxxxxxxxxxxxx'); // OpenAI APIキー
define('OPENAI_MODEL', 'gpt-5.2'); // OpenAI モデル名
define('INQUIRY_DB_NAME', 'inquiry_db'); // お問い合わせDB名
define('STAFF_DB_NAME', 'staff_master'); // 担当者マスタDB名
// ============================================================
// メイン処理
// ============================================================
// フォームから登録されたデータを取得(ver.1形式)
$recordId = $SPIRAL->getContextByFieldTitle('contactId');
$content = $SPIRAL->getParam('content');
$customerName = $SPIRAL->getParam('name');
$companyName = $SPIRAL->getParam('company');
$email = $SPIRAL->getParam('email');
// 内容が空の場合はスキップ
if (empty(trim($content))) {
return;
}
// 過去のお問い合わせ履歴を取得(同一メールアドレス)
$customerHistory = [];
if (!empty($email)) {
$customerHistory = getCustomerHistory($email, $recordId);
}
// AI分析を実行
$analysisResult = analyzeInquiry($content, $customerName, $companyName, $customerHistory);
// ルーティング(担当者決定)- 負荷分散機能付き
$routing = routeWithLoadBalancing(
$analysisResult['category'],
$analysisResult['urgency'],
$analysisResult['ltv_score']
);
// エスカレーション判定
$escalation = checkEscalation(
$analysisResult['category'],
$analysisResult['urgency'],
$analysisResult['sentiment'],
$analysisResult['ltv_score'],
$analysisResult['churn_risk'],
$analysisResult['escalation_reason']
);
// SELECTフィールドのIDマッピング(SPIRALのDB設定に合わせて各種ID値を変更してください)
$selectMap = [
'category' => ['技術' => '1', '営業' => '2', 'クレーム' => '3', 'その他' => '4'],
'urgency' => ['高' => '1', '中' => '2', '低' => '3'],
'sentiment' => ['ポジティブ' => '1', 'ニュートラル' => '2', 'ネガティブ' => '3'],
'escalation' => ['要' => '1', '不要' => '2'],
'status' => ['未対応' => '1', '対応中' => '2', '完了' => '3']
];
// DB更新用データを準備(セレクトフィールドは文字列ではなくID値として格納)
$updateData = [
'category' => $selectMap['category'][$analysisResult['category']] ?? '4',
'urgency' => $selectMap['urgency'][$analysisResult['urgency']] ?? '2',
'sentiment' => $selectMap['sentiment'][$analysisResult['sentiment']] ?? '2',
'ltv_score' => $analysisResult['ltv_score'],
'escalation' => $selectMap['escalation'][$escalation['required'] ? '要' : '不要'] ?? '2',
'department' => $routing['department'],
'assignee' => $routing['assignee_name'],
'ai_analysis' => json_encode([
'summary' => $analysisResult['summary'],
'suggested_response_points' => $analysisResult['suggested_response_points'],
'keywords' => $analysisResult['keywords'],
'churn_risk' => $analysisResult['churn_risk'],
'upsell_opportunity' => $analysisResult['upsell_opportunity'],
'escalation_reason' => $escalation['reason'],
'escalation_level' => $escalation['level'],
'routing_reason' => $routing['reason'],
'analyzed_at' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE),
'status' => $selectMap['status']['未対応'] ?? '1'
];
// SPIRAL APIでレコード更新
updateRecord($recordId, $updateData);
// ============================================================
// AI分析機能
// ============================================================
/**
* お問い合わせ内容をAIで分析
*/
function analyzeInquiry($content, $customerName = '', $companyName = '', $customerHistory = [])
{
$prompt = buildAnalysisPrompt($content, $customerName, $companyName, $customerHistory);
$response = callOpenAI($prompt);
if ($response === false) {
return getDefaultAnalysisResult();
}
return parseAnalysisResponse($response);
}
/**
* 分析用プロンプトを構築
*/
function buildAnalysisPrompt($content, $customerName, $companyName, $customerHistory)
{
$systemPrompt = <<<EOT
あなたはお問い合わせ分析AIです。以下の情報を分析し、JSON形式で結果を返してください。
## 分析項目
1. category(カテゴリ): 以下から1つ選択
- "技術": 技術的な質問、バグ報告、使い方の質問
- "営業": 見積もり依頼、導入相談、デモ依頼
- "クレーム": 不満、苦情、解約示唆
- "その他": 上記に該当しないもの
2. urgency(緊急度): 以下から1つ選択
- "高": 本番障害、業務停止、即時対応必要
- "中": 当日〜翌日対応が望ましい
- "低": 通常対応で問題なし
3. sentiment(感情): 以下から1つ選択
- "ポジティブ": 好意的、感謝、期待
- "ニュートラル": 中立的、事務的
- "ネガティブ": 不満、怒り、失望
4. ltv_score(LTV予測スコア): 0-100の整数
5. escalation_reason(エスカレーション理由): 必要な場合はその理由
6. summary(要約): 50文字以内
7. suggested_response_points(回答のポイント): 箇条書き
8. keywords(キーワード): 最大5つ
9. churn_risk(解約リスク): 0-100の整数
10. upsell_opportunity(アップセル機会): 0-100の整数
## 出力形式(JSON)
{
"category": "技術",
"urgency": "中",
"sentiment": "ニュートラル",
"ltv_score": 50,
"escalation_reason": "",
"summary": "APIの認証方法についての質問",
"suggested_response_points": ["認証フローの説明"],
"keywords": ["API", "認証"],
"churn_risk": 10,
"upsell_opportunity": 30
}
EOT;
$userPrompt = "## お問い合わせ情報\n\n";
if ($customerName)
$userPrompt .= "お客様名: {$customerName}\n";
if ($companyName)
$userPrompt .= "会社名: {$companyName}\n";
if (!empty($customerHistory)) {
$userPrompt .= "\n## 過去のお問い合わせ履歴(直近3件)\n";
foreach (array_slice($customerHistory, 0, 3) as $history) {
$userPrompt .= "- [{$history['date']}] {$history['category']}: {$history['summary']}\n";
}
}
$userPrompt .= "\n## 今回のお問い合わせ内容\n{$content}";
return ['system' => $systemPrompt, 'user' => $userPrompt];
}
/**
* OpenAI APIを呼び出し
*/
function callOpenAI($prompt)
{
$data = [
'model' => OPENAI_MODEL,
'messages' => [
['role' => 'system', 'content' => $prompt['system']],
['role' => 'user', 'content' => $prompt['user']]
],
'response_format' => ['type' => 'json_object']
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . OPENAI_API_KEY
],
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return false;
}
$result = json_decode($response, true);
return $result['choices'][0]['message']['content'] ?? false;
}
/**
* AI分析レスポンスをパース
*/
function parseAnalysisResponse($response)
{
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return getDefaultAnalysisResult();
}
return [
'category' => $data['category'] ?? 'その他',
'urgency' => $data['urgency'] ?? '中',
'sentiment' => $data['sentiment'] ?? 'ニュートラル',
'ltv_score' => max(0, min(100, intval($data['ltv_score'] ?? 50))),
'escalation_reason' => $data['escalation_reason'] ?? '',
'summary' => mb_substr($data['summary'] ?? '', 0, 100),
'suggested_response_points' => $data['suggested_response_points'] ?? [],
'keywords' => $data['keywords'] ?? [],
'churn_risk' => max(0, min(100, intval($data['churn_risk'] ?? 0))),
'upsell_opportunity' => max(0, min(100, intval($data['upsell_opportunity'] ?? 0)))
];
}
/**
* デフォルト分析結果(API失敗時)
*/
function getDefaultAnalysisResult()
{
return [
'category' => 'その他', 'urgency' => '中', 'sentiment' => 'ニュートラル',
'ltv_score' => 50, 'escalation_reason' => '', 'summary' => 'AI分析に失敗しました',
'suggested_response_points' => [], 'keywords' => [],
'churn_risk' => 0, 'upsell_opportunity' => 0
];
}
// ============================================================
// ルーティング機能
// ============================================================
/**
* 負荷分散を考慮したルーティング
*/
function routeWithLoadBalancing($category, $urgency, $ltvScore)
{
$departmentMap = [
'技術' => 'テクニカルサポート',
'営業' => '営業部',
'クレーム' => 'カスタマーサポート',
'その他' => 'カスタマーサポート'
];
$department = $departmentMap[$category] ?? $departmentMap['その他'];
$vipThreshold = 70;
// 担当者マスタから候補を取得
$candidates = getRoutingCandidates($category, $urgency, $ltvScore, $vipThreshold);
if (empty($candidates)) {
return [
'department' => $department,
'assignee_id' => '',
'assignee_name' => '未割当',
'assignee_email' => '',
'reason' => "カテゴリ「{$category}」に対応可能な担当者が見つかりませんでした"
];
}
// 最初の担当者を選択(ver.1では負荷情報がないため)
$selectedStaff = $candidates[0];
$reasons = ["カテゴリ「{$category}」"];
if ($ltvScore >= $vipThreshold)
$reasons[] = "VIP顧客(LTVスコア: {$ltvScore})";
if ($urgency === '高')
$reasons[] = "緊急度「高」";
return [
'department' => $department,
'assignee_id' => $selectedStaff['staff_id'] ?? '',
'assignee_name' => $selectedStaff['staff_name'] ?? '未割当',
'assignee_email' => $selectedStaff['email'] ?? '',
'reason' => implode('、', $reasons) . " → {$selectedStaff['staff_name']}を選定"
];
}
/**
* ルーティング候補を取得(ver.1 API)
*/
function getRoutingCandidates($category, $urgency, $ltvScore, $vipThreshold)
{
global $SPIRAL;
// SELECTフィールドのIDマッピング
$selectMap = [
'category' => ['技術' => '1', '営業' => '2', 'クレーム' => '3', 'その他' => '4']
];
$categoryId = $selectMap['category'][$category] ?? '4';
// 担当者マスタから対応可能カテゴリ(マルチセレクト)に指定IDが含まれる担当者を検索
$db = $SPIRAL->getDataBase(STAFF_DB_NAME);
$db->addSelectFields('staff_id', 'staff_name', 'email', 'department', 'categories', 'role');
$db->addLikeCondition('categories', '%' . $categoryId . '%');
$db->setLinesPerPage(50);
$result = $db->doSelect();
if (!$result || empty($result['data'])) {
return [];
}
// VIPまたは緊急の場合はマネージャー/リーダーを優先
$candidates = [];
foreach ($result['data'] as $staff) {
if (($ltvScore >= $vipThreshold || $urgency === '高') &&
in_array($staff['role'] ?? '', ['マネージャー', 'リーダー'])) {
array_unshift($candidates, $staff);
}
else {
$candidates[] = $staff;
}
}
return $candidates;
}
// ============================================================
// エスカレーション判定機能
// ============================================================
/**
* エスカレーション判定を実行
*/
function checkEscalation($category, $urgency, $sentiment, $ltvScore, $churnRisk, $aiEscalationReason = '')
{
$thresholds = [
'ltv_vip' => 80,
'ltv_high_value' => 60,
'churn_risk_high' => 70,
'churn_risk_medium' => 40
];
$reasons = [];
$level = 0;
// レベル3(最高緊急度)
if ($ltvScore >= $thresholds['ltv_vip'] && $category === 'クレーム' && $urgency === '高') {
$reasons[] = 'VIP顧客の緊急クレーム';
$level = max($level, 3);
}
if ($ltvScore >= $thresholds['ltv_vip'] && $churnRisk >= $thresholds['churn_risk_high']) {
$reasons[] = 'VIP顧客の解約リスク高';
$level = max($level, 3);
}
// レベル2
if ($category === 'クレーム' && $urgency === '高') {
$reasons[] = '緊急クレーム';
$level = max($level, 2);
}
if ($ltvScore >= $thresholds['ltv_vip'] && $urgency === '高') {
$reasons[] = 'VIP顧客の緊急案件';
$level = max($level, 2);
}
if ($ltvScore >= $thresholds['ltv_vip'] && $category === 'クレーム') {
$reasons[] = 'VIP顧客からのクレーム';
$level = max($level, 2);
}
if ($churnRisk >= $thresholds['churn_risk_high'] && $sentiment === 'ネガティブ') {
$reasons[] = '解約リスク高+ネガティブ';
$level = max($level, 2);
}
// レベル1
if ($category === 'クレーム' && $sentiment === 'ネガティブ') {
$reasons[] = 'ネガティブなクレーム';
$level = max($level, 1);
}
if ($ltvScore >= $thresholds['ltv_high_value'] && $sentiment === 'ネガティブ') {
$reasons[] = '高価値顧客の不満';
$level = max($level, 1);
}
if ($category === '営業' && $ltvScore >= $thresholds['ltv_vip']) {
$reasons[] = 'VIP商談機会';
$level = max($level, 1);
}
if ($churnRisk >= $thresholds['churn_risk_medium'] && $churnRisk < $thresholds['churn_risk_high']) {
$reasons[] = '解約リスク中程度';
$level = max($level, 1);
}
if (!empty($aiEscalationReason)) {
$reasons[] = "AI判定: {$aiEscalationReason}";
$level = max($level, 1);
}
$levelDescriptions = [
0 => '通常対応',
1 => '部門責任者へエスカレーション',
2 => '事業部長へ緊急エスカレーション',
3 => '経営層へ最優先エスカレーション'
];
return [
'required' => $level > 0,
'level' => $level,
'level_description' => $levelDescriptions[$level] ?? '不明',
'reason' => implode('、', $reasons)
];
}
// ============================================================
// SPIRAL API関連機能(ver.1)
// ============================================================
/**
* 顧客の過去のお問い合わせ履歴を取得
*/
function getCustomerHistory($email, $excludeRecordId)
{
global $SPIRAL;
$db = $SPIRAL->getDataBase(INQUIRY_DB_NAME);
$db->addSelectFields('contactId', 'category', 'ai_analysis', 'regist_date');
$db->addEqualCondition('email', $email);
$db->addSortField('regist_date', false);
$db->setLinesPerPage(4);
$result = $db->doSelect();
if (!$result || empty($result['data'])) {
return [];
}
$history = [];
foreach ($result['data'] as $item) {
if (isset($item['contactId']) && $item['contactId'] == $excludeRecordId)
continue;
if (count($history) >= 3)
break;
$aiAnalysisStr = (isset($item['ai_analysis']) && is_string($item['ai_analysis'])) ? $item['ai_analysis'] : '{}';
$aiAnalysis = json_decode($aiAnalysisStr, true) ?: [];
$history[] = [
'date' => date('Y-m-d', strtotime($item['regist_date'] ?? 'now')),
'category' => (isset($item['category']) && is_string($item['category'])) ? $item['category'] : 'その他',
'summary' => $aiAnalysis['summary'] ?? ''
];
}
return $history;
}
/**
* レコードを更新
*/
function updateRecord($recordId, $data)
{
global $SPIRAL;
$db = $SPIRAL->getDataBase(INQUIRY_DB_NAME);
$db->addEqualCondition("contactId", $recordId);
return $db->doUpdate($data);
}
?>
コード詳細解説
上記の本番用コードを機能ごとに分割して解説します。
各機能の仕組みを理解することで、カスタマイズが容易になります。
1. AI分析機能
OpenAI APIを呼び出してお問い合わせ内容を分析する機能です。
analyzeInquiry()関数がエントリーポイントとなり、以下の処理を行います:
・ API呼び出し:OpenAI APIにリクエストを送信
・ レスポンス解析:JSON形式の結果をパースし、バリデーション
// ============================================================
// AI分析機能
// ============================================================
/**
* お問い合わせ内容をAIで分析
*/
function analyzeInquiry($content, $customerName = '', $companyName = '', $customerHistory = [])
{
$prompt = buildAnalysisPrompt($content, $customerName, $companyName, $customerHistory);
$response = callOpenAI($prompt);
if ($response === false) {
return getDefaultAnalysisResult();
}
return parseAnalysisResponse($response);
}
/**
* 分析用プロンプトを構築
*/
function buildAnalysisPrompt($content, $customerName, $companyName, $customerHistory)
{
$systemPrompt = <<<EOT
あなたはお問い合わせ分析AIです。以下の情報を分析し、JSON形式で結果を返してください。
## 分析項目
1. category(カテゴリ): 以下から1つ選択
- "技術": 技術的な質問、バグ報告、使い方の質問
- "営業": 見積もり依頼、導入相談、デモ依頼
- "クレーム": 不満、苦情、解約示唆
- "その他": 上記に該当しないもの
2. urgency(緊急度): 以下から1つ選択
- "高": 本番障害、業務停止、即時対応必要
- "中": 当日〜翌日対応が望ましい
- "低": 通常対応で問題なし
3. sentiment(感情): 以下から1つ選択
- "ポジティブ": 好意的、感謝、期待
- "ニュートラル": 中立的、事務的
- "ネガティブ": 不満、怒り、失望
4. ltv_score(LTV予測スコア): 0-100の整数
5. escalation_reason(エスカレーション理由): 必要な場合はその理由
6. summary(要約): 50文字以内
7. suggested_response_points(回答のポイント): 箇条書き
8. keywords(キーワード): 最大5つ
9. churn_risk(解約リスク): 0-100の整数
10. upsell_opportunity(アップセル機会): 0-100の整数
## 出力形式(JSON)
{
"category": "技術",
"urgency": "中",
"sentiment": "ニュートラル",
"ltv_score": 50,
"escalation_reason": "",
"summary": "APIの認証方法についての質問",
"suggested_response_points": ["認証フローの説明"],
"keywords": ["API", "認証"],
"churn_risk": 10,
"upsell_opportunity": 30
}
EOT;
$userPrompt = "## お問い合わせ情報\n\n";
if ($customerName)
$userPrompt .= "お客様名: {$customerName}\n";
if ($companyName)
$userPrompt .= "会社名: {$companyName}\n";
if (!empty($customerHistory)) {
$userPrompt .= "\n## 過去のお問い合わせ履歴(直近3件)\n";
foreach (array_slice($customerHistory, 0, 3) as $history) {
$userPrompt .= "- [{$history['date']}] {$history['category']}: {$history['summary']}\n";
}
}
$userPrompt .= "\n## 今回のお問い合わせ内容\n{$content}";
return ['system' => $systemPrompt, 'user' => $userPrompt];
}
/**
* OpenAI APIを呼び出し
*/
function callOpenAI($prompt)
{
$data = [
'model' => OPENAI_MODEL,
'messages' => [
['role' => 'system', 'content' => $prompt['system']],
['role' => 'user', 'content' => $prompt['user']]
],
'response_format' => ['type' => 'json_object']
];
$ch = curl_init('https://api.openai.com/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . OPENAI_API_KEY
],
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
return false;
}
$result = json_decode($response, true);
return $result['choices'][0]['message']['content'] ?? false;
}
/**
* AI分析レスポンスをパース
*/
function parseAnalysisResponse($response)
{
$data = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return getDefaultAnalysisResult();
}
return [
'category' => $data['category'] ?? 'その他',
'urgency' => $data['urgency'] ?? '中',
'sentiment' => $data['sentiment'] ?? 'ニュートラル',
'ltv_score' => max(0, min(100, intval($data['ltv_score'] ?? 50))),
'escalation_reason' => $data['escalation_reason'] ?? '',
'summary' => mb_substr($data['summary'] ?? '', 0, 100),
'suggested_response_points' => $data['suggested_response_points'] ?? [],
'keywords' => $data['keywords'] ?? [],
'churn_risk' => max(0, min(100, intval($data['churn_risk'] ?? 0))),
'upsell_opportunity' => max(0, min(100, intval($data['upsell_opportunity'] ?? 0)))
];
}
/**
* デフォルト分析結果(API失敗時)
*/
function getDefaultAnalysisResult()
{
return [
'category' => 'その他', 'urgency' => '中', 'sentiment' => 'ニュートラル',
'ltv_score' => 50, 'escalation_reason' => '', 'summary' => 'AI分析に失敗しました',
'suggested_response_points' => [], 'keywords' => [],
'churn_risk' => 0, 'upsell_opportunity' => 0
];
}
2. ルーティング機能
AI分析結果に基づいて適切な担当者を自動決定する機能です。
routeWithLoadBalancing()関数が以下のロジックで担当者を選定します:
・ VIP顧客判定:LTVスコア70以上はマネージャー/リーダーを優先
・ 担当者マスタから対応可能カテゴリでフィルタリング
// ============================================================
// ルーティング機能
// ============================================================
/**
* 負荷分散を考慮したルーティング
*/
function routeWithLoadBalancing($category, $urgency, $ltvScore)
{
$departmentMap = [
'技術' => 'テクニカルサポート',
'営業' => '営業部',
'クレーム' => 'カスタマーサポート',
'その他' => 'カスタマーサポート'
];
$department = $departmentMap[$category] ?? $departmentMap['その他'];
$vipThreshold = 70;
// 担当者マスタから候補を取得
$candidates = getRoutingCandidates($category, $urgency, $ltvScore, $vipThreshold);
if (empty($candidates)) {
return [
'department' => $department,
'assignee_id' => '',
'assignee_name' => '未割当',
'assignee_email' => '',
'reason' => "カテゴリ「{$category}」に対応可能な担当者が見つかりませんでした"
];
}
// 最初の担当者を選択(ver.1では負荷情報がないため)
$selectedStaff = $candidates[0];
$reasons = ["カテゴリ「{$category}」"];
if ($ltvScore >= $vipThreshold)
$reasons[] = "VIP顧客(LTVスコア: {$ltvScore})";
if ($urgency === '高')
$reasons[] = "緊急度「高」";
return [
'department' => $department,
'assignee_id' => $selectedStaff['staff_id'] ?? '',
'assignee_name' => $selectedStaff['staff_name'] ?? '未割当',
'assignee_email' => $selectedStaff['email'] ?? '',
'reason' => implode('、', $reasons) . " → {$selectedStaff['staff_name']}を選定"
];
}
/**
* ルーティング候補を取得(ver.1 API)
*/
function getRoutingCandidates($category, $urgency, $ltvScore, $vipThreshold)
{
global $SPIRAL;
// SELECTフィールドのIDマッピング
$selectMap = [
'category' => ['技術' => '1', '営業' => '2', 'クレーム' => '3', 'その他' => '4']
];
$categoryId = $selectMap['category'][$category] ?? '4';
// 担当者マスタから対応可能カテゴリ(マルチセレクト)に指定IDが含まれる担当者を検索
$db = $SPIRAL->getDataBase(STAFF_DB_NAME);
$db->addSelectFields('staff_id', 'staff_name', 'email', 'department', 'categories', 'role');
$db->addLikeCondition('categories', '%' . $categoryId . '%');
$db->setLinesPerPage(50);
$result = $db->doSelect();
if (!$result || empty($result['data'])) {
return [];
}
// VIPまたは緊急の場合はマネージャー/リーダーを優先
$candidates = [];
foreach ($result['data'] as $staff) {
if (($ltvScore >= $vipThreshold || $urgency === '高') &&
in_array($staff['role'] ?? '', ['マネージャー', 'リーダー'])) {
array_unshift($candidates, $staff);
}
else {
$candidates[] = $staff;
}
}
return $candidates;
}
3. エスカレーション判定機能
複数の条件を組み合わせてエスカレーションレベル(0〜3)を判定します。
checkEscalation()関数の判定ロジック:
・ レベル2(事業部長):緊急クレーム、VIP顧客の緊急案件
・ レベル1(部門責任者):ネガティブなクレーム、高価値顧客の不満
// ============================================================
// エスカレーション判定機能
// ============================================================
/**
* エスカレーション判定を実行
*/
function checkEscalation($category, $urgency, $sentiment, $ltvScore, $churnRisk, $aiEscalationReason = '')
{
$thresholds = [
'ltv_vip' => 80,
'ltv_high_value' => 60,
'churn_risk_high' => 70,
'churn_risk_medium' => 40
];
$reasons = [];
$level = 0;
// レベル3(最高緊急度)
if ($ltvScore >= $thresholds['ltv_vip'] && $category === 'クレーム' && $urgency === '高') {
$reasons[] = 'VIP顧客の緊急クレーム';
$level = max($level, 3);
}
if ($ltvScore >= $thresholds['ltv_vip'] && $churnRisk >= $thresholds['churn_risk_high']) {
$reasons[] = 'VIP顧客の解約リスク高';
$level = max($level, 3);
}
// レベル2
if ($category === 'クレーム' && $urgency === '高') {
$reasons[] = '緊急クレーム';
$level = max($level, 2);
}
if ($ltvScore >= $thresholds['ltv_vip'] && $urgency === '高') {
$reasons[] = 'VIP顧客の緊急案件';
$level = max($level, 2);
}
if ($ltvScore >= $thresholds['ltv_vip'] && $category === 'クレーム') {
$reasons[] = 'VIP顧客からのクレーム';
$level = max($level, 2);
}
if ($churnRisk >= $thresholds['churn_risk_high'] && $sentiment === 'ネガティブ') {
$reasons[] = '解約リスク高+ネガティブ';
$level = max($level, 2);
}
// レベル1
if ($category === 'クレーム' && $sentiment === 'ネガティブ') {
$reasons[] = 'ネガティブなクレーム';
$level = max($level, 1);
}
if ($ltvScore >= $thresholds['ltv_high_value'] && $sentiment === 'ネガティブ') {
$reasons[] = '高価値顧客の不満';
$level = max($level, 1);
}
if ($category === '営業' && $ltvScore >= $thresholds['ltv_vip']) {
$reasons[] = 'VIP商談機会';
$level = max($level, 1);
}
if ($churnRisk >= $thresholds['churn_risk_medium'] && $churnRisk < $thresholds['churn_risk_high']) {
$reasons[] = '解約リスク中程度';
$level = max($level, 1);
}
if (!empty($aiEscalationReason)) {
$reasons[] = "AI判定: {$aiEscalationReason}";
$level = max($level, 1);
}
$levelDescriptions = [
0 => '通常対応',
1 => '部門責任者へエスカレーション',
2 => '事業部長へ緊急エスカレーション',
3 => '経営層へ最優先エスカレーション'
];
return [
'required' => $level > 0,
'level' => $level,
'level_description' => $levelDescriptions[$level] ?? '不明',
'reason' => implode('、', $reasons)
];
}
4. SPIRAL API関連機能
SPIRAL ver.1のAPIを呼び出すための共通関数群です。
レコードの取得・更新を行います。
・ updateRecord():分析結果でレコードを更新
// ============================================================
// SPIRAL API関連機能ver.1
// ============================================================
/**
* 顧客の過去のお問い合わせ履歴を取得
*/
function getCustomerHistory($email, $excludeRecordId)
{
global $SPIRAL;
$db = $SPIRAL->getDataBase(INQUIRY_DB_NAME);
$db->addSelectFields('contactId', 'category', 'ai_analysis', 'regist_date');
$db->addEqualCondition('email', $email);
$db->addSortField('regist_date', false);
$db->setLinesPerPage(4);
$result = $db->doSelect();
if (!$result || empty($result['data'])) {
return [];
}
$history = [];
foreach ($result['data'] as $item) {
if (isset($item['contactId']) && $item['contactId'] == $excludeRecordId)
continue;
if (count($history) >= 3)
break;
$aiAnalysisStr = (isset($item['ai_analysis']) && is_string($item['ai_analysis'])) ? $item['ai_analysis'] : '{}';
$aiAnalysis = json_decode($aiAnalysisStr, true) ?: [];
$history[] = [
'date' => date('Y-m-d', strtotime($item['regist_date'] ?? 'now')),
'category' => (isset($item['category']) && is_string($item['category'])) ? $item['category'] : 'その他',
'summary' => $aiAnalysis['summary'] ?? ''
];
}
return $history;
}
/**
* レコードを更新
*/
function updateRecord($recordId, $data)
{
global $SPIRAL;
$db = $SPIRAL->getDataBase(INQUIRY_DB_NAME);
$db->addEqualCondition("contactId", $recordId);
return $db->doUpdate($data);
}
動作確認
テストケース1:技術的な質問(通常)
「APIの認証方法について教えてください。ドキュメントを見ましたが、トークンの取得方法がわかりません。」
期待される結果:
カテゴリ: 技術 / 緊急度: 低 / 感情: ニュートラル / エスカレーション: 不要
テストケース2:クレーム(緊急)
「先週から何度も問い合わせていますが、一向に返信がありません。このままでは契約を解除せざるを得ません。至急対応してください。」
期待される結果:
カテゴリ: クレーム / 緊急度: 高 / 感情: ネガティブ / エスカレーション: 要(マネージャーに通知)
テストケース3:営業案件(高LTV)
「弊社は従業員5000名規模の企業です。全社的なシステム導入を検討しており、年間予算として1000万円程度を想定しています。デモをお願いできますか。」
期待される結果:
カテゴリ: 営業 / 緊急度: 中 / 感情: ポジティブ / LTVスコア: 85以上 / エスカレーション: 要(VIP案件)
マーケティング活用
LTVスコアの活用
・ 高価値顧客(60-79点):アップセル/クロスセル提案の対象
・ 標準顧客(30-59点):通常対応
・ 低価値顧客(30点未満):自動応答やFAQ誘導を検討
感情分析の活用
・ ポジティブ傾向の検出:アップセル提案のタイミング
・ トレンド分析:製品改善のフィードバック
コスト試算
| 月間お問い合わせ数 | 推定APIコスト |
|---|---|
| 100件 | 約50〜100円 |
| 1,000件 | 約500〜1,000円 |
| 10,000件 | 約5,000〜10,000円 |
まとめ
特にクレームの早期検出やVIP顧客の優先対応は、顧客満足度向上に直結します。
さらにLTV予測や感情分析を活用することで、マーケティング施策にも活かせます。
不具合やご質問がある場合は、下記の「コンテンツに関しての要望はこちら」からご連絡ください。