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で問い合わせ返信の下書きを作る運用では、プロンプトを改善するたびに「誰が、何のために、どの評価を通して変更したか」を追える必要があります。本文だけを上書きすると、品質が落ちたときに原因と戻し先を特定できません。

この記事では、Googleスプレッドシートを変更台帳、Google Apps Script(GAS)を登録・承認・適用判定の実行基盤として使い、プロンプト変更を安全に管理する最小構成を作ります。

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

この記事で作るもの

次の3つを実装します。

  1. 変更目的、評価結果、承認者、戻し先を1行で管理する台帳
  2. 未承認の版を本番対象にしない状態遷移
  3. 重大エラーや修正率から差し戻し候補を判定する日次チェック

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

完成状態は、現在の有効版を一意に取得でき、問題が起きたら承認済みの前版へ戻せることです。

シート構成

スプレッドシートに PromptChanges シートを作ります。1行目は次の見出しです。

change_id,created_at,from_version,to_version,purpose,dataset_id,total_cases,critical_errors,unsupported_claims,human_fix_rate,status,approved_by,approved_at,rollback_to,applied_at

status は次の値だけを使います。

  • DRAFT: 登録済み、評価前
  • EVALUATED: 評価値を記録済み
  • APPROVED: 人間が適用を承認済み
  • ACTIVE: 現在利用する版
  • ROLLED_BACK: 問題発生後に差し戻した版
  • REJECTED: 評価または承認で不採用

自由入力にすると表記ゆれが起きるため、データの入力規則も設定してください。

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

シートIDや閾値をコード本文へ散らさず、スクリプトプロパティへ保存します。

function setupProperties() {
  PropertiesService.getScriptProperties().setProperties({
    SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID',
    CHANGE_SHEET_NAME: 'PromptChanges',
    MIN_EVALUATION_CASES: '20',
    MAX_HUMAN_FIX_RATE: '0.20'
  });
}

実際の認証情報、顧客情報、プロンプト本文はプロパティへ入れません。プロンプト本文はアクセス制御された別の保管先で版管理し、台帳には版IDだけを置きます。

共通関数

const HEADERS = [
  'change_id', 'created_at', 'from_version', 'to_version', 'purpose',
  'dataset_id', 'total_cases', 'critical_errors', 'unsupported_claims',
  'human_fix_rate', 'status', 'approved_by', 'approved_at',
  'rollback_to', 'applied_at'
];

function getConfig() {
  const p = PropertiesService.getScriptProperties();
  return {
    spreadsheetId: p.getProperty('SPREADSHEET_ID'),
    sheetName: p.getProperty('CHANGE_SHEET_NAME') || 'PromptChanges',
    minCases: Number(p.getProperty('MIN_EVALUATION_CASES') || 20),
    maxFixRate: Number(p.getProperty('MAX_HUMAN_FIX_RATE') || 0.20)
  };
}

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

function rowsAsObjects() {
  const values = getSheet().getDataRange().getValues();
  const headers = values.shift();
  return values.filter(row => row[0]).map((row, index) => ({
    rowNumber: index + 2,
    ...Object.fromEntries(headers.map((h, i) => [h, row[i]]))
  }));
}

function findChange(changeId) {
  const item = rowsAsObjects().find(row => row.change_id === changeId);
  if (!item) throw new Error(`change_idが見つかりません: ${changeId}`);
  return item;
}

変更案を登録する

function createChange(input) {
  const required = ['fromVersion', 'toVersion', 'purpose', 'datasetId', 'rollbackTo'];
  required.forEach(key => {
    if (!input[key]) throw new Error(`必須項目がありません: ${key}`);
  });

  if (input.fromVersion === input.toVersion) {
    throw new Error('変更前と変更後の版は分けてください');
  }

  const now = new Date();
  const changeId = `PRM-${Utilities.formatDate(now, 'Asia/Tokyo', 'yyyyMMdd-HHmmss')}`;
  getSheet().appendRow([
    changeId, now, input.fromVersion, input.toVersion, input.purpose,
    input.datasetId, '', '', '', '', 'DRAFT', '', '', input.rollbackTo, ''
  ]);
  return changeId;
}

実行例です。

function exampleCreateChange() {
  const id = createChange({
    fromVersion: 'reply-v12',
    toVersion: 'reply-v13',
    purpose: '情報不足時は断定せず確認質問を返す',
    datasetId: 'inquiry-golden-set-v4',
    rollbackTo: 'reply-v12'
  });
  console.log(id);
}

評価結果を記録する

評価データには問い合わせ原文を入れず、匿名化済みケースの集計値だけを渡します。

function recordEvaluation(changeId, result) {
  const item = findChange(changeId);
  if (item.status !== 'DRAFT' && item.status !== 'EVALUATED') {
    throw new Error(`評価を記録できない状態です: ${item.status}`);
  }

  const numbers = ['totalCases', 'criticalErrors', 'unsupportedClaims', 'humanFixRate'];
  numbers.forEach(key => {
    if (!Number.isFinite(Number(result[key]))) throw new Error(`数値が不正です: ${key}`);
  });

  const sheet = getSheet();
  sheet.getRange(item.rowNumber, 7, 1, 5).setValues([[
    Number(result.totalCases),
    Number(result.criticalErrors),
    Number(result.unsupportedClaims),
    Number(result.humanFixRate),
    'EVALUATED'
  ]]);
}

合格条件をコードで固定する

承認者の感覚だけで適用を決めず、最低条件を先に固定します。

function evaluateGate(changeId) {
  const item = findChange(changeId);
  const c = getConfig();
  const reasons = [];

  if (Number(item.total_cases) < c.minCases) reasons.push('評価件数不足');
  if (Number(item.critical_errors) > 0) reasons.push('重大エラーあり');
  if (Number(item.unsupported_claims) > 0) reasons.push('根拠なし回答あり');
  if (Number(item.human_fix_rate) > c.maxFixRate) reasons.push('人間修正率が上限超過');

  return { passed: reasons.length === 0, reasons };
}

この関数は承認を代行しません。機械判定を通過しても、業務責任者による人間承認が必要です。

承認と適用を分離する

function approveChange(changeId, approverId) {
  if (!approverId) throw new Error('承認者IDが必要です');
  const item = findChange(changeId);
  if (item.status !== 'EVALUATED') throw new Error('評価済みではありません');

  const gate = evaluateGate(changeId);
  if (!gate.passed) throw new Error(`合格条件未達: ${gate.reasons.join(', ')}`);

  const sheet = getSheet();
  sheet.getRange(item.rowNumber, 11, 1, 3)
    .setValues([['APPROVED', approverId, new Date()]]);
}

function activateChange(changeId) {
  const lock = LockService.getScriptLock();
  lock.waitLock(10000);
  try {
    const item = findChange(changeId);
    if (item.status !== 'APPROVED') throw new Error('承認済みではありません');

    const sheet = getSheet();
    rowsAsObjects()
      .filter(row => row.status === 'ACTIVE')
      .forEach(row => sheet.getRange(row.rowNumber, 11).setValue('ROLLED_BACK'));

    sheet.getRange(item.rowNumber, 11).setValue('ACTIVE');
    sheet.getRange(item.rowNumber, 15).setValue(new Date());
  } finally {
    lock.releaseLock();
  }
}

LockService を使い、同時実行で有効版が複数生まれるのを防ぎます。本番では実行者をApps Scriptの共有設定で制限し、承認者と適用者を分ける運用も検討してください。

差し戻し候補を検出する

適用後の監視集計を別処理から渡し、差し戻しが必要かを判定します。自動で外部送信や設定変更はせず、人間へ確認対象を返します。

function checkRollbackCandidate(metrics) {
  const active = rowsAsObjects().find(row => row.status === 'ACTIVE');
  if (!active) throw new Error('ACTIVEな変更がありません');

  const reasons = [];
  if (Number(metrics.criticalErrors) >= 1) reasons.push('重大エラーを検出');
  if (Number(metrics.unsupportedClaims) >= 1) reasons.push('根拠なし回答を検出');
  if (Number(metrics.humanFixRate) > getConfig().maxFixRate) {
    reasons.push('人間修正率が上限超過');
  }

  return {
    needsHumanReview: reasons.length > 0,
    changeId: active.change_id,
    rollbackTo: active.rollback_to,
    reasons
  };
}

時間主導トリガーで1日1回実行する場合も、結果は担当者への通知や確認キューへ送り、無人でロールバックしない構成から始めるのが安全です。

動作確認チェックリスト

公開前にテスト用スプレッドシートで確認します。

  • 必須項目が欠けた変更案を拒否する
  • DRAFT 以外へ不正に評価を書けない
  • 評価件数不足、重大エラー、根拠なし回答を不合格にする
  • 合格前に承認できない
  • 未承認版を ACTIVE にできない
  • 同時実行でも ACTIVE が1件だけになる
  • 差し戻し先が台帳に残る
  • 顧客の問い合わせ原文や個人情報を保存していない
  • 外部送信前の人間確認が残っている

運用上の境界

この仕組みは、プロンプトの良し悪しを自動で保証するものではありません。特に次は人間が判断します。

  • 評価ケースが実際の問い合わせ種類を代表しているか
  • 法務、契約、返金、個人情報を含む回答を許可するか
  • 根拠として利用してよいFAQや社内資料は何か
  • 限定公開から通常運用へ進めてよいか
  • 顧客へ送る最終文面が妥当か

また、シートの編集権限、Apps Scriptの実行権限、変更履歴の保存期間も別途決める必要があります。

まとめ

AI返信プロンプトは、本文だけでなく「変更目的・評価・承認・適用範囲・戻し先」を一緒に管理すると、安全に改善しやすくなります。

最初はスプレッドシートとGASで十分です。未承認版を適用しないこと、重大エラーを見逃さないこと、外部送信前の人間確認を残すことから始めてください。

Miraigentでは、AI導入前の業務フロー、入力禁止情報、人間確認、例外処理、記録先を整理する無料診断を提供しています。ツール選定前に、現在の変更・承認の流れを1枚へ書き出すと、必要な実装範囲を判断しやすくなります。

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?