第 6 章 工具与 MCP:把外部能力标准化接进来
难度 ★4 | 约 3.5 学时 | 6 节
这一章讲「模型伸出去的手」是怎么接上的。前半章管住手(数量与权限),后半章管住接口(协议与边界)——两件事都不做,接得再顺也是风险。
学习目标:理解工具调用与 MCP 在架构上的位置,并能判断「该不该引入 MCP」。
本章交付物:一份工具清单(名称 / 权限 / 副作用 / 幂等性)+ 一次接入记录
6.1 工具是模型伸向系统的手
「工具是模型伸向系统的手」这个说法是本书的比喻,先明标身份:它是帮助理解用的,不作论据。
为什么还留着它?因为它抓到了一个真实的比例关系:手越多、权限越大,出错的影响面越大。 而这一点有官方材料作证——风险清单在 2026 版里把「过度代理」从第 6 位升到第 3 位,这是该版最大的排名变动。清单给的原因很直接:AI 系统越来越多地拿到工具、接口与业务系统的访问权。
但请注意引用纪律:只引条目名称与它落在哪一层,不引排名当论据。 排名每年变,去年它是第 6。拿一个会变的数当论证,等于给结论装了一个过期日期。
隐喻之后要接的是可检查的东西。官方给这套风险配的处置建议是五条:
① 限制代理可调用的工具。 ② 限制每个工具能提供的功能。 ③ 让工具只拿到完成个别操作所需的最低权限。 ④ 授权与权限检查由模型之外的机制执行。 ⑤ 高影响或不可逆的操作,执行前要求人工核准。
五条合起来做的是同一件事:把「模型能做什么」从模型手里拿走,放回你的代码里。 这也正是第 3.4 节那句话的另一面——模型只声明意图,执行在你这里。
那么本章的交付物是什么?一份工具清单,每项至少四格:名称 / 权限 / 副作用 / 幂等性。
清单的判据只有一条,而且很残酷:每一项都要能回答「为什么必须有它」。
答不出来的那一个,就是该删的那一个。请注意这条判据的用法——它不问你「有没有用」,它问你「为什么必须」。 一个「有时候挺方便」的工具,在这一问下会立刻露馅:它带来的便利,抵不过它带来的那一份攻击面与那一份 token 成本(3.4 节讲过,工具定义每次调用都要付费)。
最后一句:「能接」和「该接」是两件事。 前者是技术问题,后者是这一节的问题——而后者更难,因为它没有报错。
6.2 MCP 解决的是「接一次、到处能用」,它住在集成层
「住在集成层」这个定位,不是本书画的架构图,它可以被规范里的一句话证实。
现行规范把服务器对客户端暴露的东西分成三类:工具、资源与提示模板。这正是「一个进程把能力按统一形状暴露出去」的定义。而清单类接口还带有效期与缓存范围——客户端可以在重连之后继续用缓存好的清单。这一条很说明问题:只有「清单是稳定的」这件事成立,缓存才有意义。
还有一条更硬的侧证,值得单独说:规范允许中间层把请求体里的关键字段镜像到信封元数据上,让网关不用解析请求体就能路由与限流。
请想一下这条需求是从哪来的:如果 MCP 住在模型层,或者住在你和用户之间,网关就没有「按方法名路由」这个需求。 正因为它是一个可以被基础设施当作普通服务来治理的东西,才会需要「不解析业务内容也能路由」。这条侧证比任何架构图都有力。
现在补三组否定边界,因为「它住在哪」这个问题,用「它不在哪」回答更清楚:
① 不在模型层。 它不改变模型的能力,只改变能力的暴露方式。接上 MCP 不会让模型变聪明,只会让它多几个能调的东西。
② 不在你和用户之间。 它不参与界面呈现。界面拿到的是工具调用的结果,不是工具本身。所以「MCP 能不能渲染 UI」这个问法,问的是另一层的事。
③ 本机进程与远端服务的授权路径不同。 本机以标准输入输出方式启动的服务不带授权流程——它从环境里取凭据。把远端那套套到本机进程上,会得到一套没有对手方的握手(4.5 节已经踩过同一个坑)。
最后必须说清成本面,否则读者会在只有一个消费方时也去引入它:
「接一次、到处能用」这个收益,只有在「多于一个消费方」时才开始兑现。
只有一个客户端的时候,MCP 给你的是一层额外抽象:多一个进程要部署、多一套协议要对齐、多一处可能静默失败的地方(5.4 的教训)。收益为零,成本为正。 所以「该不该引入」的判据不是「它是不是标准」,而是:我现在有几个消费方?半年后会有几个?
这句判据很朴素,但它能挡掉大部分「为了架构好看而引入」的决定。
6.3 MCP 的无状态化改变了部署假设:不必再要粘性会话
这一节有一条极硬的原话,先抄下来:
「没有协商握手。每个请求都携带自己的协议版本,服务器独立接受或拒绝每个请求。」
这句话一句话废掉了三个旧假设:没有 initialize 握手、没有会话 ID、不需要粘性路由与会话存储。官方公告把对生产部署的效果说得更具体:一个原先需要粘性会话、共享会话存储、并在网关做深度包检测的远端服务器,现在可以放在普通的轮询负载均衡之后。握手与被移除的会话头,都不再是协议的一部分。
所以「必须粘性会话」这条结论已经过期。 如果读者手上的部署文档还写着「要开会话保持」,那是上一代的形状。
但这一条只写了一半。另一半是:
「协议层无状态」不等于「你的应用必须无状态」。
规范专门有一节叫「无状态的协议,有状态的应用程序」,并且给了替代方案:需要跨调用携带状态的服务器,像普通 HTTP 接口一样铸一个显式句柄(比如一个购物车编号、一个浏览器编号)从工具返回,让模型在后续调用里把它作为普通参数传回来。
官方对这个模式的评价很高:它「往往比会话状态更强大」。原因很实在——句柄对模型是可见的,所以模型能组合、推理,并在步骤之间交接它。会话状态对模型是不可见的(它只是一条连接上的隐含上下文),而句柄是一个可以放进推理里的值。
这带来的是能力,不是免费。句柄模式把三件事变成了你的责任:
① 有效期。 句柄活多久?过期了返回什么? ② 清理。 谁回收它?泄漏了会怎样? ③ 防篡改。 一个被改过的句柄,你要能拒掉(4.5 节的「不可信输入」在这里同样成立)。
规范还立了两条硬禁止,它们是「无状态」这件事的另一面:
① 服务器不得依赖同一连接上的先前请求来建立上下文——包括能力、协议版本、客户端身份。 ② 不应要求客户端复用同一连接或进程来完成相关操作。
换句话说:任何「上一次请求留下的东西」,都不能是这一次请求的前提。
最后划清过期的范围,这一步很重要,因为「MCP 过时了」这类结论一旦传开就很难收:
过期的只是「必须粘性会话」这一条,不是「MCP 需要认证」。 相反,认证在新的授权规范里被写得更细了(4.5 节那四条验证就是它的一部分)。别把一条部署假设的过期,读成整个协议不需要安全。
6.4 协议版本与能力是逐请求协商的,不再假设「连上就是同一个版本」
6.3 说了「没有握手」,这一节说「那版本怎么对」。
机制只有一句:版本随请求走。
具体落点有两处:每个请求在自己的元数据字段里声明协议版本;在基于 HTTP 的传输上,还同时放在一个专门的请求头里。两处都要带——只带一处的实现会在某一种传输上翻车。
不支持时怎么办?动作是固定的,不是「看实现」:
服务器必须返回一个「不支持的协议版本」错误,并且把错误码与它支持的版本列表一起返回。 客户端应当从列表里挑一个共同支持的版本重试。
请注意这里的工程含义:版本不匹配是一个可恢复的正常分支,不是故障。 所以客户端要有一条「按对方支持的版本重试」的路径,而不是把这个错误直接弹给用户。这一条决定了你的错误处理写得对不对——把可恢复的分支当故障处理,用户就会看到一堆本不该出现的报错。
还有两个必须实现的部件:
① 一个发现接口。 服务器必须实现一个**能一次性返回「支持的版本 + 能力 + 身份」**的调用。客户端可以调它,也可以不调直接发请求、撞到版本错误再处理。两种都合法,但你得选一种并写下来。
② 缺字段是硬错。 缺少必需的版本或能力字段,按参数错误处理,并以 400 拒绝。不是「猜一个默认值」,是拒。
能力也在同一层随身携带。可选扩展通过能力里的扩展字段声明,而扩展标识符必须带强制前缀(规范给的例子是界面扩展与任务扩展各有一个具名前缀)。不支持的一方按核心协议行为降级,或直接拒绝请求——两条路都行,但不能假装支持。
还有一个容易被忽略的机制:「时代」探测。
因为现实里会同时存在握手式的旧服务器与逐请求式的新服务器,客户端要能判定对面是哪种。判定的方式与传输绑定:标准输入输出用发现接口探测;HTTP 用一次现代请求并检查 400 的响应体。
这里有一条口径必须写清:探测结果是服务器的属性,不是单次请求的属性。 所以客户端应该缓存它——不能每来一个请求就探一次,那等于把「无状态」的收益又还回去了。
最后一条最实用的:不要再画「先连接、握手、再调用」的时序图。 那是上一代的形状。你照着旧图写出来的实现,会在第一次请求就出错,而错误信息看起来像版本不兼容——但真正的原因是你在等一个不会来的握手。
6.5 工具注解不能当安全凭据:它只是提示,且默认是悲观的
这一节的价值在于把「不可信」从一个形容词,变成一个具体动作。而做到这件事的,是规范里的两句原话。
第一句:「注意:注解里的所有属性都只是提示。它们不保证能忠实描述工具的行为(包括标题这类描述性属性)。」
第二句:「客户端永远不应基于来自不可信服务器的注解做出工具使用决策。」
第二句的力度值得单独体会:它不是说「要小心」,是说**「永远不应」**。一句话把「按注解放行」这条路彻底关掉了。
注解一共四个布尔值,而它们的默认值是刻意悲观的——「没写就按最坏情况算」:
| 注解 | 含义 | 默认 |
|---|---|---|
| 只读提示 | 这个工具不改变任何东西 | 否 |
| 破坏性提示 | 这个工具可能造成破坏 | 是 |
| 幂等提示 | 重复调用结果相同 | 否 |
| 开放世界提示 | 这个工具可能接触外部世界 | 是 |
请把最后一列读一遍:一个什么注解都没写的工具,会被当成「非只读、可能破坏、非幂等、开放世界」的。 这就是「悲观默认」的意思——不是它真的危险,是它不被信任。
「开放世界」要单列一句,因为它最依赖部署语境:它的含义是「这个工具可能接触外部世界」,而**「外部」的边界取决于服务器跑在哪**。同一个工具,跑在内网与跑在公网,这个提示的含义不同。所以这条注解是四条里最不该被单独拿来做判断的。
那权限靠什么判?分工必须写死:
注解用于「要不要先问用户」这种人机判断;权限判定必须在你自己的代码里做。
换句话说:注解是一组给人看的风险词汇,不是给程序判权限的凭据。你的代码里要有模型之外的白名单、要有按操作的最小权限、要有高影响操作的人工确认点(6.1 那五条)。注解可以帮你决定「这句话要不要弹一个确认框」,但它不能帮你决定「这个请求放不放行」。
还有两条现状要带上,它们都属于「今天没有的能力,下个版本可能有」:
① 目前有五个增强提案在提新的注解(可信与敏感度、治理与体验、不安全输出提示、秘密提示、可信提示)。其中可信与敏感度那一项是两个大厂在生产里踩到坑之后联合提出的——这个细节很能说明问题:这些注解是被事故推出来的,不是被设计出来的。
② 「任务支持」没有做成注解,而是落在工具的执行属性里(取值:禁止 / 可选 / 必需,默认禁止)。官方给的理由很讲究:执行元数据不算行为提示。
最后一句:正文不依赖这些提案。 今天能写的只有那两条原话与四个默认值——而它们已经够用了。
6.6 一个可观测的验证:记录「模型选错工具 / 选错参数」的比例
这一节的东西是本书构造的:本轮检索没有找到任何机构发布过「工具选错率」的标准定义、统计口径或健康阈值。官方给的是方向性的处置建议(限制工具集、收窄工具功能、最小权限、模型之外做授权检查),没有给「收到什么程度算够」这个数。
所以它是一个有用的自查,但它不是行业指标。这个区别必须写在前面,否则读者会把它当成质量分。
记录三类错误,它们是三种不同的病:
① 选错工具。 该用 A,用了 B。病在工具集重叠或描述不清——两个工具听起来都能干这件事。
② 选错参数。 工具选对了,形状也对,值不对。病在参数描述不够,或者模型没有拿到足以填对的信息。
③ 该调没调。 该用工具,它直接编了个答案。这一类最危险,因为它看起来最正常——用户拿到的是一个通顺的回答,没有任何异常。
分母怎么定? 这一条决定了这个数有没有意义:只统计「你核过的那一批」。
而且必须写明:未发现的错误不在分母里。 也就是说,这个比例天然是偏低的——你算出来的 3%,对应的真实值一定更高。把这个上限写出来,比把这个数算准更重要,否则它会变成一个自我安慰的指标。
怎么用? 顺序不能反:
比例高时,先收窄工具集与描述,而不是先去改提示词。
理由在 6.1:工具是攻击面,也是成本。 二十个工具里模型只认得出八个,正确的动作不是「教它认另外十二个」,而是把那十二个删掉或合并。改提示词是最后一步——它最贵、最不可回归、也最容易把别的地方弄坏(第 12 章会讲这件事)。
给不给阈值?不给。 没有权威阈值的时候给一个数,就是编数字。一个不给阈值的自查工具,比一个给了假阈值的自查工具更有用——因为它逼你去看分布,而不是去对一条线。
最后补一条依据,说明「为什么值得收窄工具集」。安全研究里有一组被反复引用的组合风险:能访问私有数据 + 暴露于不可信内容 + 有能力对外通信——三者同时具备,就构成了数据外泄的条件。 收窄工具集,正是拆掉其中一条的常规手段。
请注意这个组合的用法:它不是让你去查「我的工具危不危险」,而是让你去查「这三条里有几条同时成立」。 三条都成立的那条链路,就是你的第一优先级。