六周前,你将 get_user_email 重命名为 lookup_contact。新名称已发布,旧的处理程序已移除,变更日志记录了这一点,你的评估集也通过了。然而上周二,一位客户支持工程师联系了你:智能体在上周的大约 3% 的工具调用中返回了错误——tool_not_found: get_user_email。那个已被重命名的名称。那个在实时系统中已经不再对外公开的名称。
先验知识(Prior)具有粘性。你的智能体正在与之对话的模型是在一个语料库上训练的,在这个语料库中,get_user_email 是询问“这个人的电子邮件是什么”的绝大多数规范方式。即使你在推理时传递的 tools 数组中仅列出了 lookup_contact,模型偶尔——在特定的上下文条件下,特别是长追踪(long traces)或错误恢复状态下——仍会退回到它记忆中的名称。直接切换并不能消除长尾效应;它只是将软故障变成了硬故障。
这就是关于 MCP 版本控制的公开讨论中一直绕不开的失效模式。服务器动态宣告工具;客户端刷新;协议提供了 tools/list_changed 来提示过时的缓存。这些都没错。但它们都没有解决模型的先验知识问题。协议是简单的部分。困难的部分在于,JSON-RPC 管道另一端的东西是一个概率分布,它看过五年的 GitHub 代码,而你的变更日志并不在它的权重(weights)中。
直接切换模式对工具而言是错误的
大多数团队参考的心理模型是 API 弃用:宣布一个停用日期,发送六个月的 Deprecation 请求头,然后在切换日返回 410 Gone。这种模式假设调用者是一个能够读取响应代码并更新其代码的软件。它适用于人类编写的 REST 客户端,因为人类对 410 的反应是提交一个 Jira 工单。
模型不会读取 410 并提交 Jira 工单。模型收到 tool_not_found 错误后,会根据其训练分布和周围的上下文决定做什么,并且经常会用同一个名称重试。或者它会向用户道歉。或者它会幻觉出另一个同样错误的工具名称。或者它通过另一条路径成功了,而你永远不知道它曾尝试过已弃用的名称,因为错误已被吸收进了一个恢复分支中。
直接切换模式还假设弃用窗口是昂贵的——你希望窗口尽可能短,以便移除旧代码。对于模型来说,窗口是廉价的。弃用途径只是一个转发到新名称的单行垫片(shim)。真正的成本是在垫片消失那一刻你所创建的失效模式。
在 MCP 介导的智能体系统中,工具重命名不是 API 版本控制事件。它们是模型输入分布的偏移,需要像迁移数据库列时使用的那种双写(dual-write)机制。
真正有效的弃用垫片
在生产环境中经受住考验的模式包含三个部分,MCP 规范都没有强制要求这些,但协议本身都允许。
保持两个名称同时在线。 当你将 get_user_email 重命名为 lookup_contact 时,在至少一个模型生成周期和客户端更新周期(以较长者为准)内,在 tools/list 中同时宣告两者。旧工具的处理程序是对新工具的单行转发,参数保持一致。Schema 应该完全相同或更加宽泛。旧工具的描述应以弃用标记开头——[DEPRECATED: use lookup_contact]——模型有时会读取并对此做出反应,而人类在 grep 你的清单文件时也肯定能看到。
记录对已弃用名称的每一次调用。 为其标记原始会话 ID、模型版本、在追踪中的位置,以及该调用是模型的第一次尝试还是在其他失败后的重试。这是你目前缺失的数据,也是唯一能告诉你何时可以安全移除垫片的数据。没有它,你的移除决定就像是在抛硬币。
根据流量而非日期定义移除触发器。 “我们将在六周后移除 get_user_email”是错误的承诺。“一旦我们路由到的所有模型版本中,每周调用次数连续两周低于 N 次,我们就会移除它”才是正确的做法。模型正在通过实际表现告诉你它何时内化了新名称;让它自己决定。
这就是针对工具的“双写”。旧工具保持工作,新工具是规范工具,切换发生在旧名称调用率的经验衰减曲线上——而不是发生在某个产品经理因为下一个发布分支要在那天截断而选定的日期上。
能力版本控制的作用比你想象的要小
几个 MCP 版本控制提案倾向于使用语义化版本(semver)风格的工具版本:[email protected] 弃用 [email protected]。服务器同时发布两者。客户端明确请求其中一个。模型选择客户端呈现的内容。这很整洁、有原则,但在很大程度上偏离了重点。
偏离重点的原因在于,模型看不到 @2.0.0。它看到的是工具名称和描述。版本限定符是客户端和服务器围绕模型协商的东西,而不是模型进行推理的东西。如果你将 lookup_contact 和 lookup_contact_v2 作为两个独立的名称发布,模型必须根据描述在它们之间做出选择——你刚刚把模型需要消歧的表面积增加了一倍,而且还要面对模型偶尔仍会尝试原始弃用名称的长尾问题。
能力版本控制在同一个工具确实需要两个契约共存时是有用的——例如当某些工作流需要 v1 而其他工作流需要 v2,且选择权在于调用者时。当目标是“逐步淘汰旧名称”时,它不能替代弃用垫片。对于淘汰过程,你希望旧名称是新名称的一个标记清晰的别名,而不是一个竞争对手。
版本控制无法解决的另一件事是:模型的先验知识。如果你的模型在其训练分布中见过 get_user_email 一万次,而见过 lookup_contact 零次,那么无论客户端和服务器之间进行多少次版本协商,都无法改变模型想要输入它所熟悉的名称这一事实。解决办法是让新名称看起来像模型已经偏好的那种名称——动词-名词形式、动作导向、与描述的第一句话匹配——而不是强行加上一个版本后缀。
重新引入提示词
在长期运行的智能体(agent)会话中,出现了一个更有趣的模式:即使模型已经成功调用了几次新工具,某些追踪轨迹(trace shapes)也会导致它回退。最常见的情况是从错误中恢复。模型尝试了某项操作,失败了,现在它处于“让我尝试不同方法”的心理模式,而它所寻找的不同方法通常是来自其训练先验(training prior)的名字——那个感觉更基础、更成熟的处理方式。
解决方法是在系统提示词级别进行“重新引入”。不是一段话;而是在工具列表附近的一行文字,明确指出重命名:“工具已更新。使用 lookup_contact 获取用户联系信息;get_user_email 已弃用。”这是对工具描述中弃用标记的口头等价表达,它作用于模型注意力模式(attention pattern)的不同部分。你不需要永远保留它。你只需要在垫片(shim)存活的同一窗口期内保留它——这段时间要足够长,以便模型的会话内学习(即它在追踪中看到之前的工具调用正在使用新名称的部分)能够压倒其权重内先验(in-weights prior)。
重新引入提示词也是你作为一种“后弃用里程碑”而移除的东西。当你可以将其移除且旧名称的调用率保持为零时,重命名才算真正落地。在那之前,你处于“弃用进行中”,而非“弃用完成”。
协议做对了什么,以及留给你了什么
MCP 规范拥有正确的原语。tools/list_changed 通知意味着客户端可以在不重启会话的情况下刷新其工具列表,这弥补了曾让早期采用者头疼的缓存失效漏洞。2025-06-18 规范修订加强了结构化输出和工具名称验证,这使得发布那种会诱导基于先验回退的模糊工具表面变得更加困难。在握手时协商的日期字符串协议版本为你声明所支持的内容提供了一个稳定的位置。
协议没有做——而且任何协议都做不到——的是告诉连接另一端的模型,它在预训练期间学习的名称不再有效。这个问题属于你。协议的工作是让你能够同时宣传两个名称,记录每个名称的调用情况,并在数据表明可以移除旧名称时将其移除。实际上执行这些操作的纪律,将工具重命名视为模型分布事件(model-distribution events)而非软件弃用事件(software-deprecation events)的做法,是任何规范都无法为你处理的部分。
那些在工具重命名上栽跟头的团队,是那些将 MCP 服务器视为 REST API 并将模型视为 Python 客户端的团队。那些没有栽跟头的团队,则是将重命名视为数据库迁移的团队,其中数据库是你无法控制的概率分布,迁移窗口由经验衰减曲线设定,而新名称需要看起来像模型本来就会输入的名称。选择第二种思维模型,长尾效应就变成了你观察的曲线;选择第一种,长尾效应就会变成你未曾预料到的寻呼机故障事件(pager incident)。
计划用三个月的时间完成重命名,并发布一个持续那么久的垫片(shim)。记录对旧名称的每一次调用,让数据告诉你何时移除它。按照模型想要命名的方式来命名新工具。模型最终会按你的意愿行事。只是这比你的发布时间表预期的要长。
会员专享
余下内容仅对会员开放。
会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
- —完整文章,包含未公开存档的部分
- —可落地的工作框架,附带权衡与决策依据
- —新文章抢先看,先于公开发布
随时取消 · 一次订阅,畅读全部