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?

GoogleスプレッドシートとGASでAI運用ルールの変更イベントを管理する

0
Posted at

生成AIの利用ルールは、一度作れば終わりではありません。価格、担当者、社内規程、入力データ、利用モデルが変わると、以前の確認条件が合わなくなることがあります。

この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)を使い、変更を検知したら影響するルールと停止対象を人間の確認キューへ追加する最小構成を作ります。AIによる自動承認や、顧客向け処理の自動再開は行いません。

この記事で作るもの

3枚のシートを使います。

  • EVENT_RULES: 変更種別、影響ルール、確認者、停止対象を定義
  • CHANGE_EVENTS: 発生した変更とレビュー状態を記録
  • REVIEW_LOG: 人間が行った判断と理由を記録

AI運用の入口と人間確認ゲート

処理の流れは次の通りです。

  1. 商品、価格、担当者などの変更を登録する
  2. GASが変更種別に対応する影響ルールを抽出する
  3. 停止対象があればレビュー待ちとして記録する
  4. 担当者が通常例、境界例、停止例を確認する
  5. 権限を持つ人が承認、修正、却下を記録する
  6. 別の運用手順で停止中の処理を再開する

このコードは5までを支援します。外部返信や見積もりを再開する処理は実装しません。

EVENT_RULESシートを作る

1行目に次の見出しを置きます。

event_type | affected_rules | reviewer_role | priority | pause_actions | enabled

代表的な7種類を登録します。

product_changed | faq,proposal_scope | product_owner | medium | external_reply | TRUE
price_changed | price_reply,quote_draft,approval_threshold | sales_owner | high | external_reply,quote_generation | TRUE
owner_changed | approval_route,escalation | ops_owner | high | new_automation_activation | TRUE
policy_changed | input_policy,usage_scope,logging | policy_owner | high | external_reply,new_automation_activation | TRUE
input_changed | masking,storage,access_control | data_owner | high | ai_input,new_automation_activation | TRUE
ai_changed | output_review,evaluation_set | ai_ops_owner | high | new_automation_activation | TRUE
feedback_changed | faq,escalation,prohibited_response | cs_owner | medium |  | TRUE

pause_actions はAI利用全体ではなく、レビュー中に止める必要がある処理だけに絞ります。たとえば価格変更時は、問い合わせ分類を続けながら、金額を含む外部返信と見積もり生成を止められます。

CHANGE_EVENTSとREVIEW_LOGを作る

CHANGE_EVENTS の見出しです。

event_id | detected_at | event_type | source_ref | summary | affected_rules | reviewer_role | priority | pause_actions | status

REVIEW_LOG の見出しです。

event_id | reviewed_at | reviewed_by | decision | reason | tested_normal | tested_boundary | tested_stop | next_review_at

source_ref には価格表や規程など、アクセス制御された正本のIDを入れます。顧客名、契約書本文、問い合わせ原文を変更ログへ複製しないでください。

GASで変更イベントを登録する

スプレッドシートに紐づくApps Scriptへ追加します。

const RULE_SHEET = 'EVENT_RULES';
const EVENT_SHEET = 'CHANGE_EVENTS';
const REVIEW_SHEET = 'REVIEW_LOG';

function rowsToObjects_(sheet) {
  const values = sheet.getDataRange().getDisplayValues();
  if (values.length < 2) return [];

  const headers = values.shift();
  return values
    .filter((row) => row.some((cell) => cell !== ''))
    .map((row) => Object.fromEntries(
      headers.map((header, index) => [header, row[index]])
    ));
}

function splitList_(value) {
  return String(value || '')
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean);
}

function findEventRule_(eventType) {
  const sheet = SpreadsheetApp.getActive().getSheetByName(RULE_SHEET);
  if (!sheet) throw new Error(`${RULE_SHEET} sheet not found`);

  return rowsToObjects_(sheet).find((rule) =>
    rule.event_type === eventType && rule.enabled === 'TRUE'
  );
}

function registerChangeEvent(input) {
  if (!input || !input.event_type || !input.source_ref || !input.summary) {
    throw new Error('event_type, source_ref, summary are required');
  }

  const sheet = SpreadsheetApp.getActive().getSheetByName(EVENT_SHEET);
  if (!sheet) throw new Error(`${EVENT_SHEET} sheet not found`);

  const rule = findEventRule_(input.event_type);
  const eventId = `CHG-${Utilities.getUuid()}`;

  const reviewTask = rule
    ? {
        affectedRules: splitList_(rule.affected_rules),
        reviewerRole: rule.reviewer_role,
        priority: rule.priority,
        pauseActions: splitList_(rule.pause_actions),
        status: 'review_pending',
      }
    : {
        affectedRules: [],
        reviewerRole: 'ops_owner',
        priority: 'high',
        pauseActions: ['new_automation_activation'],
        status: 'manual_classification_required',
      };

  sheet.appendRow([
    eventId,
    new Date(),
    input.event_type,
    input.source_ref,
    input.summary,
    reviewTask.affectedRules.join(','),
    reviewTask.reviewerRole,
    reviewTask.priority,
    reviewTask.pauseActions.join(','),
    reviewTask.status,
  ]);

  return { eventId, ...reviewTask };
}

未知の event_type を通常処理へ流さず、manual_classification_required にするのが重要です。分類が未定でも、新しい自動処理の有効化だけは保留できます。

人間のレビュー結果を記録する

承認者、理由、3種類のテスト結果が揃わない限り、完了扱いにしません。

function recordReview(input) {
  const required = [
    'event_id', 'reviewed_by', 'decision', 'reason',
    'tested_normal', 'tested_boundary', 'tested_stop',
    'next_review_at',
  ];

  const missing = required.filter((key) => !input[key]);
  if (missing.length) {
    throw new Error(`missing fields: ${missing.join(', ')}`);
  }

  if (!['approved', 'revised', 'rejected'].includes(input.decision)) {
    throw new Error('invalid decision');
  }

  const reviewSheet = SpreadsheetApp.getActive().getSheetByName(REVIEW_SHEET);
  const eventSheet = SpreadsheetApp.getActive().getSheetByName(EVENT_SHEET);
  if (!reviewSheet || !eventSheet) throw new Error('required sheet not found');

  reviewSheet.appendRow([
    input.event_id,
    new Date(),
    input.reviewed_by,
    input.decision,
    input.reason,
    input.tested_normal,
    input.tested_boundary,
    input.tested_stop,
    input.next_review_at,
  ]);

  const rows = eventSheet.getDataRange().getValues();
  const rowIndex = rows.findIndex((row, index) =>
    index > 0 && row[0] === input.event_id
  );
  if (rowIndex < 0) throw new Error('event not found');

  const statusColumn = rows[0].indexOf('status') + 1;
  eventSheet.getRange(rowIndex + 1, statusColumn).setValue(
    input.decision === 'rejected' ? 'rejected' : 'reviewed'
  );
}

この関数が reviewed を付けても、停止中の処理を自動再開しません。実運用では、承認者の権限確認と再開手順を別に用意してください。

期限超過のレビューを抽出する

変更イベントが放置されていないか、時間主導トリガーで確認できます。

function listPendingReviews() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(EVENT_SHEET);
  const pendingStatuses = new Set([
    'review_pending',
    'manual_classification_required',
  ]);

  const pending = rowsToObjects_(sheet).filter((event) =>
    pendingStatuses.has(event.status)
  );

  console.log(JSON.stringify(pending.map((event) => ({
    event_id: event.event_id,
    event_type: event.event_type,
    reviewer_role: event.reviewer_role,
    priority: event.priority,
    detected_at: event.detected_at,
  }))));

  return pending;
}

ログ出力だけでは担当者に届きません。実運用では社内のタスク管理や通知先へ接続しますが、通知本文へ機密情報を入れないようにします。

動作確認

次のテストデータを用意します。

  1. price_changed: 既知の高優先度イベント
  2. feedback_changed: 停止対象が空の既知イベント
  3. vendor_changed: ルールにない未知イベント

確認項目です。

  • 価格変更で3つの影響ルールと2つの停止対象が入る
  • 顧客反応の変更はレビュー待ちになるが、不要な処理停止を作らない
  • 未知イベントは manual_classification_required になる
  • 必須項目が欠けたレビューを保存できない
  • decision が許可値以外なら拒否される
  • 通常例、境界例、停止例が揃わないと完了できない
  • 顧客情報や秘密情報をイベントログへ複製していない
  • レビュー完了だけで外部処理が自動再開しない

この構成で自動化しないこと

  • 変更後のルールを正式版にする判断
  • 価格、契約、返金、個人情報、法務・会計・人事の判断
  • 顧客向け返信や見積もりの自動再開
  • AIによる承認者の代行
  • 正本のアクセス権変更

GASは変更の見落としを減らすために使い、ルールの妥当性や再開可否は責任者が判断します。

まとめ

AI運用ルールを古いままにしないためには、定期見直しに加えて、変更が起きた時点でレビューを始める仕組みが必要です。

最初は直近3か月で最も多かった変更種別を1つだけ EVENT_RULES へ登録し、影響ルール、確認者、停止対象を決めてください。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?