AIに問い合わせ返信の下書きを作らせるとき、回答文だけを保存しても「どのFAQを根拠にしたか」「そのFAQは現在も有効か」を確認できません。
この記事では、GoogleスプレッドシートをFAQ出典レジストリにし、Google Apps Script(GAS)で 承認済み・期限内・同じカテゴリ のFAQだけを抽出する最小構成を作ります。AI APIへの接続や返信の自動送信は行いません。
この記事で作るもの
次の2枚のシートを使います。
-
FAQ_SOURCES: FAQの版、状態、期限、エスカレーション条件を管理 -
DRAFT_LOG: AI下書きが参照したFAQ IDと人間の確認結果を記録
処理の境界は次の通りです。
- 問い合わせをカテゴリへ分類する
- 利用可能なFAQだけを抽出する
- AIへ渡すデータを組み立てる
- 下書きと根拠IDを保存する
- 人間が根拠、個別事実、約束内容を確認する
- 承認後に別の送信処理へ渡す
このコードは5までを支援し、送信は実装しません。
FAQ_SOURCESシートを作る
1行目に次の見出しを置きます。
source_id | version | status | category | question | answer | source_url | approved_by | reviewed_at | review_due_at | escalation_tags
サンプル行です。実在顧客の情報は入れません。
FAQ-BILLING-012 | 3 | approved | billing | 支払方法を変更できますか | 管理画面から変更できます | https://intranet.example/faq/billing-012 | cs_owner | 2026-07-01 | 2026-10-01 | contract,refund
status は少なくとも draft、approved、retired を使い分けます。AIが利用できるのは approved だけです。
DRAFT_LOGシートを作る
下書き本文だけではなく、根拠と確認状態を残します。
draft_id | created_at | inquiry_ref | category | source_refs_json | unanswered_points_json | escalation_reason | review_status | reviewed_by | reviewed_at
問い合わせ原文や氏名、メールアドレスをログへ複製しないでください。inquiry_ref からアクセス制御された問い合わせ管理の正本を参照します。
GASで利用可能なFAQだけを抽出する
スプレッドシートに紐づくApps Scriptへ次のコードを追加します。
const SOURCE_SHEET = 'FAQ_SOURCES';
const DRAFT_LOG_SHEET = 'DRAFT_LOG';
function rowsToObjects_(sheet) {
const values = sheet.getDataRange().getDisplayValues();
if (values.length < 2) return [];
const headers = values.shift();
return values
.filter((row) => row.some((cell) => cell !== ''))
.map((row) => Object.fromEntries(
headers.map((header, index) => [header, row[index]])
));
}
function getUsableSources(category, now = new Date()) {
const sheet = SpreadsheetApp.getActive().getSheetByName(SOURCE_SHEET);
if (!sheet) throw new Error(`${SOURCE_SHEET} sheet not found`);
return rowsToObjects_(sheet).filter((source) => {
if (source.status !== 'approved') return false;
if (source.category !== category) return false;
const dueAt = new Date(`${source.review_due_at}T23:59:59+09:00`);
return !Number.isNaN(dueAt.getTime()) && dueAt >= now;
});
}
期限が空欄または不正なFAQも除外します。「期限がないから利用可能」と暗黙に判断しないためです。
AIへ渡す入力を最小化する
抽出結果から必要な項目だけを選びます。
function buildDraftRequest(inquiry) {
const sources = getUsableSources(inquiry.category);
return {
inquiry: {
inquiry_ref: inquiry.inquiry_ref,
category: inquiry.category,
sanitized_text: inquiry.sanitized_text,
},
allowed_sources: sources.map((source) => ({
source_id: source.source_id,
version: Number(source.version),
question: source.question,
answer: source.answer,
escalation_tags: source.escalation_tags
.split(',')
.map((tag) => tag.trim())
.filter(Boolean),
})),
required_output: {
draft: 'string',
source_refs: [{ source_id: 'string', version: 'number' }],
unanswered_points: ['string'],
escalation_reason: 'string|null',
},
instructions: [
'allowed_sourcesだけを根拠にする',
'根拠がない内容を推測しない',
'根拠がない点はunanswered_pointsへ入れる',
'契約、返金、苦情、金額は人間確認へ戻す',
'送信判断をしない',
],
};
}
sanitized_text は、必要性を確認して個人情報や秘密情報を除いた本文です。除去処理をAI任せにせず、入力前の工程で行います。
下書きと根拠を同時に記録する
AIから構造化された結果を受け取った後、送信前の保留状態で記録します。
function appendDraftLog(inquiry, result) {
const sheet = SpreadsheetApp.getActive().getSheetByName(DRAFT_LOG_SHEET);
if (!sheet) throw new Error(`${DRAFT_LOG_SHEET} sheet not found`);
const draftId = `DRAFT-${Utilities.getUuid()}`;
sheet.appendRow([
draftId,
new Date(),
inquiry.inquiry_ref,
inquiry.category,
JSON.stringify(result.source_refs || []),
JSON.stringify(result.unanswered_points || []),
result.escalation_reason || '',
'pending',
'',
'',
]);
return draftId;
}
返信本文の保存が必要なら、アクセス権と保存期間を決めた別シートまたは問い合わせ管理へ置きます。根拠ログを共有しやすいという理由で顧客情報まで広く共有しないようにします。
期限切れFAQを毎日検出する
時間主導トリガーから実行する関数です。自動で retired にせず、担当者が内容を確認します。
function listExpiredSources() {
const sheet = SpreadsheetApp.getActive().getSheetByName(SOURCE_SHEET);
const today = new Date();
const expired = rowsToObjects_(sheet).filter((source) => {
if (source.status !== 'approved') return false;
const dueAt = new Date(`${source.review_due_at}T23:59:59+09:00`);
return Number.isNaN(dueAt.getTime()) || dueAt < today;
});
console.log(JSON.stringify(expired.map((source) => ({
source_id: source.source_id,
version: source.version,
review_due_at: source.review_due_at,
approved_by: source.approved_by,
}))));
return expired;
}
トリガーはApps Scriptの「トリガー」から1日1回で設定できます。ログだけで終わらせず、運用では担当者の確認キューへ追加してください。
動作確認
次の順番で確認します。
-
approvedかつ同カテゴリかつ期限内のFAQだけ返る -
draftとretiredは返らない - 期限切れと期限不正のFAQは返らない
-
AI出力に
source_refsとunanswered_pointsがある - 根拠のない質問をAIが補完しない
-
下書きログは
pendingで作られる - 契約、返金、苦情、金額は人間確認へ戻る
- 人間の承認前に送信処理が動かない
- 問い合わせ原文や個人情報をログへ複製していない
テストでは、期限内、期限切れ、未承認、カテゴリ違いの4行を用意すると抽出条件を確認しやすくなります。
この構成で自動化しないこと
この例は次を自動化しません。
- AIによる返信の直接送信
- FAQの自動承認
- 契約、返金、苦情、金額の判断
- 個人情報をAIへ送ってよいかの最終判断
- 期限切れFAQの自動削除
最初は問い合わせが多く回答が定型な1カテゴリ、FAQ 5〜10件から始めます。1週間は全件を人間が確認し、根拠不足をFAQへ、分類ミスをカテゴリルールへ戻します。
Miraigentでは、FAQの量だけでなく、AIに渡す情報、根拠、期限、人間確認、判断ログが一つの運用としてつながっているかを無料診断で整理しています。
