Skip to content

AIによるアンケート結果からの口コミ自動生成

概要

項目内容
ステータス🟢 進行中
Issue#10
担当-

AI がアンケート回答結果を分析し、口コミ文章を自動生成する機能。高評価のアンケート回答後、システムが口コミの下書きを自動生成し、顧客が確認・編集してGoogleに投稿する。

提案内容

背景・課題

  • 口コミの作成が手動で行われており、時間と手間がかかる
  • アンケートで収集した顧客の声を口コミとして有効活用できていない
  • 口コミの品質やトーンが統一されていない

提案するソリューション

LaravelからClaude APIを直接呼び出し、アンケートデータから自然な口コミ文章を生成する機能を構築する。専用のマイクロサービスは不要、Pythonも不要。

主な機能:

  • アンケート結果を口コミ形式の文章に自動変換(顧客が事前に選択する必要なし)
  • 店舗ごとのキーワード設定 — 設定済みキーワードを自然に含む口コミを生成
  • ハイブリッド品質チェック(ルールベース + LLM)
  • アンケートのお礼画面に直接統合
  • 生成後の編集・確認UI
  • 確認後のGoogle口コミ投稿フロー

機能一覧

#機能名説明優先度
1AI口コミ生成アンケート送信後、最も星が高い項目からフル口コミを自動生成
2店舗キーワード設定店舗ごとにキーワードを設定し、口コミに自然に反映
3ハイブリッド品質チェックルールベース + LLMで生成文章の品質をチェック
4編集・確認UI生成された口コミの確認・手動編集画面
5口コミ投稿連携確認済み口コミをGoogleに直接投稿するフロー

画面遷移フロー & モック

全体フロー(お礼画面に統合)

画面一覧

#画面新規/既存説明
1アンケート画面既存変更なし
2お礼画面(低評価)既存変更なし
3プレビュー・編集新規高評価 → 最も星が高い項目からフル口コミを自動生成 + 編集 + Copy & Google Review投稿ボタン

既存コードへの挿入箇所

File: resources/js/mappy/views/guest/Questionnaire.vue

現在のロジック(line 288-292):

javascript
// アンケート送信後
if (this.getStarRatingAverage() >= this.GOOGLE_REVIEW_BUTTON_DISPLAY_VALUE) {
    this.showGoogleReviewLink = true;  // Google Reviewボタンのみ表示
}

新しいロジック:

javascript
// アンケート送信後
axios.post(url, params).then((response) => {
    // race conditionを避けるためbackendからanswer_setを返す
    this.answerSet = response.data.answer_set;

    if (this.getStarRatingAverage() >= this.GOOGLE_REVIEW_BUTTON_DISPLAY_VALUE) {
        this.currentStep = 'review';
        this.generateReview();  // answerSet付きでAPI呼び出し、最も星が高い項目から直接フル口コミを生成
    } else {
        this.currentStep = 'thank-you';
    }
    this.completed = true;
});

Race condition

answer_setsaveAnswersのレスポンスから返却し、generate-review APIに渡す必要がある。 「最新のanswer_set」をクエリしてはいけない。他の顧客が同時にsubmitする可能性があるため。

送信後の2つの画面状態

状態条件表示内容
thank-you低評価従来通りのお礼画面
review高評価最も星が高い項目からフル口コミ(200-400文字)を自動生成 + 編集 + Copy & Google Review投稿ボタン

口コミ生成の詳細

Sonnetを1回だけ呼び出し、最も星が高い評価項目に焦点を絞った200-400文字のフル口コミを生成する。顧客が事前に書き出しを選択する必要はない。

焦点となる評価項目の選択ロジック:

  • 星評価の質問の中から最も星が高い項目を取得
  • 複数の項目が同じ最高星数の場合 → 元の質問順を優先(例:接客 → 清潔感 → 満足度、3項目とも★5なら接客を選択)
  • 選択された1項目のみ(項目名 + 星数)をプロンプトに送信。他のアンケート回答(残りの項目、テキスト回答、Y/N回答)は送信しない → プロンプトが簡潔で焦点が絞られる

詳細:

  • デフォルトトーン: 丁寧
  • 待ち時間: 5-10秒
  • 「トーン変更」ボタン → regenerate_with_toneを呼び出し(別トーンで最初から再生成)
  • 「Copy & Google Review投稿」ボタンは2つのアクションを統合: 内容をクリップボードにコピーし、そのままGoogle Mapsに遷移して顧客が貼り付けて投稿できる

フロントエンドのエラーハンドリング

エラー表示内容
生成失敗(3回リトライ超過)「生成に失敗しました」メッセージ + 「もう一度試す」ボタン または Google Reviewボタン
レート制限(429)「本日の生成上限に達しました」メッセージ + Google Reviewボタン
タイムアウト / APIエラー通常のお礼画面 + Google Reviewボタンにフォールバック(既存フロー)

原則: AI失敗時は常にGoogle Reviewボタンにフォールバックし、顧客が手動でレビューを書けるようにする。フローをブロックしない。

既存アンケートフロー

QRコード → アンケート回答 → 高評価時に口コミ案内

1QR読取
2アンケート
3完了
4クチコミ

店舗に設置されたQRコードをスキャン

https://example.com/questionnaire/abc123

AI口コミ生成フロー

最も星が高い項目からフル口コミを自動生成 → 編集 → Copy & 投稿

アンケート

ご来店ありがとうございます
スタッフの接客はいかがでしたか
店内の清潔感はいかがでしたか
サービスにご満足いただけましたか
また来店したいと思いますか
ご意見・ご要望

AI reviewキーワード設定(新規画面)

既存のキーワード画面を使わない理由

画面 /mappy/keywords(KeywordSettings.vue)はmappy_keywordsテーブルに店舗あたり最大8キーワードで保存している。このデータはスクレイピングに使用されている。9番目以降のキーワードを追加するとスクレイピングのロジックに影響するか、スクレイピングのデータ取得ロジックの更新が必要になる。そのためAI reviewキーワード用に別テーブル別画面を作成する必要がある。

継承ロジック: 店舗がAI review用のキーワードを未設定の場合 → mappy_keywordsテーブル(既存のキーワード画面)のキーワードをデフォルトとして自動使用。

項目内容
画面パス/mappy/review-keywords (新規)
コンポーネントReviewKeywordSettings.vue (新規)
DBテーブルmappy_review_keywords (新規)
フォールバック未設定の場合 → mappy_keywordsから継承
上限キーワード数制限なし(推奨3-10)

レビュー生成時のキーワード取得フロー:

1. mappy_review_keywords WHERE gbp_location_id = ? をクエリ
2. 結果がある場合 → 専用キーワードを使用
3. 空の場合 → フォールバックで mappy_keywords WHERE gbp_location_id = ? をクエリ

キーワード管理

登録キーワード8 / 8件
1MEO対策
2美容室 渋谷
3ヘアサロン
4カット 安い
5縮毛矯正
6トリートメント
7ヘッドスパ
8カラー 渋谷
上限に達しています。上限撤廃モードを有効にしてください。

変更内容

項目現行変更後
キーワード上限8件無制限(推奨10件以上)
スクレイピング8件固定登録数に応じて動的
表示固定レイアウトスクロール対応

アーキテクチャ

技術スタック

項目技術備考
バックエンドLaravel(既存)新規サービス不要、既存コードベースに追加
LLM APIAnthropic API(HTTP直接呼び出し)Guzzle HTTPクライアントを使用
LLM(生成)Claude Sonnet 4日本語品質が良好、口コミ生成に使用
LLM(チェック)Claude Haiku 4.5低コスト、品質チェックに使用
APIキー1つのキーを共有(全モデル共通)ANTHROPIC_API_KEY

Python / LangChainが不要な理由

この機能の処理フローは「プロンプト送信 → テキスト受信 → JSON検証」という1回のリトライを含む単純な順次処理のみ。LangChain/LangGraphの機能(RAG、tool calling、parallel branching、multi-agent)は一切使用しない。LaravelからAnthropic APIを直接呼び出すことで:

  • 依存関係の削減: Python パッケージ約15個の追加が不要
  • デプロイの簡素化: 専用マイクロサービス不要、Python用Dockerも不要
  • デバッグの容易さ: API呼び出しを隠す抽象化レイヤーがない
  • 既存インフラの活用: Laravel サーバーを共有、Guzzle HTTPクライアントが既に利用可能

システム構成

アーキテクチャの特徴:

  • 専用マイクロサービス不要 — 既存Laravelコードベースに追加
  • Guzzle HTTPクライアント(既存)でClaude APIを直接呼び出し
  • デプロイ・運用がシンプル: 1サーバー、1ログ

口コミ生成フロー

ステップ処理内容
最も星が高い項目を選択最も星が高い評価項目を特定(同点の場合は質問順でtie-break)+ 店舗キーワードをクエリ
口コミ生成Anthropic API(Sonnet 4)を1項目 + キーワード + トーンのみを含むプロンプトで呼び出し(アンケートの残りは送信しない)
品質チェックルールベースを先に → LLMを後に(ハイブリッド、詳細は下記)
フィードバック付き再生成NG時、スコア + 改善提案を付けてプロンプトを再送信(最大3回)

品質チェック(ハイブリッド)

全5項目をLLMで呼び出す代わりに、2ステップに分割:

ステップ1: ルールベース(即時、無料)

基準配点チェック方法
文字数(200〜400)20点mb_strlen()
キーワード反映(1〜3語)15点部分文字列チェック
  • ルールベースで明確にfailした場合(例:50文字のみ)→ LLMステップをスキップし、即座にリトライ
  • 明らかなfailケースでLLM呼び出し1回を節約

ステップ2: LLM(意味理解が必要な基準のみ)

基準配点チェック方法
自然さ(AI感がないか)30点Claude Haiku 4.5
不適切な表現20点Claude Haiku 4.5
焦点の反映(選択された項目)15点Claude Haiku 4.5

合格基準: 合計 >= 70点 → 合格 / 70点未満 → 再生成(最大3回)

プロンプト構成

プロンプトの詳細設計はプロンプト & API設計を参照。

Generate full review(概要):

Sonnetに送る入力は以下のみ: 最も星が高い項目の名前 + 星数 + 店舗キーワードリスト + トーン。 アンケートの他の回答は送信しない

System: 口コミ生成アシスタント
  - 提供された1つの項目に焦点を絞り、200〜400文字のフル口コミを生成
  - デフォルトトーン: 丁寧
  - 店舗キーワードを自然に組み込む(詰め込みNG)

User (例):
  項目: 接客 (★5)
  キーワード: カット, カラー, スタイリング
  トーン: 丁寧

LaravelからのAPI呼び出し(例)

php
// app/Services/ReviewGeneratorService.php
use Illuminate\Support\Facades\Http;

class ReviewGeneratorService
{
    public function generate(array $topItem, array $keywords, string $tone = '丁寧'): array
    {
        // $topItem = ['name' => '接客', 'rating' => 5]  ← 最も星が高い項目のみ
        // surveyデータ全体を受け取らない — プロンプトを簡潔にし、AIが不要なデータに引きずられるのを避ける

        // 1. 最も星が高い項目からフル口コミを生成(Sonnet 4)
        $review = $this->callClaude('claude-sonnet-4-20250514', 0.8, [
            ['role' => 'user', 'content' => $this->buildGeneratePrompt($topItem, $keywords, $tone)]
        ]);

        // 2. ハイブリッド品質チェック
        for ($attempt = 0; $attempt < 3; $attempt++) {
            $check = $this->qualityCheck($review, $topItem, $keywords);
            if ($check['passed']) {
                return ['review_text' => $review, 'quality_score' => $check['score']];
            }
            // フィードバック付きリトライ
            $review = $this->callClaude('claude-sonnet-4-20250514', 0.9, [
                ['role' => 'user', 'content' => $this->buildRetryPrompt($topItem, $keywords, $tone, $check['feedback'])]
            ]);
        }
        throw new GenerationFailedException();
    }

    private function qualityCheck(string $review, array $topItem, array $keywords): array
    {
        $score = 0;

        // ステップ1: ルールベース(即時)
        $charCount = mb_strlen($review);
        $score += ($charCount >= 200 && $charCount <= 400) ? 20 : 0;

        $found = collect($keywords)->filter(fn($kw) => str_contains($review, $kw))->count();
        $score += min($found, 3) * 5;

        // ルールベースで明確にNGなら早期fail
        if ($score < 15) {
            return ['passed' => false, 'score' => $score, 'feedback' => '文字数またはキーワードが基準未達'];
        }

        // ステップ2: LLMチェック(Haiku 4.5)— 項目名+星数のみ渡し、焦点が反映されているか確認
        $llmResult = $this->callClaude('claude-haiku-4-5-20251001', 0.1, [
            ['role' => 'user', 'content' => $this->buildQualityCheckPrompt($review, $topItem)]
        ]);
        $result = json_decode($llmResult, true);
        $score += ($result['naturalness'] ?? 0) + ($result['inappropriate'] ?? 0) + ($result['topic_reflection'] ?? 0);

        return ['passed' => $score >= 70, 'score' => $score, 'feedback' => $result['feedback'] ?? ''];
    }

    private function callClaude(string $model, float $temperature, array $messages, string $system = ''): string
    {
        // 全モデル共通の1つのAPIキー
        $payload = [
            'model' => $model,
            'max_tokens' => 1024,
            'temperature' => $temperature,
            'messages' => $messages,
        ];
        // Anthropic API: system promptはmessagesとは別に送信する必要がある
        if ($system) {
            $payload['system'] = $system;
        }

        $response = Http::withHeaders([
            'x-api-key' => config('services.anthropic.api_key'),
            'anthropic-version' => '2023-06-01',
        ])->timeout(30)->post('https://api.anthropic.com/v1/messages', $payload);

        if ($response->failed()) {
            throw new \RuntimeException("Anthropic API error: {$response->status()}");
        }

        return $response->json('content.0.text');
    }
}

LLM比較 & コスト

ModelInputOutputコスト/件日本語品質推奨
Claude Sonnet 4$3.00/1M tokens$15.00/1M tokens~¥0.5推奨
Claude Haiku 4.5$0.80/1M tokens$4.00/1M tokens~¥0.1コスト優先
GPT-4o$2.50/1M tokens$10.00/1M tokens~¥0.4代替
GPT-4o mini$0.15/1M tokens$0.60/1M tokens~¥0.03非推奨
Gemini 2.5 Flash$0.15/1M tokens$0.60/1M tokens~¥0.03コスト優先

コスト算出条件

  • 1件あたり: 入力 ~150 tokens(項目名 + 星数 + キーワード + トーン)+ 出力 ~400 tokens(口コミ文章)
  • 品質チェック再生成: 平均1.2回/件
  • 100件/月生成の場合: Claude Sonnet 4 ~¥50/月(プロンプトが簡潔になったため旧設計より削減)

推奨構成

用途Model理由
口コミ生成(本番)Claude Sonnet 4日本語品質◎、コスト妥当
品質チェックClaude Haiku 4.5判定処理にはコスト優先
開発・テストClaude Haiku 4.5高速、繰り返しテストに低コスト

概算工数(AI前提)

体制

役割人数担当内容
設計者1名要件確認 → AIに設計書作成指示 → レビュー → 製造へ指示
製造者1名ISSUEを元にAIに作成指示 → コードレビュー → テスト実施 → デプロイ

工数内訳(Python廃止により削減)

#作業項目AIリテイクレビュー工数(人日)担当
1要件確認 & 設計書作成2回0.5日/回1.0設計
2プロンプト設計 & API設計2回0.5日/回1.0設計
3AI生成Service実装(Laravel)2回0.5日/回1.0製造
4レビュー画面UI実装(Vue.js)2回0.5日/回1.0製造
5Copy & Google Review投稿ボタン実装1回0.5日/回0.5製造
6結合テスト & 品質調整2回0.5日/回1.0製造
7デプロイ & 動作確認1回0.5日/回0.5製造
合計6.0

前提条件・制約

  • Anthropic APIアカウントとANTHROPIC_API_KEYが設定済みであること
  • アンケートデータがDBからクエリ可能であること(mappy_questionnaire_answers
  • LaravelサーバーがHTTPS外部通信可能であること(api.anthropic.com宛)
  • 設計・製造ともにAI(Claude Code/Cursor等)を活用することが前提

スケジュール

タスク担当日数4/34/44/54/64/74/84/94/104/114/124/13
要件確認 & 設計書作成設計1d
プロンプト & API設計設計1d
設計レビュー & 修正設計1d
製造指示 & ISSUE作成設計1d
AI生成Service実装(Laravel)製造1d
レビュー画面UI実装製造1d
Copy & Google投稿ボタン実装製造1d
結合テスト & 品質調整製造1d
デプロイ & 動作確認製造1d