AI活用

AIエージェントのHuman in the Loop設計|承認・停止・再開を実装する

AIエージェントの承認・停止・再開を設計する開発者向けに、承認対象の固定、未知操作の拒否、実行前の再検証を解説。停止後に残る外部処理と結果不明の扱い、境界を確かめるテストケースも整理します。

この記事の目次
  1. AIエージェントの承認は「実行の直前」に置く
  2. Human in the Loopが必要な理由
  3. AIエージェントの操作をリスク別に分類する
  4. ルールをコードで判定する例
  5. 承認画面に何を表示するか
  6. 停止・タイムアウト・再開を状態として実装する
  7. 承認後、実行直前に再検証する
  8. 停止ボタンを押した後、何を確認するか
  9. 承認ログを監査と改善に使う
  10. HITLだけでは防げないリスク
  11. Anthropicのリスク開示を実装へどう生かすか
  12. 承認・停止の境界を試すテストケース
  13. AIエージェントの承認設計チェックリスト
  14. Human in the Loopの承認範囲でよくある質問
  15. すべてのツール呼び出しを承認制にすべきですか?
  16. 承認者は誰にすべきですか?
  17. 承認後に内容を少し修正して実行できますか?
  18. 承認フローを一つの操作から確かめる
  19. 参考情報

AIエージェントのHuman in the Loop(HITL)は、外部送信や削除など、影響の大きい操作の前に人の判断を挟む設計です。対象・差分・影響を確認し、必要なら保留や差し戻しを選べるようにします。すべての操作を毎回、人が承認するという意味ではありません。

たとえば、メールの宛先を確かめて承認した後に、送信先が変わっていたら。承認ボタンは押されていますが、その送信を認めたことにはなりません。画面に確認を一つ足すだけでは、承認した内容と実際の操作のずれが残ります。

ここでは、「何を承認したか」を実行直前まで保ち、途中で止めたら「何が済んだか」を追える承認フローを考えます。リスク分類から画面・状態管理・ログ・テストまで、自分のツールに足りない確認を見つけられる順で進めます。

情報確認日:2026年11月2日(日本時間)

AIエージェントの承認は「実行の直前」に置く

承認フローで確かめること

  • 操作を低・中・高リスクに分類する
  • 承認画面へ対象、差分、権限、取り消し可否を表示する
  • 承認待ちの間は副作用を発生させない
  • 期限切れや対象変更時は古い承認を無効にする
  • 実行直前に対象と権限を再検証する
  • 申請、判断、実行結果を監査ログへ残す

承認はプロンプトの途中ではなく、実際に副作用を起こすツールの直前に置きます。「メールを作ってよい」と「100人へ送ってよい」は別の判断です。下書き作成と外部送信を分ければ、確認が必要な操作に人の時間を使えます。

スポンサーリンク

Human in the Loopが必要な理由

AIエージェントの基本構造は、モデルが計画し、ツールを選び、アプリが外部処理を実行する流れです。全体像はAIエージェントとは何か、ツール呼び出しの仕組みはFunction Callingの基本で解説しています。

利用者の依頼
  ↓
モデルが操作を提案
  ↓
アプリがリスクを判定
  ├─ 低リスク → 自動実行
  └─ 要承認   → 停止 → 人が確認
                         ├─ 却下
                         ├─ 修正して再申請
                         └─ 承認 → 再検証 → 実行

NISTのAI RMF関連資料では、人とAIの役割や監督責任を定義し、文書化することが重視されています。OWASPも、LLMへ過剰な機能、権限、自律性を与える「Excessive Agency」をリスクとして挙げています。HITLはその対策の1つですが、最小権限や入力検証の代わりにはなりません。

AIエージェントの操作をリスク別に分類する

区分 例 基本方針
低リスク 公開情報の検索、下書き作成、読み取り専用の集計 自動実行し、必要に応じて結果を記録
中リスク 社内文書の更新、限定相手への送信、可逆な設定変更 条件付き自動化または担当者承認
高リスク 削除、支払い、公開、権限変更、機密情報の外部送信 明示承認、再認証、複数人承認を検討
禁止 法令・規程違反、承認不能な不可逆操作 ツール自体を提供しない

リスクは操作名だけで決まりません。同じメール送信でも、自分宛てのテストと顧客全員への一斉送信では影響が異なります。対象件数、データの機密度、金額、公開範囲、可逆性、本人確認の要否を条件に含めます。

ルールをコードで判定する例

function classifyRisk(action) {
  // 操作名はサーバー側の許可リストで照合する。
  if (!action || typeof action.type !== "string") return "prohibited";
  switch (action.type) {
    case "read_public_document":
      return "low";
    case "update_document":
      return "medium";
    case "send_email":
    case "delete_file":
    case "publish":
    case "pay":
    case "change_permission":
      return "high";
    default:
      return "prohibited";
  }
}

これは操作名を分類する最小例です。未知の操作は拒否し、読み取りにも取得先・対象・件数の検査を別途通します。「公開文書を読む」という名前だけで、任意のURLへの接続を許可してはいけません。モデルが渡した機密度や件数も、そのまま信用せず実データで確かめます。モデルは説明を補助できますが、承認ルールや実行権限を自分で変更できない構成にします。

承認画面に何を表示するか

「許可しますか?」だけでは、何を承認するのか判断できません。承認要求は、実行予定の内容を人が短時間で検証できる情報に変換します。

表示項目 具体例 目的
操作 顧客へメールを送信 何が起きるかを明確にする
対象 送信先20件、顧客グループA 誤った対象を発見する
内容・差分 件名、本文、変更前後 結果を事前確認する
影響 外部送信、取り消し不可 判断の重さを伝える
根拠 利用者の依頼と参照資料 指示との一致を確認する
期限 10分後に失効 古い状態での実行を防ぐ

危険なUI:「確認済みです」というAIの要約だけでは、送信先や変更内容を照合できません。実行予定の引数から対象・件数・差分を表示し、外部文書の文章と操作の説明を見分けられるようにします。判断に必要な時間や情報が足りなければ、保留や差し戻しを選べるようにします。

停止・タイムアウト・再開を状態として実装する

承認待ちは、単に処理を長時間スリープさせる状態ではありません。申請内容を保存し、ワーカーを解放し、承認イベントを受けて安全に再開できる状態機械として扱います。

draft
  ↓ request_approval
pending_approval
  ├─ reject  → rejected
  ├─ expire  → expired
  └─ approve → approved
                  ↓ revalidate
              executing
                ├─ succeeded
                ├─ failed(失敗を確認)
                └─ unknown(外部の結果を照合)

状態はアプリ側で保存し、AIやブラウザから送られた「approved」という値だけで承認済みと扱いません。申請IDから、承認者・期限・承認した操作・対象の版を読み直します。

再開には、同じ処理を識別する冪等性キーを使います。ただし、キーを付けるだけで二重送信がなくなるわけではありません。外部API側の対応や有効期間も確認します。送信後に応答だけ失われた場合は「失敗」と決めず、結果不明として照合し、同じ送信を繰り返さないようにします。

承認後、実行直前に再検証する

申請から承認までの間に、文書、価格、送信先、権限が変わる場合があります。承認時のスナップショットと現在値を比較し、変更されていれば再申請します。

// アダプターの契約を示す擬似コード。これだけでは実行できません。
async function executeApprovedAction(requestId) {
  return approvalStore.withExclusiveLock(requestId, async () => {
    // AIの申告ではなく、信頼できる保存先の記録を読む。
    const request = await approvalStore.load(requestId);
    if (!request || request.state !== "approved") {
      throw new Error("not_approved");
    }
    if (!Number.isFinite(Date.parse(request.expiresAt)) ||
        Date.parse(request.expiresAt) <= Date.now()) {
      throw new Error("approval_expired");
    }

    await assertRunNotStopped(request.runId);
    await assertCurrentPolicyAndPermissions(request);
    await assertTargetVersion(request.targetId, request.targetVersion);
    // 保存した操作を使う。モデルに引数を作り直させない。
    await assertApprovedActionHash(request.action, request.actionHash);
    await approvalStore.markExecuting(request.id);

    try {
      const result = await performApprovedAction(request.action, {
        idempotencyKey: request.id,
        expectedTargetVersion: request.targetVersion,
      });
      await approvalStore.markSucceeded(request.id, result);
      return result;
    } catch (error) {
      // 外部で完了した可能性を残す。自動で再実行しない。
      await approvalStore.markUnknown(request.id);
      throw error;
    }
  });
}

ロックは同じ申請の同時実行を抑えるためのものです。対象データの変更まで防ぐには、更新先でも版番号や条件付き更新を検査します。承認者だけでなく、実行するサービス用IDの権限も確認が必要です。

この例の保存処理は、状態を永続化し、承認した操作を改変できないようにする実装を前提にしています。ハッシュだけでは承認者の本人確認にはなりません。実行中にプロセスが落ちた申請も照合対象へ回し、起動し直しただけで再実行しない設計にします。

停止ボタンを押した後、何を確認するか

停止は「次の操作を始めないこと」と「すでに外部へ依頼した操作を取り消すこと」に分けます。アプリのループを止めても、外部サービスで進行中の送信や更新が取り消されるとは限りません。

例えば一斉送信の途中で止めたら、送信済み・未送信・結果不明を分け、未送信分の新規実行を止めます。外部APIに取消機能があれば、その応答と結果を確かめます。送信済みの訂正連絡など、誰が引き継ぐかも決めておきます。

実行直前の停止チェックにも、確認直後に停止要求が届く時間差があります。強い停止保証が必要な処理では、実行側の受付停止・キュー取消・権限失効を組み合わせ、その保証が及ぶ時点を試験します。画面に「停止済み」と表示しただけでは完了にしません。

承認ログを監査と改善に使う

{
  "approval_id": "apr_01J...",
  "action_type": "send_email",
  "risk_level": "high",
  "requested_by": "agent-support",
  "requested_for": "user-123",
  "target_count": 20,
  "snapshot_hash": "sha256:...",
  "decision": "approved",
  "decided_by": "manager-456",
  "decision_reason": "送信先と本文を確認",
  "requested_at": "2026-07-18T03:00:00Z",
  "decided_at": "2026-07-18T03:03:10Z",
  "executed_at": "2026-07-18T03:03:12Z",
  "result": "succeeded"
}

ログには判断に必要なメタデータを残しますが、機密本文を無制限に複製しないようにします。却下率、期限切れ率、承認後の失敗率、承認者ごとの偏りを分析すると、ルールが厳しすぎる箇所や、危険な操作が自動化されすぎている箇所を見つけられます。

HITLだけでは防げないリスク

  • 承認画面自体に悪意ある外部文章が混入する
  • 承認者が内容を読まずに許可する
  • ツールへ必要以上の権限が付与されている
  • 承認後に対象や権限が差し替わる
  • 低リスクと分類した操作を大量実行される

そのため、最小権限、入力検証、出力のエスケープ、件数上限、サンドボックス、実行直前の再検証を組み合わせます。外部コンテンツからの指示混入についてはプロンプトインジェクション対策も確認してください。

Anthropicのリスク開示を実装へどう生かすか

Anthropicの上場申請書は、停止への抵抗や情報操作、エージェントの誤動作による被害などをリスクとして開示しています。これは、すべての日常利用でそうした挙動が起きているという報告ではありません。関連する同社の研究も、条件を設定した模擬環境での結果です。

開発側で考えたいのは、モデルが不適切な操作を提案しても、実行側で止められるかです。外部からの指示混入だけでなく、善意の依頼を誤って解釈した場合も試験します。停止や監査ログの管理権限は、作業を進めるエージェントから分離します。

承認・停止の境界を試すテストケース

以下は編集部による試験設計例です。テスト用の送信先や模擬APIを使い、画面の表示に加えて、実行側で何回処理されたかを確かめます。

変える条件 確かめる結果
未登録の操作を要求 低リスク扱いにならず、実行0回
承認待ち・却下・期限切れで再開 外部処理を始めず、理由が記録される
承認後に宛先・本文・対象の版を変更 古い承認では実行されず、再確認になる
同じ申請を同時に2回再開 対象APIの実行が重ならず、状態が食い違わない
外部APIの処理後に応答が途切れる 結果不明として照合し、無条件に再送しない
送信途中で停止、続いて再起動 未送信分は止まり、送信済み分を繰り返さない
1件ずつの許可操作を大量に要求 実行全体の件数・時間などの上限で止まる

上限でループを止める処理はTool Callingの停止条件を持つ実装例へ。ここでは、承認した操作と実際の操作が一致するかを検査します。

AIエージェントの承認設計チェックリスト

  • 操作ごとのリスク分類と責任者が決まっている
  • 禁止操作はツールとして公開していない
  • 承認画面に対象、差分、影響、期限が表示される
  • 承認待ちでは副作用が起きない
  • 期限切れと対象変更で再申請になる
  • 実行時に権限を再確認する
  • 冪等性キーで二重実行を防ぐ
  • 緊急停止と手動復旧の手順がある
  • 申請から結果まで監査ログで追跡できる

Human in the Loopの承認範囲でよくある質問

すべてのツール呼び出しを承認制にすべきですか?

いいえ。低リスクな読み取りまで毎回止めると、承認疲れが起きます。影響、可逆性、対象、機密度に応じて自動実行と承認を分けます。

承認者は誰にすべきですか?

操作対象と責任に応じて決めます。本人の外部送信は本人、一定額以上の購入は管理者、権限変更はシステム管理者など、役割と金額・件数の条件を規程化します。

承認後に内容を少し修正して実行できますか?

承認対象が変わるため、原則として再申請します。誤字修正など許容する変更を設ける場合も、変更可能なフィールドと範囲を明確にし、差分をログへ残します。

承認フローを一つの操作から確かめる

冒頭の「承認後に宛先が変わったメール」は、実行直前の照合で差し戻せる設計にします。確認した内容が実際の操作に結び付いてこそ、承認した人の判断を生かせます。

まずは外部送信など一つの操作を選び、模擬APIで「未承認」「承認後の対象変更」「応答の途切れ」を試してください。画面の表示だけでなく、実行回数と記録を見れば、承認フローのどこを直すべきかが具体的になります。

参考情報

スポンサーリンク