做 Agent 的第一版,我把精力几乎都放在 prompt 上:怎么让模型选对工具、怎么让它别乱编、怎么让输出更稳定。

直到有一次,一个外部客户端接进来,我才发现真正难的不是“让它会用工具”,而是“让它只能用它该用的工具”。

问题不是“能不能调”,而是“凭什么能调”

一开始的思路很简单:给客户端一个 Key,能通过认证就放行。工具不多,先跑起来再说。

跑起来之后,问题一个接一个:

  • 这个 Key 能调检索工具,也能调写操作工具,权限太大;
  • Key 被贴在 URL 里,日志里全是明文;
  • 同一个调用重试了两次,计量记录写了两条;
  • 出问题查日志,只看到“调用成功”,不知道是谁、调了什么、为什么调。

这时我才意识到:Agent 的工具调用,本质上是 API 调用,只是调用方从人变成了模型。 人调接口要鉴权、限流、审计,模型调工具一样要,而且更不能省,因为模型会重试、会组合、会“自作主张”。

我先把“认证”和“授权”拆开

认证解决“你是谁”,授权解决“你能做什么”。第一版把这两件事混在一起,只要 Key 有效,所有工具都能调。

后来改成两层:

  1. 认证层:Key 只证明身份,不携带业务权限;
  2. 授权层:每个 Key 有工具白名单,调用前先检查工具名是否在范围内。

这个检查不能只放在 Agent 网关里。网关可以被绕过,调用方也可能直接打业务接口。所以业务侧还要再做一次校验,形成“双保险”。

几个当时没想到的坑

坑一:只校验“调用前”,不校验“调用后”。

调用前检查了权限,但调用失败、超时、重试之后,计量和审计没跟上。后来统一用 requestId 做幂等,同一个逻辑调用无论重试几次,只记一条。

坑二:把 Key 放在 URL 里。

有些客户端不支持自定义 Header,图省事就把 Key 拼在 query 上。结果访问日志、浏览器历史、代理日志里全是 Key。后来统一要求走 Header,网关负责把 query 里的 Key 转成 Header,并且日志脱敏。

坑三:工具描述写得太“诚实”。

工具描述是给模型看的,也是给攻击者看的。描述里如果把内部参数、路径、限额写得太细,反而方便别人构造调用。现在工具描述只保留“做什么、参数是什么、边界是什么”,不暴露内部实现。

坑四:写操作没有人工确认。

检索、查询这类只读工具可以自动;收藏、删除、发邮件、扣费这类写操作,必须有人确认。不是模型不可信,是一次错误调用的代价太高。

我现在会怎么设计

如果重新做一遍,我会先画一张表:

工具 读/写 谁能调 要不要确认 失败怎么处理
检索 读 所有 Key 否 重试 + 降级
查详情 读 所有 Key 否 返回空
收藏 写 指定 Key 是 幂等 + 审计
删除 写 管理员 是 二次确认 + 软删

这张表比 prompt 重要。prompt 决定“它想做什么”,这张表决定“它能不能做”。

最后一句

MCP 是个好协议,但它只解决“怎么接”,不解决“该不该接”。协议是通道,权限才是门。 门没装好,通道越顺,出事越快。