首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent 权限工程的三个断层:工具白名单、参数级授权与累积权限

Agent 权限工程的三个断层:工具白名单、参数级授权与累积权限

原创
作者头像
IT大佬 jzit-top
发布于 2026-11-09 18:26:30
发布于 2026-11-09 18:26:30
270
举报

生产环境里的 Agent 出问题,很少是因为"模型太笨",更多是因为模型做了它被允许做、但不该在这个时刻以这个参数做的事。工具白名单解决的是"能不能调用这个函数",它解决不了"这次调用的参数是否越界""连续五步操作累积起来是否构成了一个不该被自动批准的业务动作"。Agent 的权限问题本质上是运行时授权问题,而绝大多数框架只提供了静态注册,没有提供执行前的动态门禁。下面三个断层,每一个都能用一段代码在调用真正落地之前拦住。

断层一:工具注册表是"能力清单",不是"授权策略"

几乎所有 Agent 框架都有工具注册机制:把函数挂到 registry 上,模型就能看到并调用。这个机制经常被误当作权限控制,但它只回答"存在哪些工具",不回答"当前会话能不能用这个工具"。一个客服 Agent 和财务 Agent 共享同一套工具注册表,如果不做会话级过滤,模型完全可能在错误的场景下调用退款接口。

正确的做法是在注册表之上再加一层会话级能力过滤,把工具集合按角色裁剪后再交给模型:

代码语言:javascript
复制
from dataclasses import dataclass, field

@dataclass
class ToolSpec:
    name: str
    fn: callable
    risk: str                      # low / medium / high
    roles: set[str] = field(default_factory=set)

REGISTRY = {
    "search_kb":   ToolSpec("search_kb", lambda q: [], "low", {"support", "finance"}),
    "query_order": ToolSpec("query_order", lambda oid: {}, "medium", {"support"}),
    "refund":      ToolSpec("refund", lambda oid, amt: {}, "high", {"finance"}),
}

def tools_for_role(role: str) -> list[dict]:
    """只暴露当前角色可用的工具给模型,未授权工具不出现在 schema 中"""
    visible = [t for t in REGISTRY.values() if role in t.roles]
    return [{"type": "function",
             "function": {"name": t.name, "description": f"risk={t.risk}"}}
            for t in visible]

def dispatch(role: str, name: str, args: dict) -> dict:
    spec = REGISTRY.get(name)
    if spec is None or role not in spec.roles:
        return {"ok": False, "error": "unauthorized_tool",
                "tool": name, "role": role}
    return {"ok": True, "result": spec.fn(**args)}

关键设计是 未授权工具根本不进入模型可见的 schema。只靠 dispatch 做拒绝是不够的——模型看到一个工具存在,就会尝试调用它,被拒绝后可能换个参数重试,浪费轮次。从源头上不暴露,比在调用时拒绝更干净。risk 字段不是装饰,它决定了下一步的门禁强度。

断层二:工具名合法,参数越界,等于授权失效

refund 在角色白名单里,不代表 refund(order_id="...", amount=999999) 应该被执行。工具级别的授权粒度太粗,真实的风险几乎都发生在参数空间里:金额上限、订单归属、操作频率、目标对象是否属于当前用户。这些约束无法写进角色表,必须在执行前用参数校验器拦截:

代码语言:javascript
复制
from pydantic import BaseModel, Field, field_validator

class RefundArgs(BaseModel):
    order_id: str
    amount: float = Field(gt=0, le=5000)          # 单笔硬上限
    currency: str = "CNY"

    @field_validator("order_id")
    @classmethod
    def order_must_belong_to_session(cls, v, info):
        ctx = info.context or {}
        if v not in ctx.get("owned_orders", set()):
            raise ValueError("order not owned by current session")
        return v

def guarded_refund(role: str, args: dict, session_ctx: dict) -> dict:
    spec = REGISTRY["refund"]
    if role not in spec.roles:
        return {"ok": False, "error": "unauthorized_tool"}
    try:
        parsed = RefundArgs.model_validate(args)
    except Exception as e:
        return {"ok": False, "error": "param_violation",
                "detail": str(e), "retryable": True}
    return {"ok": True, "result": spec.fn(**parsed.model_dump())}

Field(gt=0, le=5000) 和 owned_orders 校验代表了两类不同的约束:一类是业务规则(单笔退款上限),一类是会话归属(只能操作自己名下的订单)。后者尤其重要,因为它是防越权的最后一道:即使模型从上下文里捡到了一个别人的订单号,参数校验也会拒绝。retryable: True 让模型知道"换个合法参数还有机会",而不是陷入无效重试。

断层三:单步合法,多步累积越界

最隐蔽的权限问题是每一步都通过了授权,但五步连起来构成了一次不该自动执行的操作。Agent 反复调用 refund,每次金额 4999,单笔都在上限内,十分钟内累计退了二十万——这个动作在单步校验里完全合法,在业务意义上是一次事故。同理,连续调用 update_config 修改不同字段,每一步都是"小的、合法的"修改,合起来是一次大规模配置变更。

工程上的解法是引入会话级累积计数器与熔断阈值,把"单次授权"扩展为"带预算的授权":

代码语言:javascript
复制
from collections import defaultdict

class SessionBudget:
    def __init__(self):
        self.counts = defaultdict(int)
        self.sums = defaultdict(float)

    def check(self, tool: str, args: dict, limits: dict) -> dict:
        limits = limits.get(tool, {})
        max_calls = limits.get("max_calls")
        if max_calls and self.counts[tool] >= max_calls:
            return {"allow": False, "reason": "call_budget_exceeded",
                    "tool": tool, "used": self.counts[tool]}

        if "sum_field" in limits:
            f = limits["sum_field"]
            projected = self.sums[tool] + float(args.get(f, 0))
            if projected > limits["max_sum"]:
                return {"allow": False, "reason": "sum_budget_exceeded",
                        "tool": tool, "projected": projected}
            self.sums[tool] = projected

        self.counts[tool] += 1
        return {"allow": True, "used": self.counts[tool]}

LIMITS = {"refund": {"max_calls": 3, "sum_field": "amount", "max_sum": 8000}}

projected 的检查必须在递增之前完成,这个顺序决定了熔断发生在越界的那一次调用之前,而不是之后。LIMITS 里的 max_calls 和 max_sum 应该由业务方定义,而不是由 Agent 开发者拍脑袋——它们是业务授权规则的技术表达,不是工程优化的参数。

这三个断层的共同逻辑是:Agent 的权限不是一个静态属性,而是一条随时间、参数和累积量变化的运行时状态。工具白名单控制"能否触及",参数校验控制"这次是否越界",累积预算控制"多步之后是否已经出格"。把这三层拆开分别实现,比试图用一个"超级授权函数"包揽一切要可靠得多——因为每一层的失败模式不同,对应的审计日志和告警也应该分开。Agent 能做多少事,取决于你允许它在什么条件下做多少,而不是取决于它在注册表里看见了什么。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 断层一:工具注册表是"能力清单",不是"授权策略"
  • 断层二:工具名合法,参数越界,等于授权失效
  • 断层三:单步合法,多步累积越界
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档