07 Agent 设计的控制面:让不确定性可控
结论先说:Agent 真正可控,不靠模型听话,靠控制面把不确定性包起来 #
如果你只看 LLM 规划能力,很容易误以为 Agent 的核心是“模型够不够聪明”。但在 SecurityClaw 这种安全场景里,真正决定系统能不能上线的,不是规划层,而是控制面。
控制面不是一个单独模块,而是一组边界:
- 权限边界:哪些动作允许执行
- 预算边界:能跑多少步、花多少 token
- 幂等边界:失败重试会不会造成重复副作用
- 人工确认边界:高风险动作何时必须暂停
- 可观测性边界:事后能不能解释自己做了什么
一、什么叫“控制面” #
你可以把 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,必须能回答这些问题:
- 为什么选了这个技能?
- 为什么拒绝了那个动作?
- 为什么在第 4 步停止?
- 为什么进入恢复路径?
- 哪一步花费最大?
这就要求控制面不仅要拦,还要记。
# 伪代码:控制面记录的是“决策证据”,不是原始大 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