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

生成AIで問い合わせ返信の下書きを作る運用では、異常を検知するだけでは足りません。古いFAQ、急なエラー増加、承認者不在が起きたときに、新しい下書き生成を止め、未処理案件を人間へ戻せる必要があります。

この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、AI返信下書き処理のサーキットブレーカーを作ります。外部送信やAI API呼び出しは実装せず、処理可否の判定、停止、試験再開、監査ログまでを対象にします。

この記事で作るもの

  1. 運用状態を CLOSED、OPEN、HALF_OPEN で管理する設定
  2. エラー率や手動操作で新規処理を止めるGAS関数
  3. 少数の試験処理だけを許可して安全に再開する判定
  4. 停止・再開理由を残す監査ログ

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

完成状態は、障害時に「AIが動くか」ではなく「業務として使ってよいか」を判定し、停止中の案件を人間確認キューへ送れることです。

状態とシートを準備する

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

timestamp,event,previous_state,next_state,reason,actor,window_total,window_failures,note

状態は次の3つです。

  • CLOSED: 通常運用。条件を満たす案件だけ下書き生成へ進める
  • OPEN: 新規生成を停止し、人間確認へ切り替える
  • HALF_OPEN: 少数の試験案件だけ許可し、再開可否を確認する

設定値はスクリプトプロパティへ保存します。

function setupCircuitBreaker() {
  PropertiesService.getScriptProperties().setProperties({
    BREAKER_STATE: 'CLOSED',
    BREAKER_OPENED_AT: '',
    BREAKER_REASON: '',
    WINDOW_SIZE: '20',
    FAILURE_THRESHOLD: '0.25',
    MIN_FAILURES_TO_OPEN: '3',
    HALF_OPEN_AFTER_MINUTES: '30',
    HALF_OPEN_MAX_TRIALS: '3',
    HALF_OPEN_SUCCESS_REQUIRED: '3',
    HALF_OPEN_TRIALS: '0',
    HALF_OPEN_SUCCESSES: '0'
  });
}

しきい値は業務影響に合わせて決めます。返金、契約、個人情報を含む問い合わせは、この仕組みの対象にせず、最初から人間確認へ送ります。

現在状態を読む

function getBreakerConfig() {
  const p = PropertiesService.getScriptProperties();
  return {
    state: p.getProperty('BREAKER_STATE') || 'OPEN',
    openedAt: p.getProperty('BREAKER_OPENED_AT'),
    reason: p.getProperty('BREAKER_REASON'),
    windowSize: Number(p.getProperty('WINDOW_SIZE') || 20),
    failureThreshold: Number(p.getProperty('FAILURE_THRESHOLD') || 0.25),
    minFailures: Number(p.getProperty('MIN_FAILURES_TO_OPEN') || 3),
    halfOpenAfterMinutes: Number(p.getProperty('HALF_OPEN_AFTER_MINUTES') || 30),
    halfOpenMaxTrials: Number(p.getProperty('HALF_OPEN_MAX_TRIALS') || 3),
    halfOpenTrials: Number(p.getProperty('HALF_OPEN_TRIALS') || 0)
  };
}

設定が読めないときは CLOSED にせず OPEN として扱います。障害時に安全側へ倒すためです。

処理結果から異常を判定する

直近の処理結果は ProcessingLog シートに timestamp、result、reason を保存している前提です。問い合わせ本文や個人情報は保存しません。

function getRecentMetrics() {
  const c = getBreakerConfig();
  const sheet = SpreadsheetApp.getActive().getSheetByName('ProcessingLog');
  if (!sheet) throw new Error('ProcessingLog sheet is missing');

  const values = sheet.getDataRange().getValues();
  const rows = values.slice(1).filter(row => row[0]).slice(-c.windowSize);
  const failures = rows.filter(row => row[1] === 'FAILED').length;

  return {
    total: rows.length,
    failures,
    failureRate: rows.length ? failures / rows.length : 0
  };
}

function shouldOpenBreaker(metrics) {
  const c = getBreakerConfig();
  return metrics.failures >= c.minFailures &&
    metrics.failureRate >= c.failureThreshold;
}

失敗理由には source_expired、policy_missing、output_schema_invalid など、顧客情報を含まない分類コードを使います。

停止操作を一つの関数へ集約する

function openBreaker(reason, actor, metrics = {}) {
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  try {
    const p = PropertiesService.getScriptProperties();
    const previous = p.getProperty('BREAKER_STATE') || 'OPEN';
    p.setProperties({
      BREAKER_STATE: 'OPEN',
      BREAKER_OPENED_AT: new Date().toISOString(),
      BREAKER_REASON: reason,
      HALF_OPEN_TRIALS: '0',
      HALF_OPEN_SUCCESSES: '0'
    });
    appendBreakerLog('OPEN', previous, 'OPEN', reason, actor, metrics);
  } finally {
    lock.releaseLock();
  }
}

function evaluateHealthAndStop() {
  const metrics = getRecentMetrics();
  if (shouldOpenBreaker(metrics)) {
    openBreaker('failure_threshold_exceeded', 'system', metrics);
  }
}

LockService を使い、複数実行が同時に状態を書き換える競合を避けます。自動停止だけでなく、責任者が手動停止できるメニューも用意してください。

下書き生成前にゲートを通す

function canStartReplyDraft() {
  const c = getBreakerConfig();

  if (c.state === 'OPEN') {
    return { allowed: false, action: 'human_review', reason: c.reason };
  }
  if (c.state === 'HALF_OPEN' && c.halfOpenTrials >= c.halfOpenMaxTrials) {
    return { allowed: false, action: 'human_review', reason: 'trial_limit' };
  }
  return {
    allowed: true,
    mode: c.state === 'HALF_OPEN' ? 'trial' : 'normal'
  };
}

停止中に案件を捨てたり、自動で「対応済み」にしたりしてはいけません。受付時刻、案件ID、停止理由だけを人間確認キューへ残し、本文は権限管理された元システムで確認します。

時間だけで全復旧させない

一定時間後にできるのは HALF_OPEN への移行までです。

function moveToHalfOpenIfEligible() {
  const c = getBreakerConfig();
  if (c.state !== 'OPEN' || !c.openedAt) return;

  const elapsedMinutes = (Date.now() - new Date(c.openedAt).getTime()) / 60000;
  if (elapsedMinutes < c.halfOpenAfterMinutes) return;

  PropertiesService.getScriptProperties().setProperties({
    BREAKER_STATE: 'HALF_OPEN',
    HALF_OPEN_TRIALS: '0',
    HALF_OPEN_SUCCESSES: '0'
  });
  appendBreakerLog('HALF_OPEN', 'OPEN', 'HALF_OPEN',
    'cooldown_elapsed', 'system', {});
}

試験処理は、機密性が低く、正本確認済みで、必ず人が出力を見る案件に限定します。失敗が1件でもあれば再び OPEN に戻します。

function recordHalfOpenResult(success) {
  const p = PropertiesService.getScriptProperties();
  const c = getBreakerConfig();
  if (c.state !== 'HALF_OPEN') return;

  const trials = c.halfOpenTrials + 1;
  const successes = Number(p.getProperty('HALF_OPEN_SUCCESSES') || 0) +
    (success ? 1 : 0);
  p.setProperty('HALF_OPEN_TRIALS', String(trials));
  p.setProperty('HALF_OPEN_SUCCESSES', String(successes));

  if (!success) openBreaker('half_open_trial_failed', 'system');
}

CLOSED への復帰は、試験成功数だけで自動化せず、業務責任者が原因解消、滞留件数、参照元、承認者の稼働を確認して実行します。

監査ログを残す

function appendBreakerLog(event, previous, next, reason, actor, metrics) {
  const sheet = SpreadsheetApp.getActive().getSheetByName('BreakerLog');
  if (!sheet) throw new Error('BreakerLog sheet is missing');
  sheet.appendRow([
    new Date(), event, previous, next, reason, actor,
    metrics.total ?? '', metrics.failures ?? '', ''
  ]);
}

actor はメールアドレスを公開ログへ書くのではなく、社内の役割IDを推奨します。ログの閲覧権限、保存期間、削除責任者も別途決めます。

本番前チェックリスト

  • 停止中の案件が人間確認キューへ残る
  • 設定欠落時は安全側の OPEN になる
  • 自動停止と責任者による手動停止の両方がある
  • 時間経過だけで CLOSED へ戻らない
  • HALF_OPEN の試験件数と対象条件が決まっている
  • 再開前に原因解消、参照元、滞留件数、承認者を確認する
  • AIが外部送信、返金、契約、個人情報の判断をしない
  • ログへ問い合わせ本文や認証情報を保存しない
  • 停止・再開の責任者と代理担当が決まっている
  • 定期的に停止訓練を行い、復旧手順を更新する

まとめ

サーキットブレーカーはAPI障害対策だけではありません。AIの出力品質、参照元、承認体制のどれかが崩れたときに、業務を安全に人間へ戻す仕組みです。

まずは自動返信ではなく下書き生成を対象に、OPEN 時の人間確認キューと、責任者が確認する再開チェックリストから作ってください。

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?