给 Agent 加一道门:工具权限比 Prompt 难多了
做 Agent 的第一版,我把精力几乎都放在 prompt 上:怎么让模型选对工具、怎么让它别乱编、怎么让输出更稳定。
直到有一次,一个外部客户端接进来,我才发现真正难的不是“让它会用工具”,而是“让它只能用它该用的工具”。
问题不是“能不能调”,而是“凭什么能调”
一开始的思路很简单:给客户端一个 Key,能通过认证就放行。工具不多,先跑起来再说。
跑起来之后,问题一个接一个:
- 这个 Key 能调检索工具,也能调写操作工具,权限太大;
- Key 被贴在 URL 里,日志里全是明文;
- 同一个调用重试了两次,计量记录写了两条;
- 出问题查日志,只看到“调用成功”,不知道是谁、调了什么、为什么调。
这时我才意识到:Agent 的工具调用,本质上是 API 调用,只是调用方从人变成了模型。 人调接口要鉴权、限流、审计,模型调工具一样要,而且更不能省,因为模型会重试、会组合、会“自作主张”。
我先把“认证”和“授权”拆开
认证解决“你是谁”,授权解决“你能做什么”。第一版把这两件事混在一起,只要 Key 有效,所有工具都能调。
后来改成两层:
- 认证层:Key 只证明身份,不携带业务权限;
- 授权层:每个 Key 有工具白名单,调用前先检查工具名是否在范围内。
这个检查不能只放在 Agent 网关里。网关可以被绕过,调用方也可能直接打业务接口。所以业务侧还要再做一次校验,形成“双保险”。
几个当时没想到的坑
坑一:只校验“调用前”,不校验“调用后”。
调用前检查了权限,但调用失败、超时、重试之后,计量和审计没跟上。后来统一用 requestId 做幂等,同一个逻辑调用无论重试几次,只记一条。
坑二:把 Key 放在 URL 里。
有些客户端不支持自定义 Header,图省事就把 Key 拼在 query 上。结果访问日志、浏览器历史、代理日志里全是 Key。后来统一要求走 Header,网关负责把 query 里的 Key 转成 Header,并且日志脱敏。
坑三:工具描述写得太“诚实”。
工具描述是给模型看的,也是给攻击者看的。描述里如果把内部参数、路径、限额写得太细,反而方便别人构造调用。现在工具描述只保留“做什么、参数是什么、边界是什么”,不暴露内部实现。
坑四:写操作没有人工确认。
检索、查询这类只读工具可以自动;收藏、删除、发邮件、扣费这类写操作,必须有人确认。不是模型不可信,是一次错误调用的代价太高。
我现在会怎么设计
如果重新做一遍,我会先画一张表:
| 工具 | 读/写 | 谁能调 | 要不要确认 | 失败怎么处理 |
|---|---|---|---|---|
| 检索 | 读 | 所有 Key | 否 | 重试 + 降级 |
| 查详情 | 读 | 所有 Key | 否 | 返回空 |
| 收藏 | 写 | 指定 Key | 是 | 幂等 + 审计 |
| 删除 | 写 | 管理员 | 是 | 二次确认 + 软删 |
这张表比 prompt 重要。prompt 决定“它想做什么”,这张表决定“它能不能做”。
最后一句
MCP 是个好协议,但它只解决“怎么接”,不解决“该不该接”。协议是通道,权限才是门。 门没装好,通道越顺,出事越快。