AI エージェントに人間の承認を挟む設計(human in the loop)

公開日:2026-11-08

人の承認は、取り消せない操作、外部に影響する操作、費用がかかる操作に絞って挟みます。承認の判定はモデルの外、つまりエージェントの実行環境のフックや実行する側のシステムに置き、応答がない、期限が切れた、通信に失敗したときは拒否として扱います。MCP の承認ツールは手軽ですが、モデルが呼ばない、または答えを無視することがあるので、確実に止めたい操作には向きません。

承認を挟むべき操作

すべての操作に承認を求めると、確認が多すぎて中身を読まずに押すようになります。承認は数を絞ったほうが効きます。目安は次の 4 つです。

取り消せない本番のデータの削除、マイグレーション、git push --force、バックアップの上書き
外部に影響するメールやチャットの送信、公開リポジトリへの push、リリース、顧客のデータの変更
費用がかかるクラウドの資源の作成、有料 API の大量の呼び出し、支払い
権限や秘密に触れる鍵の発行、権限の変更、本番の認証情報を使う操作

逆に、読み取り、手元のファイルの編集、テストの実行のように、あとから戻せて外に出ない操作は、承認なしで進めたほうが全体として安全です。人の注意を本当に危ない操作に残せるからです。

OWASP の LLM のリスクの一覧も、過剰な権限(Excessive Agency)への対策として、影響の大きい操作は人が承認してから実行することと、許可の判定をモデルに任せず実行する側のシステムで行うことを挙げています。

承認をどこで判定するか

同じ「承認を求める」でも、判定する場所で強さが変わります。

  1. モデルに頼む。プロンプトや承認用のツールで「危ない操作の前に確認して」と指示します。モデルが確認を飛ばしたり、答えを読み違えたりすると止まりません。プロンプトインジェクションで指示が上書きされることもあります
  2. エージェントの実行環境で止める。Claude Code の許可のルールやフックのように、ツールが動く前にモデルの外で判定します。モデルが何を考えても、判定を通らなければ実行されません
  3. 実行される側で止める。デプロイのスクリプト、API、データベースの権限の側で承認を確かめます。エージェント以外の経路から来た操作も止められます

Claude Code の説明も、許可のルールはモデルではなく Claude Code が強制するもので、プロンプトや CLAUDE.md の指示では許可の範囲は変わらないとしています。確実に止めたい操作は 2 か 3 で判定し、1 は補助と考えます。

MCP の仕様も、ツールの呼び出しを人が拒否できるようにすべきだとし、クライアントには操作の前に確認を出すことを求めています。また、ツールの注釈(destructiveHint など)は信頼できるサーバーのものでなければ信用しないよう書いています。サーバーの自己申告に頼らず、クライアントの側で判定するという考え方です。

設計のパターン

許可リストと確認

安全な操作を許可リストに入れて自動で通し、危ない操作を拒否リストに入れ、どちらでもない操作だけ人に確認します。確認の数が減るので、一つずつ読む余裕ができます。

承認ツールを MCP で渡す

エージェントに「承認を求める」ツールを渡し、必要なときに呼ばせます。選択肢や短い指示を受け取れるので、「どちらの環境に出すか」のような相談にも使えます。判定はモデルの中なので、確実な停止には使いません。

フックで強制する

ツールの実行の前に必ず通る場所で承認を確かめます。モデルが承認ツールを呼ばなくても止まります。確実に止めたい操作はこの形にします。

期限と既定の拒否

承認には期限を付け、期限が切れたら拒否にします。通信の失敗や状態が分からないときも同じです。応答がないことを承認として扱わないのが原則です。期限がないと、エージェントがいつまでも待つか、待ちきれずに別の経路を探します。

却下の理由を返す

「却下」だけ返すと、エージェントは同じ操作を言い換えて試しがちです。「本番ではなくステージングで」「先にバックアップを取る」のような理由を返すと、次の手を変えられます。

2 人の承認と引き継ぎ

本番のデータベースの変更のように 1 人で決めたくない操作は、2 人の承認がそろうまで実行しません。担当が応答しないときに別の人へ知らせる仕組みも、期限切れを減らします。

二重実行の防止と記録

承認の依頼を再送しても二重に届かないよう、操作ごとに一意のキーを付けます。誰がいつ承認したかを残しておくと、あとから確かめられます。

Nodd を使わない方法

方法の比較

プロンプトでの指示モデルが飛ばすと止まりません。補助として使います
MCP の承認ツール相談や選択に向きます。モデルが呼ばない、答えを無視することがあります
許可のルールモデルの外で確実に判定します。人に聞く場合は、その場で答える人が要ります
フックで人に聞くモデルの外で止め、人の判断を待ちます。期限と既定の拒否を自分で決めます
実行される側で確かめるエージェント以外の経路も止められます。システムごとに作り込みが要ります

Nodd で実装する

Nodd は、承認の依頼を iPhone に届け、Face ID での承認か却下を依頼したプログラムに返します。上のパターンのうち、どこで判定するかに合わせて 4 つの使い方があります。どれも nodd の CLI か送信キーを使います。

フックで強制する: nodd hook

Claude Code の PreToolUse フックです。ツールの実行前に承認を求め、記録された承認だけを許可します。却下、期限切れ、取り下げ、通信の失敗はすべて拒否です。却下の理由は Rejected in Nodd: 理由 としてエージェントに渡ります。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{"type": "command", "command": "nodd hook", "timeout": 1260}]
      }
    ]
  }
}

既定の応答期限は 300 秒で、nodd hook --expires-in 秒 で変えられます。timeout の決め方は Claude Code のフックをご覧ください。

モデルに相談させる: nodd mcp

nodd mcp は stdio の MCP サーバーです。Claude Code では claude mcp add nodd -- nodd mcp で追加し、ほかのクライアントでは次の設定を使います。

{"mcpServers":{"nodd":{"command":"nodd","args":["mcp"]}}}

使えるツールは 3 つです。

notify通知を送ります。応答は待ちません
ask承認か選択を求めます。options(2 から 4 個。省略すると承認)、input(入力欄)、expires_in(既定 3600)、wait_seconds(0 から 300、既定 50)を指定できます
check_answerask が返した id の回答を調べます

ask と check_answer は status として pending、approved、rejected、answered、expired、cancelled、failed を返し、却下の理由は note に入ります。wait_seconds のうちに答えがなければ pending になるので、check_answer で調べ直させます。MCP の回答はモデルが無視できます。実行を確実に止めたい操作はフックで止めます。input は Nodd 1.1 以降のアプリが必要です。

実行する側で止める: nodd ask

エージェントに直接コマンドを打たせず、決まったスクリプトを使わせる場合は、スクリプトの中に承認を入れます。nodd ask は承認なら終了コード 0、却下なら 10、応答なしで終われば 11 を返すので、set -e のスクリプトは承認のときだけ先へ進みます。

#!/bin/sh
# deploy.sh: the gate lives in the script, not in the agent's prompt
set -e
nodd ask "本番へ反映しますか" --body "$(git log -1 --oneline)" --level high --expires-in 900
./scripts/release.sh

却下の理由は標準エラー出力に Note: 理由 と出るので、エージェントはコマンドの出力として読めます。2 人の承認は --approvals 2、引き継ぎは --escalate-after 秒 で、どちらも Team だけです。終了コードの一覧は CLI にあります。

自分のエージェントから呼ぶ: API

自作のエージェントやサービスからは API を使います。expires_in が応答期限の秒数です。approvals_required で 1 から 3 人の承認を求められ、2 人以上は Team だけです。1 人でも却下すると確定します。Idempotency-Key を付けると、24 時間は同じキーに最初の結果を返すので、再送しても二重に届きません。

curl https://api.nodd.blipdrop.net/v1/messages \
  -H "Authorization: Bearer $NODD_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: task-913-drop-orders-archive" \
  -d '{
    "kind": "approval",
    "level": "high",
    "title": "orders_archive を削除します",
    "body": "DROP TABLE orders_archive;",
    "expires_in": 900,
    "approvals_required": 2
  }'

結果は GET /v1/messages/{id} で読み、response.option_id が approve のときだけ進めます。次は Python の標準ライブラリだけで書いた例です。例外も期限切れも「承認なし」として返します。

import json, os, time, urllib.request

API = "https://api.nodd.blipdrop.net"
KEY = os.environ["NODD_KEY"]

def call(method, path, body=None, headers=None):
    req = urllib.request.Request(
        API + path, method=method,
        data=None if body is None else json.dumps(body).encode(),
        headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json", **(headers or {})})
    with urllib.request.urlopen(req) as r:
        return json.load(r), r.headers.get("Retry-After")

def require_approval(title, body, key, expires_in=900):
    """Return (approved, note). Anything other than a recorded approval is a no."""
    try:
        msg, _ = call("POST", "/v1/messages",
                      {"kind": "approval", "level": "high", "title": title,
                       "body": body, "expires_in": expires_in},
                      {"Idempotency-Key": key})
        while True:
            state, retry = call("GET", f"/v1/messages/{msg['id']}")
            if state.get("action_state") != "open":
                break
            time.sleep(int(retry or 3))
    except Exception as e:
        return False, f"could not check approval: {e}"
    response = state.get("response") or {}
    return response.get("option_id") == "approve", response.get("note")

返した note をそのままエージェントへの次の入力に入れると、却下の理由に沿って計画を直せます。ポーリングの代わりに、署名付きの callback で受け取ることもできます。応答には承認した人、Face ID を通したかどうか(assurance)、端末の署名が付きます。

流れの図は応用例にあります。

注意点

よくある質問

AI エージェントのどの操作に人の承認が必要ですか。
取り消せない操作、外部に影響する操作、費用がかかる操作、権限や秘密に触れる操作が目安です。読み取りや手元の編集のように戻せる操作は、承認なしで進めたほうが人の注意を保てます。
MCP の承認ツールだけでは足りませんか。
確実に止めたい操作には足りません。モデルがツールを呼ばない、または答えを無視すると実行されてしまいます。フックや実行する側のシステムで判定します。
承認の応答がないときはどうすべきですか。
拒否として扱います。期限を必ず付け、期限切れ、取り下げ、通信の失敗では実行しないようにします。
却下したとき、エージェントに理由は伝わりますか。
Nodd では伝わります。nodd hook では Rejected in Nodd に続く理由として、MCP では note として、CLI では標準エラー出力の Note として、API では response.note として返ります。
2 人の承認は使えますか。
CLI の --approvals と API の approvals_required で、1 から 3 人の承認を求められます。2 人以上は Team だけです。nodd hook と MCP には人数の指定はありません。

Nodd は、スクリプトや CI、AI エージェントからの確認を iPhone に届け、Face ID での承認を送った側のプログラムへ返すアプリです。無料プランがあります。

関連する記事