GASで問い合わせ履歴から
AIでFAQを自動生成する方法

スプレッドシートに溜まった問い合わせをClaude API(Anthropic社の対話型AIをプログラムから呼び出す仕組み)に渡し、よく聞かれる質問を「質問」と「回答」のFAQ形式に自動で束ねる方法を、動くコードで解説します。

|対象: GAS / Claude API / 問い合わせ対応

Table of Contents

なぜ問い合わせ履歴からFAQを自動生成するのか

問い合わせフォームやメールに寄せられる質問は、実は同じような内容が繰り返されがちです。 これを人手で分類してFAQにまとめるのは手間がかかります。AIに任せれば、似た質問をまとめて 「質問」と「回答の下書き」のペアに整理でき、サポートページの更新や返信テンプレートづくりが一気に楽になります。

問い合わせ工数の削減

よくある質問をFAQ化して先回りで答え、同じ返信の繰り返しを減らせる

傾向の見える化

どんな質問が多いかを件数付きで把握でき、商品説明やLPの改善につながる

FAQページの土台づくり

ゼロから書くより、AIの下書きを直す方が圧倒的に速い

属人化の解消

ベテランの頭の中にあった『よく聞かれること』をデータから見える形にできる

全体の流れとシート構成

処理は「問い合わせを集める → まとめてAIに渡す → FAQを受け取る → シートに書き出す」の4ステップです。 入力となる「問い合わせログ」シートと、出力先の「FAQ下書き」シートの2枚を使います。

STEP 1

問い合わせを集める

「問い合わせログ」シートのB列(本文)を配列にまとめる

STEP 2

まとめてAIに渡す

50件ずつのバッチに分け、Claude APIに似た質問の統合を依頼する

STEP 3

FAQを受け取る

質問・回答・件数のJSON配列で受け取り、全バッチ分を統合する

STEP 4

シートに書き出す

件数が多い順に並べ替え、「FAQ下書き」シートへ出力する

APIキーを安全に保管する

Claude APIを使うにはAPIキーが必要です。キーはコードに直接書かず、 スクリプトプロパティ(プロジェクトごとに保存できる非公開の設定領域)に保管します。 こうすればコードを共有してもキーが漏れません。

// APIキーはコードに直書きせず、スクリプトプロパティに保管する
// エディタ左の「プロジェクトの設定 > スクリプト プロパティ」で
// ANTHROPIC_API_KEY を登録しておく
function getApiKey() {
  const key = PropertiesService.getScriptProperties()
    .getProperty("ANTHROPIC_API_KEY");
  if (!key) throw new Error("APIキーが未設定です");
  return key;
}

問い合わせをシートから集める

まずは入力データを用意します。「問い合わせログ」シートのB列に問い合わせ本文が入っている前提で、 空欄を除いた本文だけを配列にまとめます。getDataRange().getValues()で全データを一括取得すると高速です。

// 「問い合わせログ」シートのB列(問い合わせ本文)を集める
// 前提: 1行目はヘッダー、B列に本文が入っている
function collectInquiries() {
  const sheet = SpreadsheetApp.getActive().getSheetByName("問い合わせログ");
  const values = sheet.getDataRange().getValues();
  const rows = values.slice(1); // ヘッダーを除く

  return rows
    .map(function (row) { return String(row[1]).trim(); })
    .filter(function (text) { return text.length > 0; });
}

まとめてAIに渡しFAQをJSONで受け取る

問い合わせ一覧を1回のリクエストにまとめて渡し、似た質問を統合したFAQをJSON配列で受け取ります。プロンプトで「意味が近い質問は1つにまとめる」「代表的な質問文を1つだけ書く」と明示するのがポイントです。 回答は断定を避け、あくまで下書きとして書かせます。

// 問い合わせ一覧をまとめてAIに渡し、FAQ配列を受け取る
// items: 問い合わせ本文の配列(1バッチ分)
function generateFaqBatch(items) {
  // 1件ずつ番号を振って渡すと、AIが件数を数えやすい
  const list = items
    .map(function (t, i) { return (i + 1) + ". " + t; })
    .join("\n");

  const instruction =
    "あなたはカスタマーサポートのFAQ作成担当です。\n" +
    "次は、実際に届いた問い合わせの一覧です。\n" +
    "----\n" + list + "\n----\n" +
    "条件:\n" +
    "・意味が近い問い合わせは1つのFAQにまとめる。\n" +
    "・1つのFAQには、代表的な質問文をひとつだけ書く。\n" +
    "・question は検索されやすい自然な疑問文にする。\n" +
    "・answer は事実の断定を避け、下書きとして簡潔に書く。\n" +
    "・count は、そのFAQにまとめた問い合わせのおおよその件数。\n" +
    "・多く聞かれている順に並べる。\n" +
    "余計な説明は出力せず、JSON配列だけを返してください。\n" +
    '形式: [{"question": "質問文", "answer": "回答の下書き", "count": 件数}]';

  const payload = {
    model: "claude-sonnet-4-20250514",
    max_tokens: 2000,
    messages: [{ role: "user", content: instruction }],
  };

  const res = UrlFetchApp.fetch("https://api.anthropic.com/v1/messages", {
    method: "post",
    contentType: "application/json",
    headers: {
      "x-api-key": getApiKey(),
      "anthropic-version": "2023-06-01",
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true,
  });

  if (res.getResponseCode() !== 200) {
    throw new Error("API error: " + res.getContentText());
  }

  const responseText = JSON.parse(res.getContentText()).content[0].text;
  return JSON.parse(extractJsonArray(responseText));
}

// レスポンスから [ ... ] の部分だけを取り出す(前後の余計な文字対策)
function extractJsonArray(text) {
  const start = text.indexOf("[");
  const end = text.lastIndexOf("]");
  return text.slice(start, end + 1);
}

AIは前後に説明文を付けることがあるため、extractJsonArray()で最初の[から最後の]までを切り出してからパースし、余計な文字で失敗しないようにしています。

バッチ処理とリトライで安定させる

問い合わせが数百件あると、一度に渡すとトークン上限や6分の実行時間制限に達します。 50件ずつのバッチに分けて渡し、各バッチの結果を統合します。 レート制限などで一時的に失敗しても止まらないよう、少し待って再試行するリトライも入れます。

// 全問い合わせを50件ずつに分けてAIに渡し、結果を統合する
const BATCH_SIZE = 50;

function buildFaqDrafts() {
  const inquiries = collectInquiries();
  let faqs = [];

  for (let i = 0; i < inquiries.length; i += BATCH_SIZE) {
    const batch = inquiries.slice(i, i + BATCH_SIZE);
    const result = callWithRetry(function () {
      return generateFaqBatch(batch);
    });
    faqs = faqs.concat(result);
    Utilities.sleep(1000); // 連続呼び出しの間隔をあける
  }
  return faqs;
}

// レート制限などで失敗したとき、少し待って再試行する
function callWithRetry(fn) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      return fn();
    } catch (e) {
      if (attempt === 3) throw e;
      Utilities.sleep(2000 * attempt);
    }
  }
}

FAQをシートに書き出す

統合したFAQを「FAQ下書き」シートに書き出します。件数が多い順に並べ替えてから出力すると、 優先して整備すべき質問がひと目で分かります。シートが無ければ自動で作成します。

// FAQ下書きを「FAQ下書き」シートに書き出す
function writeFaqSheet(faqs) {
  const ss = SpreadsheetApp.getActive();
  let sheet = ss.getSheetByName("FAQ下書き");
  if (!sheet) sheet = ss.insertSheet("FAQ下書き");

  sheet.clearContents();
  sheet.getRange(1, 1, 1, 3).setValues([["質問", "回答(下書き)", "件数"]]);

  const rows = faqs.map(function (f) {
    return [f.question, f.answer, f.count || ""];
  });

  if (rows.length > 0) {
    sheet.getRange(2, 1, rows.length, 3).setValues(rows);
  }
}

// 実行のエントリーポイント
function runFaqGeneration() {
  const faqs = buildFaqDrafts();
  // 件数が多い順に並べ替えてから書き出す
  faqs.sort(function (a, b) { return (b.count || 0) - (a.count || 0); });
  writeFaqSheet(faqs);
}

定期トリガーで自動更新する

問い合わせは日々増えていきます。時間主導トリガー(決まった時間に関数を自動実行する仕組み)を使い、 たとえば毎週月曜の朝にFAQを作り直せば、直近の傾向を反映した最新のFAQ下書きへ自動更新できます。

// 毎週月曜の朝8時台にFAQを作り直すトリガーを作成する
function createWeeklyFaqTrigger() {
  ScriptApp.newTrigger("runFaqGeneration")
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.MONDAY)
    .atHour(8)
    .create();
}

精度を上げるコツと注意点

1. 回答文は必ず人が確認してから公開する

AIの回答はあくまで下書きです。料金・仕様・納期など事実にかかわる部分は、 担当者が自社の正しい情報に直してから公開してください。そのままの公開は避けます。

2. 個人情報は渡す前に取り除く

問い合わせ本文に氏名・電話番号・メールアドレスが含まれることがあります。 FAQ作成には不要なため、AIに渡す前にマスキングするか、本文だけを抜き出す運用にすると安全です。

3. バッチをまたぐ重複は二段階でまとめる

50件ずつ分けて渡すと、別々のバッチで似たFAQができることがあります。 気になる場合は、生成したFAQ一覧をもう一度AIに渡して重複を統合する二段構えにすると、よりきれいにまとまります。

4. 件数はあくまで目安として扱う

AIが返す件数は概算です。正確な集計が必要なら、生成後にキーワードで元データを数え直すなど、 GAS側で裏付けを取ると信頼性が上がります。

まとめ

GASとClaude APIを組み合わせれば、スプレッドシートに溜まった問い合わせ履歴から、 よく聞かれる質問をFAQ形式に自動で束ねられます。バッチ処理とリトライで安定させ、 定期トリガーで最新化し、回答は人が確認してから公開する——この流れを押さえれば、 問い合わせ対応の工数削減とFAQページの充実を同時に進められます。

よくある質問

顧客から繰り返し寄せられる質問を人手で分類しなくても、AIが似た内容の問い合わせをまとめて「質問」と「回答」のペアに整理してくれます。サポートページのFAQ更新や、返信テンプレートの叩き台づくりにそのまま使えるため、問い合わせ対応の工数を減らせます。

数十件からでも傾向はつかめますが、精度を上げるなら100件以上をまとめて渡すのがおすすめです。1回のAPI呼び出しに入れる件数が多すぎるとトークン上限や実行時間制限に達するため、この記事のコードでは50件ずつのバッチに分けて渡し、結果を後で統合しています。

そのまま公開するのは避けてください。AIの回答はあくまで下書きです。料金・仕様・納期など事実にかかわる部分は必ず担当者が確認し、自社の正しい情報に直してから公開する運用を前提にしています。この記事のコードも、生成結果を確認用シートに書き出す形にしています。

プロンプトで「意味が近い質問は1つのFAQにまとめる」「1つのFAQには最も代表的な質問文を1つだけ書く」と明示すると安定します。それでも細かく分かれる場合は、生成したFAQをもう一度AIに渡して重複を統合させる二段構えにすると、さらにきれいにまとまります。

できます。時間主導トリガー(ScriptApp.newTrigger)で、たとえば毎週月曜の朝にFAQ生成関数を実行するよう設定すれば、直近の問い合わせ傾向を反映したFAQへ自動で更新できます。新しい質問が増える時期の変化も追いやすくなります。

AI×GASで問い合わせ対応を
自動化する。

FAQ自動生成のほかにも、問い合わせの自動分類・返信下書き・緊急度判定など、Claude APIとGASを組み合わせた業務自動化をご相談いただけます。

AI×GAS自動化を見る