你的工具会在你没有部署的情况下改变。
工具描述是你 prompt 的一部分,而那段文字归别人所有。当某个厂商重写一句参数说明、加一个可选字段或改一个枚举值的名字,你的智能体行为就变了——你的仓库里没有一次提交,你的日志里没有一条报错。而那些吵闹的破坏反倒是无害的那一类,因为它们会抛异常。把工具目录当成它本来的样子——一个依赖——来做版本:给它快照、把哈希打进 trace、对 diff 报警。
那个没人声明过的依赖。
你钉死了模型。你给 prompt 做了版本。你逐行评审应用代码。然后你把三十个工具定义交给模型——名字、描述、参数文档、枚举值、响应形状——其中大部分是另一家公司的某个人写的,并在运行时从一个 MCP 服务器、一个托管连接器平台,或一份你在启动时转换的 OpenAPI 文档里取回来。
这些定义会直接进上下文窗口。工具描述不是配置;它是与你的系统提示争抢模型注意力的指令文本。这意味着,对你这套部署的行为,诚实的表述是(模型、prompt、工具)三元组——正如你的版本化策略早就写明的那样——而这三者里有两个被钉住了,第三个却在每次进程启动时从第三方重新拉取。
由此产生的故障在生产系统里很少见:一次行为回归,没有部署、没有配置变更、没有事故触发器,而 git log 还能证明谁都没动过任何东西。团队会把调查的第一天花在错误的那个仓库里。
四类漂移,按"坏得有多响"排序。
需要纠正的直觉是"破坏性变更才危险"。破坏性变更恰恰是安全的那种——它们会抛异常、会传呼你、然后被修掉。真正昂贵的漂移,是那种让一切保持绿色的漂移。
- 结构漂移——某个工具消失、某个必填参数出现、某个类型变了。很响。你会拿到 4xx 或 schema 校验错误,你现有的告警已经能抓住它。把这当成最好的情况。
- 语义漂移——schema 没变,含义挪了。枚举多了一个你的 prompt 从没提过的值;一个原本是国家名的字段现在是 ISO 代码;分页默认值从 50 变成 20,于是你的智能体悄悄看到的数据变少了。什么都不报错。智能体采取了一个略微错误的动作,或者对着不完整的输入采取了正确的动作。
- 描述漂移——厂商把文档串改得更清楚了,或者加上一句"仅在用户明确要求时才使用本工具"。那是一次由陌生人施加到你系统上的 prompt 编辑,而它能把工具选择率挪动几十个百分点。这一类完全没有任何错误面。
- 行为漂移——契约没变,底下的服务变了:更慢、限流方式不同、校验更严、结果排序不一样。你的容量假设悄悄不再成立。
按代价排序,顺序与噪音正好相反:描述漂移与语义漂移伤害最大、信号最少,而人人害怕的结构性破坏不过是凌晨两点的一次传呼、早饭前的一次修复。任何只盯着报错的检测策略,盯的都是无害的那一半。
给目录做快照,把 diff 变成一次评审事件。
机制很无聊,而它就是全部的修法。在构建时,把要交给智能体的每一份工具定义拉下来,做规范化序列化,把结果写进你仓库里的一个文件。算哈希。每次构建都比对。
- 先归一化再算哈希——对键与工具顺序排序,剥掉服务器时间戳、实例 ID 这类易变字段。一个不稳定的哈希一周之内就会造成告警疲劳,然后被静音。
- 既按目录算哈希,也按工具逐个算。目录哈希告诉你"有东西动了";逐工具哈希告诉你"动的是什么",并让你在盯住那三个要紧工具的同时,忽略一个无关工具的反复变动。
- 未经评审的变更让构建失败,就像没人看过的 lockfile 变更该让评审卡住一样。这个 diff 很小、可读——一句改动的描述就是两行 diff、三十秒的决定。
- 能从快照供给时就从快照供给。有些平台允许你钉住工具版本或提供自己的 schema;一个带 schema modifier 的连接器平台,能让你无论上游怎么改都自行定义模型看到的形状。这就把漂移从一次行为变更变成了一次合并冲突——而那才是它该待的地方。
这与把模型钉到带日期的快照上是同一套纪律,只不过施加在行为三元组中另一半没被钉住的东西上。已经做了其中一件却没做另一件的团队,通常只是没注意到这份对称性。
检查含义而不只是形状的契约测试。
schema 校验抓的是结构漂移,而那个你本来就会抓到。值得写的测试,断言的是你的 prompt 默默依赖着的那些语义。为每个集成留一个专用的沙箱租户,按计划对着真实的第三方跑一个小套件——通常每晚一次就够,而且刻意不放进 pull request 路径,因为一个厂商的宕机不该卡住你的部署。
- 断言你据以分支的那些枚举集合。如果你的 prompt 或代码知道
open | pending | closed,那么在第四个值出现的那一天,就该有一个测试失败——赶在模型撞上它之前。 - 端到端断言一次黄金调用。每个工具用固定输入做一次有代表性的调用,检查结果里你要提取的字段还在,而且含义还是原来的含义。格式漂移比 schema 漂移常见得多。
- 断言那些默认值。分页大小、排序、时区、币种、截断上限。它们十有八九没有文档,而且改动时不会有 changelog。
- 把描述文本单独 diff 出来。它没法做断言,那就把测试做成快照:描述变了,这是新的,批准或适配。一次被批准的描述变更要当作一次 prompt 变更来对待,并重跑覆盖该工具的那部分评估。
从撰写方看什么才算一份好契约,见工具 schema 与契约;本页是同一个问题在消费方的那一半。
在生产环境里检测,因为真正的目录住在那里。
构建期的快照描述的是你期望的东西。而模型真正遇到的是被实际提供的东西,那发生在生产环境里,还包括你从 CI 看不见的租户差异——连接器平台会依据某个客户授权了哪些集成而返回不同的工具集,所以两个租户手里未必是同一份目录。
- 把目录哈希打在每一条 trace 上。一个字段,与你已经在记的模型 ID 和 prompt 版本并列。它把"工具是不是变了"从一场考古变成一次筛选,也是本页价值最高的一行代码。见追踪与可观测性。
- 按工具、按天盯住工具选择率。一次描述编辑只会在这里现形、别处都不会:同样的流量、同样的 prompt,某个工具突然被选中的次数多了四成。这是描述漂移的先行指标,代价是一块看板。
- 盯住参数分布,而不只是调用次数。模型传入的枚举值发生偏移,或某个原本会给的参数现在不给了,那就是语义漂移正在到达。从你本来就留着的 trace 里算出来很便宜。
- 按工具、按类别拆分错误率。校验错误、鉴权错误、限流错误与超时的漂移原因各不相同,聚合到一起就成了一条读不懂的线。
- 对"出现未知枚举值或未知工具名"报警——这是上游发生变动后,机器最早能检测到的信号。
在你自己的边界上响应,而不是在 prompt 里。
漂移落地时,最诱人的修法是给系统提示打补丁:"注意 status 字段也可能是 archived"。它今天管用,然后逐渐堆成一份由别人的决定写成的 changelog。请在低一层修它。
在智能体与每个第三方工具之间放一层薄薄的门面——你自己的名字、你自己的描述、你自己的参数集,映射到它们那边。这只是一点点代码,却买来三样东西:厂商改一句文案时,模型看到的工具不再跟着变;一次改名或新增枚举,你在一个适配器里吸收掉,而不是散落在好几个 prompt 里;还有,你可以把一个臃肿的厂商工具面缩到你真正会用的那几个操作。最后这一条本身就是独立于漂移的质量收益——见工具设计的反模式。
然后把这次响应当成一次变更来跑,因为它本来就是:
- 在接受新快照之前,重跑覆盖受影响工具的那部分评估。工具变更就是行为变更,值得与换模型同等的闸门——见质量回归检测。
- 为每个工具留一个开关,这样当某个厂商在周五推了个糟糕的东西时,你能不部署就把那一个工具从目录里撤下来。
- 把漂移事件记在与事故同一个地方。同一家厂商一个季度里改了三次描述,这是采购层面的信息,而除非有人把前两次写下来,否则没人会掌握它。
把这三件按顺序做掉,其余都是打磨。把工具目录哈希打在每条 trace 上——代价是一个字段,却把一周的调查变成一次筛选。在构建时给目录做快照,未经评审的 diff 就让构建失败,于是厂商的一次文案编辑变成一条 pull request 评论,而不是一个谜。然后给你最依赖的那三个工具加上门面,让下一次改名成为一次适配器改动,而不是一场 prompt 考古。而当描述真的变了,就把它当作一次 prompt 编辑:先重跑该工具的评估,再发出去。
延伸:灰度发布与版本化讲怎么钉住行为三元组的另外两条腿,生产中的 MCP 运维讲怎么运行这些定义所来自的服务器,MCP 工具投毒讲当描述变更并非疏忽而是敌意时会怎样,智能体清册与登记讲当某个工具变动时,怎么知道哪些智能体手里有它。