0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIに渡す情報と社内に残す情報を分けるナレッジ振り分けGAS

0
Posted at

問い合わせ対応や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 判断に迷う情報を人間確認へ回す

全体像は次の流れです。

問い合わせフォームから受付ログへ集約し、FAQ候補・CRM項目・AI利用可否を同じ行で判断する流れ

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へ渡してよい情報の境界を一緒に整理します。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?