1. SecurityClaw 学习笔记/

07 Agent 设计的控制面:让不确定性可控

结论先说:Agent 真正可控,不靠模型听话,靠控制面把不确定性包起来 #

如果你只看 LLM 规划能力,很容易误以为 Agent 的核心是“模型够不够聪明”。但在 SecurityClaw 这种安全场景里,真正决定系统能不能上线的,不是规划层,而是控制面

控制面不是一个单独模块,而是一组边界:

  1. 权限边界:哪些动作允许执行
  2. 预算边界:能跑多少步、花多少 token
  3. 幂等边界:失败重试会不会造成重复副作用
  4. 人工确认边界:高风险动作何时必须暂停
  5. 可观测性边界:事后能不能解释自己做了什么

一、什么叫“控制面” #

你可以把 Agent 分成两层:

数据面(Data Plane)
- 真的去调工具
- 真的去查数据
- 真的写结果

控制面(Control Plane)
- 允许不允许执行
- 预算够不够
- 是否需要人工确认
- 是否应该停止
- 是否能复盘

很多失败的 Agent 设计,本质上不是模型差,而是控制面缺失。模型提出一个动作,系统就直接干了,没有中间审判层。


二、权限边界:manifest 不是安全边界,运行时才是 #

旧稿里最重要的一点其实非常对:permissions 是编排契约,不是最终沙箱。

permissions: Optional[List[PermissionRule]] = Field(
    default=None,
    description="声明技能需要的最小权限,执行前由编排层校验,不是安全边界"
)

这句话可以展开成一个更完整的模型:

# 伪代码:权限检查必须在执行前发生
class PermissionRule(BaseModel):
    action: Literal["read", "write", "external", "modify_file"]
    entities: list[str]


def validate_plan_permissions(plan: Plan, manifest: SkillManifest) -> bool:
    for step in plan.steps:
        if step.action not in manifest.allowed_actions:
            return False
        if step.entity not in manifest.allowed_entities:
            return False
    return True

为什么 manifest 不能替代数据库权限 #

因为 manifest 是框架层的声明,数据库权限是基础设施层的硬约束

这两层应该同时存在:

  • manifest 阻止模型提出明显越界动作
  • 数据库 / 文件系统 / 外部 API 自己的权限系统负责最后兜底

如果你把全部安全寄托在 manifest 上,那其实只是把风险前移,并没有真正消掉。


三、预算边界:系统什么时候必须说“到此为止” #

预算控制是控制面里最容易被忽略的一块,因为它看起来不像“安全”,更像“成本优化”。其实不是。

预算边界解决的是:

  • 无限重试
  • 过长推理链
  • 高成本工具被重复调用
  • 模型在失败路径里持续烧 token
# 伪代码:预算和步数都应该在控制面收口
if state["step_count"] >= state["max_steps"]:
    return StopReason("step_limit")

if token_budget.remaining <= 0:
    return StopReason("token_exhausted")

if tool_budget.remaining < tool_cost[plan.tool_name]:
    return StopReason("tool_budget_exceeded")

设计取舍 #

方案 A:只控制 max_steps #

优点:简单 缺点:不同技能成本差异被忽略

方案 B:步数 + token + 工具成本三层预算 #

优点:更真实地约束系统成本 缺点:系统更复杂,参数更多

SecurityClaw 目前更偏向 A 与 B 之间的折中。它已经意识到预算必须是运行时组件,但在粒度上还没有完全展开。这是合理的阶段性选择。


四、幂等性边界:重试不能变成重复副作用 #

如果一个 Agent 能发邮件、写数据库、改文件,那“重试”本身就是危险动作。

所以控制面还要负责判断:

这次失败后再试一遍,结果会不会变成重复写入?

# 伪代码:幂等执行记录
if execution_records.exists(idempotency_key):
    return execution_records.get(idempotency_key)

result = execute_tool(...)
execution_records.save(idempotency_key, result)
return result

这一步看起来像工程细节,但其实决定了 Agent 能不能安全地自动恢复。

如果没有幂等性:

  • 重试一次,写两次数据库
  • 重试一次,发两封邮件
  • 重试一次,删两次文件

那恢复机制会从“容错设计”变成“事故放大器”。


五、人工确认边界:高风险动作为什么不能自动通过 #

在安全场景里,模型可以规划,但某些动作不应该直接落地。

例如:

  • 删除数据
  • 外发内容
  • 修改权限
  • 写文件到敏感路径
  • 调高风险外部工具

这类动作必须让控制面接管:

# 伪代码:高风险动作先暂停
if manifest.risk_level == "high":
    request = HumanConfirmRequest(plan=step, timeout=30)
    approved = wait_for_confirmation(request)
    if not approved:
        return Denied("human confirmation timeout")

为什么这不是“多此一举” #

因为 LLM 再聪明,也不能替你承担组织责任。

控制面的意义,就是把“机器能规划”和“系统被允许执行”拆开。


六、可观测性边界:如果事后解释不了,就不算可控 #

一个可上线的 Agent,必须能回答这些问题:

  1. 为什么选了这个技能?
  2. 为什么拒绝了那个动作?
  3. 为什么在第 4 步停止?
  4. 为什么进入恢复路径?
  5. 哪一步花费最大?

这就要求控制面不仅要拦,还要记。

# 伪代码:控制面记录的是“决策证据”,不是原始大 payload
trace.append({
    "plan_summary": summarize(plan),
    "permission_result": permission_result,
    "budget_state": current_budget,
    "evaluation_reason": evaluation.reason,
    "duration_ms": duration,
})

为什么不直接全量记录 #

全量记录当然最省脑子,但问题是:

  • 敏感数据泄露风险高
  • trace 体积爆炸
  • checkpoint 变慢
  • 调试时噪音太大

所以合理设计是:记录可解释性摘要,而不是原始执行细节的无边界镜像。


七、控制面真正要统一的,是“停止权” #

如果把上面几条合并,你会发现控制面的最终职责不是“多做几次校验”,而是:

统一收回系统的停止权和执行权。

也就是说,模型只能提议,真正决定:

  • 能不能做
  • 做到哪里停
  • 失败后还能不能继续
  • 结果有没有资格进入下一轮

这些权力都必须收敛到控制面。

这才是“让不确定性可控”的真正含义。


八、这一篇要记住的框架 #

你可以把 Agent 控制面记成这五个问句:

权限:允许做吗?
预算:值得继续吗?
幂等:重试会重复伤害吗?
确认:高风险动作有人兜底吗?
可观测:事后解释得清吗?

如果一套 Agent 设计不能系统回答这五个问题,那它大概率只是“会循环的 LLM 工具调用器”,还不是可靠系统。


九、验证命令 #

pytest tests/test_permissions.py -v
pytest tests/ -k "budget or idempotent or confirm or trace" -v