第8章(応用編):AIとGoogle Apps Scriptで問い合わせ対応フローを自動化する方法
■はじめに
第7章では、AIとFAQデータを連携し、問い合わせ内容に合う回答案を自動生成する方法を整理しました。
第7章(応用編):AIとFAQデータを連携して回答案を自動生成する方法
第7章では、FAQデータを根拠としてAIに渡し、問い合わせ対応に使える回答案を作る流れを作りました。この第8章では、その考え方をGoogle Apps Scriptと組み合わせて、問い合わせ対応フローとして動かせる形にします。
Google Apps Scriptは、Googleスプレッドシート、Gmail、Googleフォームなどと連携しやすい仕組みです。そのため、問い合わせ管理表を中心にした小さな業務自動化を作るには相性が良いです。
今回作るのは、問い合わせ管理表に登録された問い合わせをGoogle Apps Scriptで処理し、FAQ検索やAI回答案作成につなげる仕組みです。ただし、ここでもいきなり自動返信はしません。AIが回答案を作り、担当者が確認し、必要に応じて修正し、最後に人間が送信する形を前提にします。
■関連章
この章は、以下の章とつながっています。
第1章(応用編):AIとAPIで問い合わせメールを管理表へ自動登録する方法
第2章(応用編):AIとGoogle Sheets APIで問い合わせ管理表を作る方法
第3章(応用編):AI分類結果をGoogle Sheets APIで管理表へ自動登録する方法
第4章(応用編):AIとSlack APIで担当者通知を自動化する方法
第5章(応用編):AIとGoogle Drive APIで議事録・資料整理を自動化する方法
第6章(応用編):AIとJira APIで過去チケット検索を自動化する方法
第7章(応用編):AIとFAQデータを連携して回答案を自動生成する方法
第7章が「FAQをもとに回答案を作る部品」だとすれば、第8章は「その部品を問い合わせ対応フローとして動かす仕組み」です。つまり、第7章では回答案生成の考え方を作り、第8章ではそれをGoogleスプレッドシートとGoogle Apps Scriptで実行できる形にします。
■この章で作るもの
この章で作るのは、Googleスプレッドシートを中心にした問い合わせ対応フローです。
1. 問い合わせ管理表に問い合わせ内容を登録する
2. Google Apps Scriptが未処理の問い合わせを取得する
3. FAQ管理表から関連FAQを探す
4. AIに渡すプロンプトを作る
5. 回答案を管理表へ記録する
6. 担当者が内容を確認するこの仕組みを作ると、担当者は毎回ゼロからFAQを探して文章を書く必要がなくなります。問い合わせ管理表を開くと、AIが作成した回答案が入っている。担当者はそれを確認して、必要に応じて修正する。この状態を目指します。
■なぜGoogle Apps Scriptを使うのか
問い合わせ対応の自動化では、いきなり本格的なWebアプリやサーバーを作ろうとすると大変です。サーバー環境、認証、権限管理、運用コスト、担当者に見せる画面などを考える必要があります。
しかし、最初の自動化では、そこまで大きな仕組みにする必要はありません。Googleスプレッドシートを管理表にして、Google Apps Scriptで処理するだけでも、実務に近い形を作れます。
Google Apps Scriptを使うメリットは以下です。
Googleスプレッドシートと連携しやすい
GmailやGoogleフォームとも連携しやすい
小さな自動化を作りやすい
サーバーを用意しなくても動かせる
業務担当者にも見せやすい特に、問い合わせ対応のように「管理表」が中心になる業務では、Google Apps Scriptはかなり使いやすいです。
■全体の流れ
今回の全体像は以下です。
問い合わせ管理表
↓
Google Apps Script
↓
FAQ管理表を検索
↓
AI用プロンプトを作成
↓
AI回答案を作成
↓
問い合わせ管理表へ書き戻し
↓
担当者が確認ポイントは、Googleスプレッドシートを中心にすることです。問い合わせも管理表で見える。FAQも管理表で更新できる。AIの回答案も管理表に残る。処理状況もstatusで確認できる。このようにすると、業務担当者にも分かりやすい仕組みになります。
AI自動化で大事なのは、裏側で勝手に動かすことではありません。何が処理されているか、どこまで進んでいるか、どこで人間が確認するかが見えることです。
■ここから解説する内容
問い合わせ管理表の設計
FAQ管理表の設計
シートデータを取得するコード
未処理問い合わせを抽出するコード
FAQ検索と信頼度判定のコード
AIプロンプトを作成するコード
管理表へ回答案を書き戻すコード
完成版Google Apps Scriptコード
トリガー設定と運用時の注意点
この章の有料部分を使うと、Googleスプレッドシートを中心にした問い合わせ対応フローの基本形を作れます。問い合わせを管理表に入れる、FAQを検索する、AI回答案を作る、管理表へ戻す、担当者が確認する。この流れをGoogle Apps Scriptで動かせる形にします。
■問い合わせ管理表の設計
まず、問い合わせ管理表を作成します。シート名は inquiries とします。
1行目には、以下のヘッダーを入れます。
id
received_at
customer_name
email
message
status
category
confidence
draft_reply
checked_by
updated_atそれぞれの意味は以下です。
id:問い合わせID
received_at:受付日時
customer_name:お客様名
email:メールアドレス
message:問い合わせ本文
status:処理状況
category:問い合わせカテゴリ
confidence:AI回答案の信頼度
draft_reply:AIが作成した回答案
checked_by:確認担当者
updated_at:更新日時サンプルデータは以下です。
id | received_at | customer_name | email | message | status | category | confidence | draft_reply | checked_by | updated_at
1 | 2026-05-01 10:00 | 山田太郎 | yamada@example.com | ログインできません。パスワードを忘れました。 | new | | | | |
2 | 2026-05-01 10:05 | 佐藤花子 | sato@example.com | 領収書は発行できますか? | new | | | | |
3 | 2026-05-01 10:10 | 鈴木一郎 | suzuki@example.com | 契約を止めたいです。 | new | | | | |重要なのは status です。Google Apps Scriptでは、status が new の行だけを処理対象にします。これにより、同じ問い合わせを何度も処理してしまうことを防げます。
処理後は、AIが回答案を作成できた場合は draft_created、FAQが見つからない場合は needs_review、処理に失敗した場合は error にします。
■FAQ管理表の設計
次に、FAQ管理表を作成します。シート名は faq とします。
1行目には、以下のヘッダーを入れます。
id
category
keyword
question
answer
status
updated_atサンプルデータは以下です。
id | category | keyword | question | answer | status | updated_at
1 | login | ログイン,パスワード,サインイン | パスワードを忘れた場合はどうすればよいですか? | ログイン画面の「パスワードをお忘れですか」から再設定してください。 | active | 2026-05-01
2 | payment | 領収書,支払い,決済,請求 | 領収書は発行できますか? | マイページの購入履歴から領収書をダウンロードできます。 | active | 2026-05-01
3 | account | メールアドレス,アカウント,変更 | メールアドレスを変更できますか? | アカウント設定画面からメールアドレスを変更できます。 | active | 2026-05-01
4 | cancel | 解約,キャンセル,退会,契約を止めたい | 解約方法を教えてください。 | マイページの契約情報から解約手続きができます。 | active | 2026-05-01keyword は、問い合わせ文とFAQを結びつけるために使います。お客様は、FAQの質問文と同じ言葉で問い合わせるとは限りません。たとえば解約について聞きたい場合でも、「退会したいです」「契約を止めたいです」「キャンセルできますか」と書かれることがあります。
このような言葉を keyword に入れておくと、FAQ検索の精度が上がります。また、status が active のFAQだけをAI処理の対象にします。古くなったFAQや一時的に使わないFAQは、削除せず inactive にしておくと管理しやすいです。
■シートデータを取得するコード
ここからGoogle Apps Scriptのコードを作ります。まず、シートの内容を扱いやすい形に変換する関数です。
function getSheetData(sheet) {
const values = sheet.getDataRange().getValues();
if (values.length < 2) {
return [];
}
const headers = values[0];
const rows = values.slice(1);
return rows.map((row, index) => {
const item = {};
headers.forEach((header, colIndex) => {
item[header] = row[colIndex];
});
item.rowNumber = index + 2;
return item;
});
}この関数では、1行目をヘッダーとして扱います。たとえば、問い合わせ管理表に message という列があれば、コード上では inquiry.message として扱えます。
rowNumber を持たせているのは、あとで該当行を更新するためです。スプレッドシートは2行目からデータが始まるため、index + 2 としています。
■未処理問い合わせを抽出するコード
次に、問い合わせ管理表から未処理の問い合わせだけを抽出します。ここでは、status が new の行だけを処理対象にします。
function getNewInquiries(inquiries) {
return inquiries.filter(inquiry => {
return inquiry.status === "new";
});
}使い方は以下です。
const inquiries = getSheetData(inquirySheet);
const newInquiries = getNewInquiries(inquiries);このようにしておくと、過去に処理済みの問い合わせを再処理しなくて済みます。AI自動化では、同じ行を何度も処理してしまうと、回答案が上書きされたり、通知が何度も飛んだりします。そのため、必ず処理対象を絞ります。
■FAQ検索と信頼度判定のコード
次に、問い合わせ本文からFAQ候補を探します。
function searchFaq(message, faqs) {
const matchedFaqs = [];
faqs.forEach(faq => {
if (faq.status !== "active") {
return;
}
const keywords = String(faq.keyword).split(",");
const isMatched = keywords.some(keyword => {
const trimmedKeyword = keyword.trim();
if (trimmedKeyword === "") {
return false;
}
return String(message).includes(trimmedKeyword);
});
if (isMatched) {
matchedFaqs.push(faq);
}
});
return matchedFaqs;
}このコードでは、FAQ管理表の keyword をカンマ区切りで分解しています。たとえば、keyword が ログイン,パスワード,サインイン の場合、問い合わせ本文にこの中のどれかが含まれていれば、関連FAQとして扱います。
次に、FAQのヒット件数から信頼度を判定します。
function judgeConfidence(matchedFaqs) {
if (matchedFaqs.length === 0) {
return "low";
}
if (matchedFaqs.length === 1) {
return "high";
}
return "medium";
}信頼度の考え方は以下です。
high:FAQが1件だけ見つかった
medium:FAQが複数見つかった
low:FAQが見つからなかったhigh の場合は、回答案を作りやすいです。medium の場合は、複数FAQの内容が混ざる可能性があります。low の場合は、AIに回答させず担当者確認に回します。
■AIプロンプトを作成するコード
FAQ候補が見つかったら、AIに渡すプロンプトを作成します。
function createPrompt(inquiry, matchedFaqs, confidence) {
const faqText = matchedFaqs.map(faq => {
return [
`FAQ ID: ${faq.id}`,
`カテゴリ: ${faq.category}`,
`Q: ${faq.question}`,
`A: ${faq.answer}`,
`更新日: ${faq.updated_at}`
].join("\n");
}).join("\n\n");
return `
あなたは問い合わせ対応担当者です。
以下のFAQ情報を根拠に、お客様への回答案を作成してください。
【重要ルール】
・FAQに書かれている内容を最優先してください
・FAQにない内容を断定しないでください
・推測で手順を追加しないでください
・複数FAQがある場合は、問い合わせ内容に最も近いFAQを中心にしてください
・判断が難しい場合は、担当者確認が必要な旨を含めてください
・丁寧で分かりやすい文章にしてください
・最後に「ご確認のほどよろしくお願いいたします。」を入れてください
【信頼度】
${confidence}
【お客様からの問い合わせ】
${inquiry.message}
【関連FAQ】
${faqText}
【回答案】
`;
}このプロンプトでは、AIに問い合わせ本文だけを渡していません。FAQ情報を一緒に渡し、FAQを根拠に回答案を作るようにしています。
また、FAQに書かれている内容を最優先する、FAQにない内容を断定しない、推測で手順を追加しない、判断が難しい場合は担当者確認にする、という安全ルールを入れています。
業務でAIを使う場合、プロンプトには必ず安全ルールを入れます。「いい感じに返信文を作って」では危険です。どの情報を根拠にするのか、どこから先は断定してはいけないのかを明確にすることで、実務で使いやすい回答案になります。
■管理表へ回答案を書き戻すコード
次に、AIが作成した回答案や処理結果を問い合わせ管理表へ書き戻します。
function updateInquiryResult(sheet, rowNumber, result) {
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const statusCol = headers.indexOf("status") + 1;
const categoryCol = headers.indexOf("category") + 1;
const confidenceCol = headers.indexOf("confidence") + 1;
const draftReplyCol = headers.indexOf("draft_reply") + 1;
const updatedAtCol = headers.indexOf("updated_at") + 1;
if (statusCol > 0) {
sheet.getRange(rowNumber, statusCol).setValue(result.status || "");
}
if (categoryCol > 0) {
sheet.getRange(rowNumber, categoryCol).setValue(result.category || "");
}
if (confidenceCol > 0) {
sheet.getRange(rowNumber, confidenceCol).setValue(result.confidence || "");
}
if (draftReplyCol > 0) {
sheet.getRange(rowNumber, draftReplyCol).setValue(result.draft_reply || "");
}
if (updatedAtCol > 0) {
sheet.getRange(rowNumber, updatedAtCol).setValue(new Date());
}
}このコードでは、列番号を固定していません。ヘッダー名から列番号を探しています。そのため、列の順番を少し変えても壊れにくいです。
ただし、ヘッダー名はコードと一致させる必要があります。たとえば、draft_reply を draft reply に変えると、コード側で見つけられません。列名は固定ルールとして扱います。
■AI回答案を作る関数
ここでは、説明を分かりやすくするため、AI APIの呼び出し部分を関数化します。
function generateDraftReply(prompt) {
// 実務では、ここでAI APIを呼び出します。
// この章では処理の流れを確認するため、仮の回答案を返します。
Logger.log(prompt);
return "お問い合わせありがとうございます。\n\nご提示いただいた内容について、FAQ情報をもとに確認いたしました。\n\n該当する手順に沿ってご対応をお願いいたします。\n\nご確認のほどよろしくお願いいたします。";
}実務では、この関数の中でAI APIを呼び出します。ただし、最初からAI API接続まで入れると、確認するポイントが増えます。
まずは、問い合わせを取得する、FAQを検索する、プロンプトを作る、回答案を管理表に書き込む、statusを更新する、という流れが動くことを確認します。ここまで動けば、あとからAI API接続を追加できます。
■完成版Google Apps Scriptコード
ここまでの処理をまとめた完成版コードです。
function processInquiries() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const inquirySheet = spreadsheet.getSheetByName("inquiries");
const faqSheet = spreadsheet.getSheetByName("faq");
if (!inquirySheet || !faqSheet) {
throw new Error("必要なシートが見つかりません。inquiries と faq を作成してください。");
}
const inquiries = getSheetData(inquirySheet);
const faqs = getSheetData(faqSheet);
const newInquiries = getNewInquiries(inquiries);
newInquiries.forEach(inquiry => {
try {
const matchedFaqs = searchFaq(String(inquiry.message), faqs);
const confidence = judgeConfidence(matchedFaqs);
if (confidence === "low") {
updateInquiryResult(inquirySheet, inquiry.rowNumber, {
status: "needs_review",
confidence: confidence,
draft_reply: "関連FAQが見つかりませんでした。担当者確認が必要です。"
});
return;
}
const prompt = createPrompt(inquiry, matchedFaqs, confidence);
const draftReply = generateDraftReply(prompt);
updateInquiryResult(inquirySheet, inquiry.rowNumber, {
status: "draft_created",
category: matchedFaqs[0].category,
confidence: confidence,
draft_reply: draftReply
});
} catch (error) {
updateInquiryResult(inquirySheet, inquiry.rowNumber, {
status: "error",
confidence: "",
draft_reply: "処理中にエラーが発生しました:" + error.message
});
}
});
}
function getSheetData(sheet) {
const values = sheet.getDataRange().getValues();
if (values.length < 2) {
return [];
}
const headers = values[0];
const rows = values.slice(1);
return rows.map((row, index) => {
const item = {};
headers.forEach((header, colIndex) => {
item[header] = row[colIndex];
});
item.rowNumber = index + 2;
return item;
});
}
function getNewInquiries(inquiries) {
return inquiries.filter(inquiry => {
return inquiry.status === "new";
});
}
function searchFaq(message, faqs) {
const matchedFaqs = [];
faqs.forEach(faq => {
if (faq.status !== "active") {
return;
}
const keywords = String(faq.keyword).split(",");
const isMatched = keywords.some(keyword => {
const trimmedKeyword = keyword.trim();
if (trimmedKeyword === "") {
return false;
}
return String(message).includes(trimmedKeyword);
});
if (isMatched) {
matchedFaqs.push(faq);
}
});
return matchedFaqs;
}
function judgeConfidence(matchedFaqs) {
if (matchedFaqs.length === 0) {
return "low";
}
if (matchedFaqs.length === 1) {
return "high";
}
return "medium";
}
function createPrompt(inquiry, matchedFaqs, confidence) {
const faqText = matchedFaqs.map(faq => {
return [
`FAQ ID: ${faq.id}`,
`カテゴリ: ${faq.category}`,
`Q: ${faq.question}`,
`A: ${faq.answer}`,
`更新日: ${faq.updated_at}`
].join("\n");
}).join("\n\n");
return `
あなたは問い合わせ対応担当者です。
以下のFAQ情報を根拠に、お客様への回答案を作成してください。
【重要ルール】
・FAQに書かれている内容を最優先してください
・FAQにない内容を断定しないでください
・推測で手順を追加しないでください
・複数FAQがある場合は、問い合わせ内容に最も近いFAQを中心にしてください
・判断が難しい場合は、担当者確認が必要な旨を含めてください
・丁寧で分かりやすい文章にしてください
・最後に「ご確認のほどよろしくお願いいたします。」を入れてください
【信頼度】
${confidence}
【お客様からの問い合わせ】
${inquiry.message}
【関連FAQ】
${faqText}
【回答案】
`;
}
function generateDraftReply(prompt) {
Logger.log(prompt);
return "お問い合わせありがとうございます。\n\nご提示いただいた内容について、FAQ情報をもとに確認いたしました。\n\n該当する手順に沿ってご対応をお願いいたします。\n\nご確認のほどよろしくお願いいたします。";
}
function updateInquiryResult(sheet, rowNumber, result) {
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
const statusCol = headers.indexOf("status") + 1;
const categoryCol = headers.indexOf("category") + 1;
const confidenceCol = headers.indexOf("confidence") + 1;
const draftReplyCol = headers.indexOf("draft_reply") + 1;
const updatedAtCol = headers.indexOf("updated_at") + 1;
if (statusCol > 0) {
sheet.getRange(rowNumber, statusCol).setValue(result.status || "");
}
if (categoryCol > 0) {
sheet.getRange(rowNumber, categoryCol).setValue(result.category || "");
}
if (confidenceCol > 0) {
sheet.getRange(rowNumber, confidenceCol).setValue(result.confidence || "");
}
if (draftReplyCol > 0) {
sheet.getRange(rowNumber, draftReplyCol).setValue(result.draft_reply || "");
}
if (updatedAtCol > 0) {
sheet.getRange(rowNumber, updatedAtCol).setValue(new Date());
}
}このコードで、inquiries シートから新規問い合わせを取得し、faq シートからFAQデータを取得し、問い合わせ本文からFAQ候補を探し、信頼度を判定し、AI用プロンプトを作成し、回答案を管理表へ書き戻せます。
この時点では、AI APIの本番接続は入れていません。まずは、管理表を中心にした問い合わせ対応フローが動くことを確認します。その後で、generateDraftReply() の中身をAI API接続に置き換えると実務に近づきます。
■トリガー設定と運用時の注意点
Google Apps Scriptは、手動実行だけでなく、時間指定で自動実行できます。最初は手動実行でテストします。問題なく動いたら、トリガーを設定します。
設定例は以下です。
実行する関数:processInquiries
イベントのソース:時間主導型
時間ベースのトリガー:分ベースのタイマー
間隔:10分おきただし、最初から短い間隔で動かす必要はありません。最初は1時間ごとでも十分です。
運用時の注意点は以下です。
AI回答案をそのまま自動送信しない
FAQが古くないか確認する
statusがnewの行だけ処理する
エラー時はerrorとして見えるようにする
FAQが見つからない場合はneeds_reviewにする
複数FAQが見つかった場合は担当者確認を強めるこの仕組みは、自動返信システムではありません。回答案作成を自動化する仕組みです。最初は、AIが回答案を作り、担当者が確認し、必要に応じて修正してから送信する形にします。この方が安全で、実務にも導入しやすいです。
■この章のまとめ
この章では、AIとGoogle Apps Scriptを使って、問い合わせ対応フローを自動化する方法を整理しました。ポイントは、Googleスプレッドシートを中心にすることです。
問い合わせ管理表を作る。FAQ管理表を作る。Google Apps Scriptで未処理の問い合わせを取得する。FAQを検索する。AIに渡すプロンプトを作る。回答案を管理表へ記録する。この流れを作ることで、問い合わせ対応の初動を効率化できます。
ただし、最初から自動返信にする必要はありません。まずは回答案を自動作成し、担当者が確認する形にします。この方が安全で、実務にも導入しやすいです。
第9章では、この流れをさらに進めて、Webhookを使ったリアルタイム業務処理の自動化に進みます。
■次回
第9章(応用編):AIとWebhookでリアルタイム業務処理を自動化する方法
