生成AIで問い合わせ返信の下書きを作る運用では、異常を検知するだけでは足りません。古いFAQ、急なエラー増加、承認者不在が起きたときに、新しい下書き生成を止め、未処理案件を人間へ戻せる必要があります。
この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)で、AI返信下書き処理のサーキットブレーカーを作ります。外部送信やAI API呼び出しは実装せず、処理可否の判定、停止、試験再開、監査ログまでを対象にします。
この記事で作るもの
- 運用状態を
CLOSED、OPEN、HALF_OPENで管理する設定 - エラー率や手動操作で新規処理を止めるGAS関数
- 少数の試験処理だけを許可して安全に再開する判定
- 停止・再開理由を残す監査ログ
完成状態は、障害時に「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 時の人間確認キューと、責任者が確認する再開チェックリストから作ってください。
