Agent 能自主执行命令、修改代码、部署服务——但不是每个操作都应该自动通过。本文设计一套生产级 Agent 审批系统,覆盖审批策略、审批事件、权限角色、超时处理和审批后的执行上下文恢复,让 Agent 在"自主"和"受控"之间找到精确平衡。

Agent 审批系统:高风险命令、数据库迁移与生产发布门禁

Agent 能自主执行命令、修改代码、部署服务——但不是每个操作都应该自动通过。本文设计一套生产级 Agent 审批系统,覆盖审批策略、审批事件、权限角色、超时处理和审批后的执行上下文恢复,让 Agent 在"自主"和"受控"之间找到精确平衡。

目录

一、为什么 Agent 需要审批

在上一篇文章中,我们设计了 Agent 执行器的命令代理(Command Proxy),将 Agent 发起的命令按风险分为 SAFE、MODERATE、HIGH、FORBIDDEN 四个等级。SAFE 和 MODERATE 级别的命令可以自动放行,FORBIDDEN 直接拒绝——但 HIGH 级别的命令需要一个关键机制:人工审批

1.1 没有审批的 Agent 平台等于定时炸弹

先看一个真实场景链路:

text
2026-06-01 09:15  Agent 接到任务:"优化数据库查询性能"
2026-06-01 09:16  Agent 分析慢查询,发现需要添加联合索引
2026-06-01 09:17  Agent 执行 ALTER TABLE orders ADD INDEX idx_user_date (user_id, created_at)
2026-06-01 09:17  索引构建锁表 45 秒,线上订单服务 503
2026-06-01 09:18  Agent 觉得索引不够,执行 DROP INDEX idx_old_status 删除旧索引
2026-06-01 09:18  旧索引被删除,依赖该索引的报表查询全部超时

这条链路中,Agent 每一步都是"正确的"——它确实在优化性能。但问题在于:数据库 DDL 操作属于高风险变更,应该有审批门禁。如果有一个审批系统,在第 3 步暂停执行、等待 DBA 确认,整个事故就不会发生。

1.2 审批不是限制 Agent,而是保护团队

很多开发者对"审批"有抵触——觉得它拖慢了 Agent 的自主性。但换一个角度:审批系统保护的不是代码,而是团队的信任和协作边界

场景 无审批 有审批
Agent 修改核心模块 直接改,Reviewer 事后发现 暂停,Reviewer 确认后执行
Agent 执行数据库迁移 直接跑 migration,可能锁表 DBA 审批后在低峰期执行
Agent 部署到生产环境 直接 deploy,无人知晓 运维审批 + 灰度发布
Agent 删除 Git 分支 直接删,可能误删 release 分支 分支保护规则拦截,人工确认

审批系统的核心设计目标是:在 Agent 的自主性和团队的安全边界之间,建立一套可编程、可审计、可回退的决策机制

二、审批系统总体架构

审批系统不是一个独立的微服务,而是嵌入在执行器 Command Proxy 中的一个决策层。它的架构如下:

text
                    Agent 发起命令
                         │
                         ▼
              ┌─────────────────────┐
              │   Command Proxy     │
              │   (命令分级)        │
              └──────────┬──────────┘
                         │
              ┌──────────▼──────────┐
              │  Risk Classification │
              │  SAFE → 直接执行     │
              │  MODERATE → 审计执行  │
              │  FORBIDDEN → 拒绝    │
              │  HIGH → 进入审批流程  │
              └──────────┬──────────┘
                         │ (HIGH)
                         ▼
              ┌─────────────────────┐
              │   Approval Engine   │
              │                     │
              │  1. 匹配审批策略     │
              │  2. 生成审批事件     │
              │  3. 通知审批人       │
              │  4. 暂停执行器       │
              └──────────┬──────────┘
                         │
              ┌──────────▼──────────┐
              │   Approval Gate     │
              │                     │
              │  approved → 恢复执行 │
              │  rejected → 拒绝+记录│
              │  timeout  → 按策略   │
              └─────────────────────┘

三个核心组件各司其职:

  • Approval Engine(审批引擎):根据命令类型、风险等级、发起人、目标环境等维度匹配审批策略,生成审批事件
  • Approval Gate(审批门禁):阻塞执行器等待审批结果,处理超时和拒绝
  • Approval Store(审批存储):持久化所有审批事件,支持审计和回放

三、审批策略引擎

审批策略是整套系统的灵魂。它决定了哪些操作需要审批、谁来审批、超时怎么处理。

3.1 策略配置格式

我们用 YAML 定义审批策略,支持多维度匹配和优先级排序:

yaml
# approval-policies.yaml
policies:
  # 策略 1:数据库 DDL 操作需要 DBA 审批
  - name: "database-ddl"
    priority: 10
    match:
      command_patterns:
        - "ALTER TABLE*"
        - "DROP TABLE*"
        - "CREATE INDEX*"
        - "DROP INDEX*"
        - "npm run migrate*"
        - "python manage.py migrate*"
      risk_level: "high"
    approval:
      required_roles: ["dba", "tech-lead"]
      min_approvers: 1
      timeout_secs: 1800          # 30 分钟
      timeout_action: "reject"    # 超时视为拒绝
      scope: "per-command"        # 每条命令单独审批

  # 策略 2:生产环境部署需要运维审批
  - name: "production-deploy"
    priority: 20
    match:
      command_patterns:
        - "kubectl apply*"
        - "helm upgrade*"
        - "docker push*prod*"
        - "npm run deploy*"
      target_env: "production"
    approval:
      required_roles: ["devops", "sre"]
      min_approvers: 1
      timeout_secs: 3600          # 1 小时
      timeout_action: "reject"
      scope: "per-task"           # 同一任务的部署命令共享审批

  # 策略 3:Git 高风险操作需要 Tech Lead 审批
  - name: "git-dangerous"
    priority: 15
    match:
      command_patterns:
        - "git push --force*"
        - "git push*main"
        - "git push*master"
        - "git branch -D*"
        - "git reset --hard*"
    approval:
      required_roles: ["tech-lead"]
      min_approvers: 1
      timeout_secs: 600
      timeout_action: "reject"
      scope: "per-command"

  # 策略 4:敏感文件修改需要安全团队审批
  - name: "sensitive-files"
    priority: 25
    match:
      file_patterns:
        - "**/auth/**"
        - "**/crypto/**"
        - "**/.env*"
        - "**/secrets/**"
        - "**/Dockerfile"
    approval:
      required_roles: ["security"]
      min_approvers: 1
      timeout_secs: 1800
      timeout_action: "escalate"  # 超时升级到更高级别
      scope: "per-command"

  # 默认策略:未匹配的高风险命令
  - name: "default-high-risk"
    priority: 100
    match:
      risk_level: "high"
    approval:
      required_roles: ["tech-lead", "admin"]
      min_approvers: 1
      timeout_secs: 900
      timeout_action: "reject"
      scope: "per-command"

3.2 策略匹配算法

策略引擎按优先级从高到低匹配,第一个命中的策略生效:

python
import fnmatch
from dataclasses import dataclass
from typing import Optional

@dataclass
class ApprovalRequest:
    """审批请求"""
    task_id: str
    command: str
    risk_level: str
    target_files: list[str]
    target_env: str
    requested_by: str
    agent_id: str

@dataclass
class ApprovalPolicy:
    """审批策略"""
    name: str
    priority: int
    match_rules: dict
    approval_config: dict

class PolicyEngine:
    """审批策略引擎"""

    def __init__(self, policies: list[ApprovalPolicy]):
        # 按优先级排序,数字小的先匹配
        self.policies = sorted(policies, key=lambda p: p.priority)

    def match(self, request: ApprovalRequest) -> Optional[ApprovalPolicy]:
        """找到第一个匹配的策略"""
        for policy in self.policies:
            if self._matches(policy, request):
                return policy
        return None

    def _matches(self, policy: ApprovalPolicy, req: ApprovalRequest) -> bool:
        rules = policy.match_rules

        # 命令模式匹配
        if "command_patterns" in rules:
            cmd_match = any(
                fnmatch.fnmatch(req.command, pat)
                for pat in rules["command_patterns"]
            )
            if not cmd_match:
                return False

        # 文件模式匹配
        if "file_patterns" in rules:
            file_match = any(
                fnmatch.fnmatch(f, pat)
                for f in req.target_files
                for pat in rules["file_patterns"]
            )
            if not file_match:
                return False

        # 风险等级匹配
        if "risk_level" in rules:
            if req.risk_level != rules["risk_level"]:
                return False

        # 目标环境匹配
        if "target_env" in rules:
            if req.target_env != rules["target_env"]:
                return False

        return True

3.3 Scope Override:同一任务的命令聚合

审批策略中有一个容易忽略的字段 scope

  • per-command:每条命令独立审批。适用于数据库 DDL——每条 ALTER 都应该单独确认
  • per-task:同一任务内的同类命令共享审批。适用于部署场景——审批一次 deploy,后续的 health check、rollback 准备不需要重复审批
python
class ApprovalSessionManager:
    """管理审批会话,支持 scope override"""

    def __init__(self, store):
        self.store = store

    def get_or_create_session(
        self, request: ApprovalRequest, policy: ApprovalPolicy
    ) -> str:
        """获取或创建审批会话,返回 session_id"""
        scope = policy.approval_config.get("scope", "per-command")

        if scope == "per-task":
            # 查找同一任务是否已有同策略的审批会话
            existing = self.store.find_session(
                task_id=request.task_id,
                policy_name=policy.name,
                status="active"
            )
            if existing:
                return existing.session_id

        # 创建新会话
        return self.store.create_session(
            task_id=request.task_id,
            policy_name=policy.name,
            command=request.command,
            requested_by=request.requested_by
        )

四、审批事件与权限模型

4.1 审批事件数据模型

每一次审批请求都会生成一个审批事件,记录完整的决策链路:

sql
CREATE TABLE approval_events (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id      UUID NOT NULL,           -- 审批会话 ID
    task_id         UUID NOT NULL,           -- 关联任务 ID
    policy_name     VARCHAR(64) NOT NULL,    -- 匹配的策略名称
    -- 审批内容
    command         TEXT NOT NULL,           -- 请求执行的命令
    risk_level      VARCHAR(16) NOT NULL,    -- 风险等级
    context         JSONB,                   -- 附加上下文(diff、影响范围等)
    -- 审批流程
    status          VARCHAR(16) NOT NULL DEFAULT 'pending',
                    -- pending → approved / rejected / timeout / escalated
    requested_by    VARCHAR(64) NOT NULL,    -- 请求人(Agent ID 或用户)
    approved_by     VARCHAR(64),             -- 审批人
    approved_at     TIMESTAMPTZ,             -- 审批时间
    reason          TEXT,                    -- 审批意见(批准理由或拒绝原因)
    -- 超时控制
    timeout_at      TIMESTAMPTZ NOT NULL,    -- 超时时间
    timeout_action  VARCHAR(16) NOT NULL DEFAULT 'reject',
    -- 审计
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 查询索引
CREATE INDEX idx_approval_task ON approval_events (task_id, status);
CREATE INDEX idx_approval_pending ON approval_events (status, timeout_at)
    WHERE status = 'pending';

4.2 权限角色模型

审批系统使用基于角色的权限控制(RBAC),角色和权限通过配置文件管理:

yaml
# approval-roles.yaml
roles:
  tech-lead:
    can_approve:
      - "database-ddl"
      - "git-dangerous"
      - "default-high-risk"
    can_reject: true
    can_escalate: true

  dba:
    can_approve:
      - "database-ddl"
    can_reject: true
    can_escalate: false

  devops:
    can_approve:
      - "production-deploy"
    can_reject: true
    can_escalate: true

  sre:
    can_approve:
      - "production-deploy"
    can_reject: true
    can_escalate: true

  security:
    can_approve:
      - "sensitive-files"
    can_reject: true
    can_escalate: true

  admin:
    can_approve: ["*"]      # 可以审批所有策略
    can_reject: true
    can_escalate: true

4.3 审批 API

审批人通过 REST API 或 Webhook 进行审批操作:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class ApprovalDecision(BaseModel):
    decision: str          # "approved" | "rejected" | "escalated"
    approver: str          # 审批人 ID
    reason: str = ""       # 审批意见

@app.post("/api/approvals/{event_id}/decide")
async def submit_decision(event_id: str, decision: ApprovalDecision):
    """提交审批决策"""
    event = await store.get_event(event_id)
    if not event:
        raise HTTPException(404, "审批事件不存在")
    if event.status != "pending":
        raise HTTPException(409, f"审批已结束,当前状态: {event.status}")

    # 验证审批人权限
    policy = await get_policy(event.policy_name)
    role = await get_user_role(decision.approver)
    if not can_approve(role, policy.name):
        raise HTTPException(403, "无权审批此策略")

    # 更新审批事件
    await store.update_event(event_id, {
        "status": decision.decision,
        "approved_by": decision.approver,
        "approved_at": "NOW()",
        "reason": decision.reason,
    })

    # 通知执行器恢复或终止
    await notify_executor(event.task_id, decision.decision)

    return {"status": decision.decision, "event_id": event_id}

五、超时处理与审批后执行上下文

5.1 超时处理策略

审批请求可能长时间无人响应。每个策略都配置了 timeout_action

超时动作 说明 适用场景
reject 超时自动拒绝 数据库 DDL、Git 操作
escalate 升级到更高级别审批人 安全敏感文件
approve 超时自动批准(慎用) 低风险但必须有人知晓的操作
notify_and_wait 再次通知并继续等待 关键部署

超时检测由一个后台定时任务驱动:

python
import asyncio
from datetime import datetime, timezone

class ApprovalTimeoutWatcher:
    """审批超时检测器"""

    def __init__(self, store, check_interval: int = 30):
        self.store = store
        self.check_interval = check_interval

    async def run(self):
        """后台循环检测超时"""
        while True:
            now = datetime.now(timezone.utc)
            expired = await self.store.find_expired(now)

            for event in expired:
                await self._handle_timeout(event)

            await asyncio.sleep(self.check_interval)

    async def _handle_timeout(self, event):
        action = event.timeout_action

        if action == "reject":
            await self.store.update_event(event.id, {
                "status": "timeout",
                "reason": f"审批超时({event.timeout_at}),自动拒绝"
            })
            await notify_executor(event.task_id, "rejected")
            await notify_requester(event, "timeout_reject")

        elif action == "escalate":
            # 升级到 admin 角色
            await self.store.update_event(event.id, {
                "status": "pending",
                "policy_name": "escalated-to-admin",
                "timeout_at": "NOW() + interval '30 minutes'"
            })
            await notify_role("admin", event, "escalated")

        elif action == "notify_and_wait":
            # 再次通知,延长超时
            await self.store.update_event(event.id, {
                "timeout_at": "NOW() + interval '30 minutes'"
            })
            await notify_approvers(event, "reminder")

5.2 审批后的执行上下文恢复

审批通过后,执行器需要从暂停状态恢复执行。这里有一个关键设计:审批结果要注入 Agent 的执行上下文

python
class ExecutorResumer:
    """审批后恢复执行器"""

    async def resume(self, task_id: str, decision: str, context: dict):
        """恢复被审批暂停的任务"""
        executor = await get_executor(task_id)

        if decision == "approved":
            # 将审批意见注入 Agent 上下文
            approval_context = {
                "type": "approval_result",
                "status": "approved",
                "approver": context["approver"],
                "reason": context.get("reason", ""),
                "additional_instructions": context.get("instructions", "")
            }

            # 如果审批人有附加指令,Agent 需要遵守
            # 例如:"索引创建请在业务低峰期执行" → Agent 应等待或调整执行策略
            await executor.inject_context(approval_context)
            await executor.resume_command()

        elif decision == "rejected":
            # 拒绝时,Agent 需要知道原因并调整策略
            rejection_context = {
                "type": "approval_result",
                "status": "rejected",
                "reason": context.get("reason", "审批被拒绝"),
                "suggestion": context.get("suggestion", "")
            }
            await executor.inject_context(rejection_context)
            # Agent 收到拒绝后应该:
            # 1. 跳过当前命令,尝试替代方案
            # 2. 如果没有替代方案,标记任务失败
            await executor.skip_and_continue()

5.3 审批事件通知

审批系统需要多种通知渠道来确保审批人能及时响应:

yaml
# notification-channels.yaml
notifications:
  pending_approval:
    channels:
      - type: slack
        target: "#approvals"
        template: |
          🔔 Agent 审批请求
          任务: {{task_title}}
          命令: {{command}}
          风险: {{risk_level}}
          策略: {{policy_name}}
          [批准]({{approve_url}}) | [拒绝]({{reject_url}})

      - type: email
        target: "{{approver_emails}}"
        subject: "[Agent 审批] {{policy_name}} - {{task_title}}"

      - type: webhook
        url: "https://internal-tools.example.com/agent-approvals"
        method: POST

  timeout_warning:
    # 超时前 5 分钟发提醒
    trigger_before_secs: 300
    channels:
      - type: slack
        target: "{{approver_dm}}"
        template: "⏰ 审批即将超时:{{command}}"

六、真实经验与踩坑

6.1 审批风暴:批量任务导致审批人崩溃

场景:Agent 接到一个大型重构任务,涉及 30 个文件的修改,每个文件都匹配了 sensitive-files 策略。审批人在 10 分钟内收到了 30 条审批通知。

问题:审批人不可能逐条审查 30 个命令,要么全部批准(失去审批意义),要么全部拒绝(阻塞工作)。

解决:引入 scope: per-task 聚合 + 批量审批 UI。同一任务内同策略的命令自动聚合为一个审批请求,审批人看到的是"Agent 要对 auth 模块做 12 处修改,这是完整的 diff",而不是 12 条独立的命令。

python
# 批量审批:审批人看到聚合后的完整变更
{
    "session_id": "sess-001",
    "task_title": "重构认证模块",
    "policy": "sensitive-files",
    "commands_count": 12,
    "files_affected": ["auth/login.py", "auth/session.py", ...],
    "full_diff": "...(完整 diff)...",
    "decision": "approved",
    "reason": "变更合理,测试覆盖充分"
}

6.2 审批上下文缺失导致 Agent 反复请求

场景:DBA 拒绝了 Agent 的 DROP INDEX 请求,Agent 收到拒绝后换了一种写法 ALTER TABLE ... DROP CONSTRAINT,再次触发审批。

问题:Agent 不知道拒绝的原因,只知道自己被拒了,于是尝试绕过。

解决:拒绝时必须附带结构化的拒绝原因和建议,Agent 会将拒绝原因作为约束条件继续工作。

json
{
    "decision": "rejected",
    "reason": "该索引被报表服务依赖,删除会导致报表超时",
    "suggestion": "请先添加替代索引后再删除旧索引,建议索引: idx_user_date_status",
    "constraints": [
        "不要删除 idx_old_status 索引",
        "新索引必须覆盖 user_id + created_at 的查询"
    ]
}

6.3 超时自动拒绝在低峰期不适用

场景:Agent 在凌晨 2 点触发了一个数据库迁移审批,超时设置 30 分钟。没有人会在凌晨审批,结果自动拒绝了。第二天开发者手动重新提交。

问题:固定超时 + reject 策略在非工作时间不友好。

解决:增加"工作时间感知"——超时计时只在工作时间内累计。凌晨 2 点的请求,超时从早上 9 点开始计算。

python
from datetime import time

def effective_timeout(request_time, timeout_secs, work_hours=(time(9, 0), time(18, 0))):
    """计算考虑工作时间的有效超时时间"""
    current = request_time.time()
    start, end = work_hours

    if current < start:
        # 工作开始前,从工作开始时间算起
        delta = (datetime.combine(request_time.date(), start) - request_time).total_seconds()
        return timeout_secs + delta
    elif current > end:
        # 工作结束后,从明天工作开始时间算起
        tomorrow = request_time.date() + timedelta(days=1)
        delta = (datetime.combine(tomorrow, start) - request_time).total_seconds()
        return timeout_secs + delta
    else:
        return timeout_secs

七、参数说明表

参数 类型 默认值 说明
policy.priority Integer 100 策略优先级,数字越小越先匹配
policy.match.command_patterns List 命令通配符列表,支持 *?
policy.match.file_patterns List 文件路径通配符列表
policy.match.risk_level String 匹配的风险等级
policy.match.target_env String 匹配的目标环境
approval.required_roles List 可以审批的角色列表
approval.min_approvers Integer 1 最少审批人数
approval.timeout_secs Integer 900 审批超时秒数
approval.timeout_action String reject 超时动作:reject / escalate / approve / notify_and_wait
approval.scope String per-command 审批范围:per-command / per-task
notification.channels List 通知渠道配置(slack / email / webhook)
work_hours.start Time 09:00 工作时间开始,非工作时间的超时暂停计算
work_hours.end Time 18:00 工作时间结束

八、落地检查清单

  • 策略覆盖完整:所有 HIGH 级别命令都有对应的审批策略,不存在"漏网之鱼"
  • 策略优先级正确:高优先级策略(DDL、部署)先于默认策略匹配
  • 审批暂停:Agent 发起需审批的命令后,执行器暂停等待,不会跳过继续执行后续命令
  • 审批恢复:审批通过后,执行器从暂停位置恢复执行,且审批意见已注入 Agent 上下文
  • 拒绝处理:审批拒绝后,Agent 收到结构化拒绝原因和建议约束,不会尝试绕过
  • 超时机制:超时检测器正常运行,超时动作按策略执行(reject / escalate)
  • 工作时间感知:非工作时间的审批请求,超时计时从下一个工作日开始
  • 批量聚合:同一任务内同策略的命令可以聚合为一个审批请求
  • 通知到达:审批通知通过 Slack / Email / Webhook 等渠道送达审批人
  • 权限控制:只有拥有对应角色的用户可以审批,越权操作返回 403
  • 审计完整:每个审批事件记录完整的请求、策略、决策、审批人、时间和原因
  • 回放能力:可以从审批事件表回溯任意任务的审批历史