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_setはsaveAnswersのレスポンスから返却し、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