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返信が古いFAQを使わないための鮮度台帳をGASで作る

0
Posted at

問い合わせ返信の下書きに生成AIを使うとき、検索できた文書が最新版とは限りません。旧価格表や終了済みキャンペーンのFAQが残っていれば、プロンプトに「最新情報だけを使う」と書いても誤案内を防げません。

この記事では、Googleスプレッドシートを参照元台帳、Google Apps Script(GAS)を鮮度判定の入口として使い、承認期限が切れた資料をAIへ渡さない最小構成を実装します。

AI APIやメール送信には接続しません。顧客の問い合わせ原文、個人情報、認証情報も台帳へ保存しません。AIの出力は返信下書きに限定し、最終送信前の人間確認を残します。

この記事で作るもの

次の3つを作ります。

  1. 正本URL、承認済み版、責任者、有効期限を管理する Sources シート
  2. 用途、承認状態、有効期限を下書き生成前に検査するGAS関数
  3. 期限切れと期限間近の資料を担当者向け一覧にする日次チェック

問い合わせ受付から人間承認までのレビューゲート

完成状態は、AIへ渡してよい参照元だけを取得でき、期限切れなら自動回答へ進まず、人間確認へ切り替えられることです。

シートを準備する

スプレッドシートに Sources シートを作り、1行目へ次の見出しを入れます。

source_id,title,canonical_url,owner_email,approved_version,approved_at,expires_at,status,sensitivity,allowed_tasks,fallback_action,last_checked_at

各列の役割は次の通りです。

  • source_id: 参照元の一意なID
  • canonical_url: コピーではなく、組織が正本と決めたURL
  • owner_email: 内容を再確認できる業務責任者
  • approved_version: AIへ渡してよい版
  • expires_at: 承認が失効する日時
  • status: DRAFTAPPROVEDREVIEW_REQUIREDRETIRED のいずれか
  • sensitivity: publicinternalrestricted のいずれか
  • allowed_tasks: 許可する用途。この記事では reply_draft を使う
  • fallback_action: 利用不可時の処理。例は human_review

owner_email にはAI基盤の管理者ではなく、価格、製品仕様、サポート方針など元の情報を判断できる担当者を指定します。

設定値をスクリプトプロパティへ置く

シートIDや通知開始日数をコード本文へ散らさず、スクリプトプロパティへ保存します。

function setupProperties() {
  PropertiesService.getScriptProperties().setProperties({
    SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID',
    SOURCE_SHEET_NAME: 'Sources',
    EXPIRY_WARNING_DAYS: '7'
  });
}

ここへAI APIキー、顧客情報、文書本文は保存しません。認証情報が必要な処理を後から追加する場合も、ソースコードやシートへ直接書かず、組織が承認した保管方法を使ってください。

台帳をオブジェクトとして読む

共通関数を用意します。

const SOURCE_HEADERS = [
  'source_id', 'title', 'canonical_url', 'owner_email',
  'approved_version', 'approved_at', 'expires_at', 'status',
  'sensitivity', 'allowed_tasks', 'fallback_action', 'last_checked_at'
];

function getConfig() {
  const p = PropertiesService.getScriptProperties();
  return {
    spreadsheetId: p.getProperty('SPREADSHEET_ID'),
    sheetName: p.getProperty('SOURCE_SHEET_NAME') || 'Sources',
    warningDays: Number(p.getProperty('EXPIRY_WARNING_DAYS') || 7)
  };
}

function getSourceSheet() {
  const c = getConfig();
  const sheet = SpreadsheetApp.openById(c.spreadsheetId)
    .getSheetByName(c.sheetName);
  if (!sheet) throw new Error(`シートがありません: ${c.sheetName}`);
  return sheet;
}

function getSources() {
  const values = getSourceSheet().getDataRange().getValues();
  const headers = values.shift();

  const missing = SOURCE_HEADERS.filter(name => !headers.includes(name));
  if (missing.length) throw new Error(`不足列: ${missing.join(', ')}`);

  return values
    .filter(row => row[0])
    .map((row, index) => ({
      rowNumber: index + 2,
      ...Object.fromEntries(headers.map((name, i) => [name, row[i]]))
    }));
}

必須列の欠落を最初に検出するため、列名の変更で判定がすり抜けるのを防げます。

参照元の利用可否を判定する

下書き生成の直前に、対象資料を必ず次のゲートへ通します。

function evaluateSource(source, task, now = new Date()) {
  const reasons = [];

  if (source.status !== 'APPROVED') {
    reasons.push('not_approved');
  }

  if (!source.canonical_url || !source.approved_version) {
    reasons.push('missing_canonical_or_version');
  }

  const expiresAt = new Date(source.expires_at);
  if (Number.isNaN(expiresAt.getTime())) {
    reasons.push('invalid_expiry');
  } else if (expiresAt <= now) {
    reasons.push('expired');
  }

  const tasks = String(source.allowed_tasks || '')
    .split(',')
    .map(value => value.trim())
    .filter(Boolean);
  if (!tasks.includes(task)) {
    reasons.push('task_not_allowed');
  }

  if (source.sensitivity === 'restricted') {
    reasons.push('restricted_source');
  }

  return {
    ok: reasons.length === 0,
    sourceId: source.source_id,
    reasons,
    fallbackAction: source.fallback_action || 'human_review',
    snapshot: reasons.length === 0 ? {
      sourceId: source.source_id,
      version: source.approved_version,
      approvedAt: source.approved_at,
      expiresAt: source.expires_at
    } : null
  };
}

restricted を一律拒否しているのは、このサンプルが外部AIサービスの保存条件や契約を判定できないためです。社内環境で許可する場合も、権限、保存期間、学習利用、データ所在地などを組織側で確認してから別ルールにしてください。

AIへ渡せる参照元だけを取得する

利用可能な参照元と、停止理由を分けて返します。

function selectSourcesForReplyDraft(sourceIds) {
  const requested = new Set(sourceIds);
  const sources = getSources().filter(item => requested.has(item.source_id));

  const results = sources.map(source => ({
    source,
    gate: evaluateSource(source, 'reply_draft')
  }));

  const missingIds = sourceIds.filter(
    id => !sources.some(source => source.source_id === id)
  );

  return {
    usable: results
      .filter(item => item.gate.ok)
      .map(item => ({
        sourceId: item.source.source_id,
        canonicalUrl: item.source.canonical_url,
        snapshot: item.gate.snapshot
      })),
    blocked: [
      ...results
        .filter(item => !item.gate.ok)
        .map(item => ({
          sourceId: item.source.source_id,
          reasons: item.gate.reasons,
          action: item.gate.fallbackAction
        })),
      ...missingIds.map(sourceId => ({
        sourceId,
        reasons: ['not_found'],
        action: 'human_review'
      }))
    ]
  };
}

実行例です。

function exampleSelectSources() {
  const result = selectSourcesForReplyDraft([
    'SRC-PRICE-001',
    'SRC-FAQ-014'
  ]);

  if (result.blocked.length > 0) {
    console.log(JSON.stringify(result.blocked));
    throw new Error('利用不可の参照元があります。人間確認へ切り替えます');
  }

  console.log(JSON.stringify(result.usable));
}

利用不可の資料を黙って除外して回答を続けると、必要な根拠がないまま下書きが作られる恐れがあります。対象資料の一つでも必須なら、生成処理自体を止める方が安全です。

期限切れを日次で一覧化する

期限当日に初めて気づかないよう、期限間近の資料も抽出します。

function listFreshnessAlerts(now = new Date()) {
  const c = getConfig();
  const warningAt = new Date(
    now.getTime() + c.warningDays * 24 * 60 * 60 * 1000
  );

  return getSources()
    .filter(source => source.status === 'APPROVED')
    .map(source => {
      const expiresAt = new Date(source.expires_at);
      let level = 'OK';

      if (Number.isNaN(expiresAt.getTime()) || expiresAt <= now) {
        level = 'EXPIRED';
      } else if (expiresAt <= warningAt) {
        level = 'EXPIRING_SOON';
      }

      return {
        level,
        sourceId: source.source_id,
        title: source.title,
        ownerEmail: source.owner_email,
        expiresAt: source.expires_at
      };
    })
    .filter(item => item.level !== 'OK');
}

function dailyFreshnessCheck() {
  const alerts = listFreshnessAlerts();
  console.log(JSON.stringify(alerts));
  return alerts;
}

時間主導型トリガーで dailyFreshnessCheck を1日1回動かせます。ただし、この例ではメールを自動送信しません。通知先、頻度、再通知停止条件を決めたうえで、既存の社内通知へ連携してください。

期限前でも再確認へ戻す

価格、仕様、契約文言などが変わったときは、有効期限を待たず REVIEW_REQUIRED にします。

function requireReview(sourceId, reason) {
  if (!reason) throw new Error('再確認理由が必要です');

  const source = getSources().find(item => item.source_id === sourceId);
  if (!source) throw new Error(`source_idが見つかりません: ${sourceId}`);

  const sheet = getSourceSheet();
  const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const statusColumn = headers.indexOf('status') + 1;
  const checkedColumn = headers.indexOf('last_checked_at') + 1;

  sheet.getRange(source.rowNumber, statusColumn).setValue('REVIEW_REQUIRED');
  sheet.getRange(source.rowNumber, checkedColumn).setValue(new Date());
  console.log(JSON.stringify({ sourceId, reason, status: 'REVIEW_REQUIRED' }));
}

次のイベントは再確認の候補です。

  • 価格、プラン、キャンペーン、提供範囲の変更
  • 製品仕様、操作手順、FAQの変更
  • 担当部署、承認者、問い合わせ窓口の変更
  • 誤案内、苦情、根拠なし回答の発見
  • 利用するAIサービスやデータ保存条件の変更

実運用で追加する安全策

このサンプルを本番運用へ広げる前に、少なくとも次を追加してください。

  • スプレッドシートとApps Scriptの編集者を必要最小限にする
  • source_id の重複と必須項目を登録時に検査する
  • 承認者と更新者、変更理由を監査ログへ残す
  • 日時は保存時刻と表示タイムゾーンを決める
  • 正本URLから内容を取得する処理には認可と失敗時停止を入れる
  • 参照元の本文をログへ出さず、IDと版だけを記録する
  • LockService で承認と更新の同時実行を制御する
  • 契約、金額、個人情報、苦情を含む返信は必ず担当者へ渡す

AIに任せる範囲と人が残す範囲

AIやGASには、承認済み参照元の抽出、期限判定、下書きへの参照元ID付与、期限間近一覧の作成を任せられます。

一方、正本の選定、情報区分、承認、有効期限、例外判断、顧客への約束、最終送信は人が担当します。法務・契約・規制対象の情報は、一般的な期限例を流用せず、組織の所管部門がルールを決めてください。

導入チェックリスト

  • AIが使う資料に一意な source_id を付けた
  • コピーではなく正本URLを記録した
  • 内容を判断できる業務責任者を割り当てた
  • 承認済み版、承認日時、有効期限を記録した
  • 許可用途を reply_draft などに限定した
  • 期限切れ時は生成を止めて人間確認へ切り替える
  • 期限前の通知と、変更イベント時の即時停止を用意した
  • 下書きには参照元IDと版を残す
  • 個人情報、秘密情報、認証情報を外部AIへ送らない
  • 人が宛先、事実、表現、添付、送信可否を確認する

まとめ

古い情報による誤案内は、プロンプトだけでは防げません。参照元に正本、責任者、承認済み版、有効期限、許可用途を持たせ、下書き生成前に機械的な鮮度ゲートを通す必要があります。

最初は、返信でよく使う資料を5件だけ台帳へ登録してください。「今日価格が変わったら、誰がどの資料を止めるか」を答えられない資料は、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?