deepseek.ac日本語で学ぶ、実践する

パターン

DeepSeekエージェントの権限と承認をどう設計するか — RBAC/ABACと承認ゲートの実装

· 約12分

リード

エージェントに「送る・書く・払う」を任せる前に決めるべきは、モデルの賢さではなく権限と承認の境界です。 この記事では、権限を静的な許可、承認を動的な停止として分離し、承認ゲートをツール実行層に置く設計を、Python のコードで示します。

前提

DeepSeek API の base_url・モデル名・パラメータ仕様は変更されることがあります。本記事のコードは OpenAI 互換 SDK を前提にした骨格なので、実際の値は公式ドキュメントで要確認してください。

本文

権限と承認は別物として設計する

混同すると設計が破綻します。

答える問い 実装場所 性質
権限 この主体はこの操作をしてよいか ポリシー判定関数 静的・事前に決まる
承認 今この操作を実行してよいか 実行直前のゲート 動的・人と状況に依存

権限だけでは足りません。権限は「できる」を表すだけで、「今やってよいか」は表さないからです。 逆に承認だけでも足りません。全操作に人間の承認を挟むと承認疲れが起き、担当者が中身を読まずに押すようになります。これは承認がないより危険です。

権限モデル: RBAC を土台に、条件は ABAC に逃がす

まずロールで大枠を作り、細かい条件は属性で表現します。ロールに条件を詰め込むとロール数が爆発するためです。

判定は 3 値にすると見通しが良くなります。allow / deny / require_approval の 3 つです。

from dataclasses import dataclass
from enum import Enum

class Decision(str, Enum):
    ALLOW = "allow"
    DENY = "deny"
    REQUIRE_APPROVAL = "require_approval"

@dataclass(frozen=True)
class Request:
    subject_role: str        # "agent:writer" など
    action: str              # "send_email" / "read_doc" など
    amount: int = 0          # 金額などのスカラ属性
    external: bool = False   # 外部への送信か
    irreversible: bool = False

# 上から評価し、最初に一致したルールを採用する
RULES = [
    # エージェントに権限管理そのものを触らせない
    (lambda r: r.subject_role.startswith("agent:")
               and r.action in {"grant_role", "delete_user"}, Decision.DENY),
    # 読み取りは承認不要
    (lambda r: r.action.startswith("read_"), Decision.ALLOW),
    # 外部送信・不可逆・高額は人間の承認を要求
    (lambda r: r.external or r.irreversible or r.amount >= 50_000,
     Decision.REQUIRE_APPROVAL),
]

def authorize(req: Request) -> Decision:
    for cond, decision in RULES:
        if cond(req):
            return decision
    return Decision.DENY  # 既定は拒否(allowlist 方式)

既定を deny にするのが要点です。新しいツールを追加したときに、ルールを書き忘れても「勝手に実行される」ことはなくなります。

承認ゲートはツール実行層に置く

最も多い失敗は、プロンプトに「危険な操作はしないでください」と書いて安心するパターンです。プロンプトは確率的な指示であり、保証ではありません。ゲートは必ずコード側、それもツールを実際に実行する直前に置きます。

操作カテゴリごとの既定は、次のように割り切ると運用が回ります。

操作の性質 既定 例
読み取り・冪等 承認不要 検索、ドキュメント取得
外部への送信 承認必須 メール送信、外部 API への POST
不可逆 承認必須 削除、公開、返金
高額・契約 多段階承認 金額閾値超え、対外文書の発行
権限変更 拒否(人間のみ) ロール付与、ユーザー削除

承認フローの形は 3 つを使い分けます。

DeepSeek 側の実装: 承認待ちで会話を止める

ツール呼び出しを受けたら、実行前に authorize() を通します。承認が必要なら実行せず、状態を永続化して停止します。

import json, time, uuid
from openai import OpenAI

client = OpenAI(
    api_key="...",                       # 環境変数から読む。直書きしない
    base_url="https://api.deepseek.com", # 公式ドキュメントで要確認
)

def to_request(tool_call) -> Request:
    args = json.loads(tool_call.function.arguments)
    return Request(
        subject_role="agent:writer",
        action=tool_call.function.name,
        amount=args.get("amount", 0),
        external=args.get("external", False),
        irreversible=args.get("irreversible", False),
    )

def tool_msg(call_id: str, payload: dict) -> dict:
    return {
        "role": "tool",
        "tool_call_id": call_id,
        "content": json.dumps(payload, ensure_ascii=False),
    }

def run_tools(tool_calls, messages, store) -> str:
    for tc in tool_calls:
        req = to_request(tc)
        decision = authorize(req)

        if decision is Decision.DENY:
            messages.append(tool_msg(tc.id, {"error": "denied by policy"}))
            continue

        if decision is Decision.REQUIRE_APPROVAL:
            record = {
                "approval_id": str(uuid.uuid4()),
                "request": req.__dict__,
                "tool_call": tc.model_dump(),
                "status": "pending",
                "expires_at": time.time() + 3600,
            }
            store.save(record)  # DB / キューに永続化
            messages.append(tool_msg(tc.id, {
                "status": "pending_approval",
                "approval_id": record["approval_id"],
            }))
            return "paused"

        messages.append(tool_msg(tc.id, execute(tc)))

    return "continue"

承認後に再開するときは、保存した tool_call_id に対して同じ ID でツール結果を返し、そのまま会話を続けます。ここで ID を変えるとモデル側の対応関係が壊れます。

def resume(record, messages, approved: bool):
    tc_id = record["tool_call"]["id"]
    if not approved:
        messages.append(tool_msg(tc_id, {"status": "rejected"}))
    else:
        messages.append(tool_msg(tc_id, execute(record["tool_call"])))
    return client.chat.completions.create(
        model="deepseek-chat",  # モデル名は公式ドキュメントで要確認
        messages=messages,
        tools=TOOLS,
    )

承認画面に「何が起きるか」を出す

承認設計で見落とされやすいのが承認者の情報不足です。承認者が権限の全体像を把握できないまま押すと、誤承認が起きます。実務では次の 3 点を承認リクエストに必ず含めます。

さらに、承認者自身のなりすましを防ぐため、承認操作は必ず人間の認証済みセッションから行い、エージェント経由の API では承認できないようにします。

記録と失効

つまずきポイント

症状 対処
承認待ちのまま会話が進まない ツール結果に pending_approval を返し、状態を永続化して停止する
同じ操作が二重実行される approval_id を冪等キーとして実行側で重複排除する
権限判定があちこちに散る authorize() 1 箇所に集約し、ツール追加時はここだけ更新する
ロールが増えすぎて説明できない ロールは職務のみ。条件は ABAC 属性に逃がす
エージェントが自分で承認してしまう 承認 API をエージェントの資格情報から呼べない構成にする
承認者が中身を見ずに押す 承認画面に差分・宛先・金額・該当ポリシーを表示する

まとめ

チェックリスト