問い合わせ対応やFAQ整備にAIを使う時、すべてのメモをAIへ渡すと危険です。
一方で、慎重になりすぎて何も渡さないと、AI返信下書き、FAQ候補作成、CRM要約の精度が上がりません。
この記事では、GoogleスプレッドシートとGASで、社内ナレッジを次の3つへ振り分ける最小実装を作ります。
-
ai_context: AIへ渡してよい要約情報 -
internal_only: 社内だけで見る情報 -
review_required: 人間確認が必要な情報
AI APIの呼び出しは扱いません。AIへ送る前に、どの情報をどの箱へ入れるかを決める前処理に絞ります。
作るもの
スプレッドシートに次の4枚を作ります。
| シート | 役割 |
|---|---|
knowledge_inbox |
問い合わせメモ、FAQメモ、CRMメモを受け取る |
ai_context |
AIへ渡してよい、マスク済みの短い文だけを置く |
internal_only |
顧客名、契約、金額、社内判断などを残す |
review_required |
判断に迷う情報を人間確認へ回す |
全体像は次の流れです。
knowledge_inbox
↓
GASでリスク語と用途を判定
↓
ai_context / internal_only / review_required へ振り分け
↓
AI返信下書き、FAQ候補、CRM更新の前段で使う
ポイントは、AIへ渡す文章を「原文」ではなく「短い要約」にすることです。
knowledge_inbox の列
まず、元メモを受け取るシートを作ります。
| 列名 | 例 | 用途 |
|---|---|---|
| item_id | KNO-20260709-001 | メモID |
| received_at | 2026/07/09 11:20 | 受付時刻 |
| source_type | inquiry / faq / crm / meeting | どこから来た情報か |
| source_id | INQ-018 | 元データへ戻るID |
| purpose | reply_draft / faq_update / crm_summary | 使いたい目的 |
| raw_text | 問い合わせ本文や社内メモ | 元の文章 |
| processed | 空欄 / yes | 処理済み管理 |
raw_text には個人情報や社内判断が混じる前提で扱います。
振り分けルール
最小ルールは次の3分類です。
| 分類 | 条件 | 例 |
|---|---|---|
ai_context |
個人情報、契約、金額、秘密情報がなく、AIに渡してよい | FAQ化したい一般質問 |
internal_only |
社内だけで見るべき情報 | 顧客名、電話番号、契約条件、社内判断 |
review_required |
自動判定だけでは迷う情報 | 返金、苦情、権限不足、公開可否不明 |
運用上は、review_required が増えることを失敗と見なさない方が安全です。
止めた理由が分かれば、後からFAQ、フォーム、承認ルールを改善できます。
GAS実装
スプレッドシートに紐づくApps Scriptへ、次のコードを貼り付けます。
const SHEET_NAMES = {
inbox: 'knowledge_inbox',
ai: 'ai_context',
internal: 'internal_only',
review: 'review_required',
};
const INBOX_HEADERS = [
'item_id',
'received_at',
'source_type',
'source_id',
'purpose',
'raw_text',
'processed',
];
const ROUTED_HEADERS = [
'routed_at',
'item_id',
'source_type',
'source_id',
'purpose',
'route',
'reason',
'safe_summary',
'next_action',
];
function setupKnowledgeRoutingSheets() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
ensureSheet_(ss, SHEET_NAMES.inbox, INBOX_HEADERS);
ensureSheet_(ss, SHEET_NAMES.ai, ROUTED_HEADERS);
ensureSheet_(ss, SHEET_NAMES.internal, ROUTED_HEADERS);
ensureSheet_(ss, SHEET_NAMES.review, ROUTED_HEADERS);
}
function routeKnowledgeInbox() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const inbox = ss.getSheetByName(SHEET_NAMES.inbox);
if (!inbox) throw new Error('setupKnowledgeRoutingSheets() を先に実行してください');
const values = inbox.getDataRange().getValues();
const headers = values.shift();
const col = buildColumnMap_(headers);
values.forEach((row, index) => {
const sheetRow = index + 2;
const processed = String(row[col.processed] || '').toLowerCase();
if (processed === 'yes') return;
const itemId = String(row[col.item_id] || '').trim();
const rawText = String(row[col.raw_text] || '').trim();
if (!itemId || !rawText) return;
const result = decideRoute_(rawText, row[col.purpose]);
const targetSheet = ss.getSheetByName(result.sheet);
targetSheet.appendRow([
new Date(),
itemId,
row[col.source_type],
row[col.source_id],
row[col.purpose],
result.route,
result.reason,
createSafeSummary_(rawText),
result.nextAction,
]);
inbox.getRange(sheetRow, col.processed + 1).setValue('yes');
});
}
function decideRoute_(text, purpose) {
const flags = detectSensitiveFlags_(text);
const normalizedPurpose = String(purpose || '').toLowerCase();
if (flags.includes('secret') || flags.includes('personal_data')) {
return {
sheet: SHEET_NAMES.internal,
route: 'internal_only',
reason: flags.join(','),
nextAction: '社内記録として保持し、AI送信対象から外す',
};
}
if (flags.includes('money_or_contract') || flags.includes('complaint')) {
return {
sheet: SHEET_NAMES.review,
route: 'review_required',
reason: flags.join(','),
nextAction: '責任者が確認してから要約可否を決める',
};
}
if (!['reply_draft', 'faq_update', 'crm_summary'].includes(normalizedPurpose)) {
return {
sheet: SHEET_NAMES.review,
route: 'review_required',
reason: 'unknown_purpose',
nextAction: '用途を確認してから振り分ける',
};
}
return {
sheet: SHEET_NAMES.ai,
route: 'ai_context',
reason: 'safe_general_context',
nextAction: 'AIへ渡す前に担当者が要約を確認する',
};
}
function detectSensitiveFlags_(text) {
const flags = [];
const body = String(text || '');
if (/電話番号|住所|メールアドレス|氏名|本人確認|個人情報/.test(body)) {
flags.push('personal_data');
}
if (/契約|返金|請求|支払い|料金|見積|解約/.test(body)) {
flags.push('money_or_contract');
}
if (/苦情|クレーム|炎上|訴訟|法務|違反/.test(body)) {
flags.push('complaint');
}
if (/パスワード|APIキー|トークン|秘密鍵|認証情報/.test(body)) {
flags.push('secret');
}
return flags;
}
function createSafeSummary_(text) {
return String(text || '')
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[email]')
.replace(/\d{2,4}-\d{2,4}-\d{3,4}/g, '[phone]')
.replace(/\s+/g, ' ')
.slice(0, 180);
}
function ensureSheet_(ss, name, headers) {
const sheet = ss.getSheetByName(name) || ss.insertSheet(name);
const current = sheet.getRange(1, 1, 1, headers.length).getValues()[0];
if (current.every((value) => value === '')) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.setFrozenRows(1);
}
}
function buildColumnMap_(headers) {
return Object.fromEntries(headers.map((header, index) => [String(header), index]));
}
動作確認用データ
knowledge_inbox に次のような行を入れて、routeKnowledgeInbox() を実行します。
| item_id | source_type | source_id | purpose | raw_text |
|---|---|---|---|---|
| KNO-001 | inquiry | INQ-001 | faq_update | 料金プランの違いを知りたいです |
| KNO-002 | crm | CRM-014 | crm_summary | 山田太郎さんの電話番号は090-0000-0000です |
| KNO-003 | inquiry | INQ-022 | reply_draft | 返金条件について確認したいです |
期待結果は次の通りです。
| item_id | 振り分け先 | 理由 |
|---|---|---|
| KNO-001 | ai_context |
一般的なFAQ候補 |
| KNO-002 | internal_only |
個人情報を含む |
| KNO-003 | review_required |
返金、契約に関わる |
AIへ渡す前のチェックリスト
実装した後は、次のチェックを通してからAI連携へ進みます。
-
ai_contextに原文の個人情報が残っていない -
internal_onlyをAIプロンプトに混ぜない -
review_requiredを自動返信に進めない - 振り分け理由が
reasonに残っている - 用途不明のメモは止まる
- 週次で
review_requiredの理由を集計する
よくある調整
最初は正規表現だけでも十分ですが、運用が続くと次の調整が必要になります。
| 課題 | 調整 |
|---|---|
| 止まりすぎる |
review_required の理由を見て、一般質問へ戻せる条件を増やす |
| 通しすぎる |
detectSensitiveFlags_() に禁止語を追加する |
| 要約が長い |
createSafeSummary_() の文字数を短くする |
| 用途が増える |
reply_draft などの許可用途を設定シート化する |
ルールは一度で完成させるものではありません。
止めた理由を残して、次の改善先へ戻すことが重要です。
Miraigentで確認していること
MiraigentのAI導入相談では、ツール選定の前に「AIへ渡す情報」と「社内だけで残す情報」を分けます。
特に、問い合わせ対応、無料診断、CRMメモでは、個人情報、契約、返金、苦情、認証情報を同じ扱いにしないことが大切です。
無料診断でも、まずはフォーム項目、FAQ、CRMメモ、承認ルールを見て、AIへ渡してよい情報の境界を一緒に整理します。
