跳到主要内容

Schema 熵:为什么你的工具定义正在生产环境中腐烂

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的 Agent 在 1 月份时运行良好。到了 3 月,它开始在 15% 的工具调用中失败。到了 5 月,它在另外 20% 的情况下会静默地产生错误输出。你的部署日志没有任何变化。没有人动过 Agent 的代码。工具定义看起来和六个月前一模一样——而这恰恰就是问题所在。

工具 Schema 并不需要被修改才会出错。它们所描述的服务在底层发生了变化。Enum(枚举)值增加了。后端重构使必填字段变成了可选字段。以前接受字符串的参数现在需要 ISO 8601 时间戳。Schema 文档保持冻结,而底层的 API 却在不断演进,你的 Agent 仍在自信地调用它,完全不知道契约(contract)已经发生了变化。

这就是 Schema 熵(Schema entropy):你的 Agent 接受训练时所使用的工具定义与生产环境服务实际表现出的行为之间逐渐产生的差异。它是生产环境 AI 系统中最被低估的可靠性问题之一,研究表明,工具版本控制问题约占生产环境 Agent 故障的 60%。

Schema 熵到底是什么样子的

Schema 熵并不是单一的失败模式,而是一类具有共同根源的失败。

最显的形式是硬中断(hard break):你重命名了一个必填参数,Agent 立即开始生成会导致 400 错误的调用。这些实际上是容易处理的情况。你能看到失败,找到不匹配的地方,然后修复它。

更危险的形式是软腐化(soft rot)。考虑以下场景:

  • 你在 status 字段中添加了一个新的枚举值 PENDING_REVIEW。你的 Agent 的工具描述中仍然只列出了发布时已知的四个值。当 API 开始返回 PENDING_REVIEW 时,Agent 会尝试用其现有的思维模型去理解它——有时能通过推理正确处理,有时则不能。
  • 你将一个以前必填的参数改为可选。省略该参数的调用现在在 API 层级可以成功,但在后端会触发不同的行为。你的 Agent 并不知道存在这个分支。
  • 你将一个字段从接受纯整数改为需要字符串格式的整数("42" vs 42)。API 会在一段时间内静默地进行强制转换,然后某次框架升级停止了这种转换。在后端更改几周后,Agent 的调用开始出现晦涩的类型错误。
  • 你在现有的工具(search_positions)旁边添加了一个更具体的同级工具(search_position_budgets)。在你的工具清单中,两者看起来很相似。Agent 开始混淆它们,将 30% 的预算查询路由到了错误的端点。

关于 Agent 工具测试的研究恰恰发现了最后一种模式:通过改进工具描述来更好地区分相似工具,对 Agent 准确性的提升比改进 Agent 自身的逻辑更大。描述本身就是那个 Bug。

为什么 Agent 是不称职的 Schema 消费者

当人类开发者调用带有过时 Schema 的 API 时,工作流包括阅读错误消息、查阅更新后的文档并进行调整。Agent 默认情况下没有这种恢复循环。它们在运行开始时接收工具定义,并在整个过程中将其视为绝对真理(ground truth)。

更关键的是,Agent 失败的方式在结构上与代码失败不同。一项针对 Schema 优先工具 API 的对照研究发现,区分以下三类失败非常有用:

  1. 接口误用(Interface misuse):结构畸形的调用——错误的类型、缺失必填字段、幻觉出的参数名称。这些是正式 Schema 可以预防的失败。
  2. 执行失败(Execution failures):调用结构正确,但触发了 Agent 不知道的运行时前提条件。
  3. 语义误用(Semantic misuse):符合 Schema 规范但在逻辑上对于任务是错误的调用。Agent 使用正确的结构调用了正确的工具,但使用了语义上错误的值。

Schema 熵主要导致第 2 和第 3 类失败。一个工具定义在语法上可能是正确的,但仍会引导你的 Agent 做出错误的行为,因为 Schema 的含义已经偏离了服务的行为

一个顽固的可观测性问题加剧了这种情况:许多 API 即使在操作失败时也会返回 HTTP 200。StackOne 关于 Agent 测试的研究发现,埋在响应体中的速率限制错误(以 {"status": 200, "data": null} 形式返回)被 Agent 理解为“不存在记录”,而不是“请求失败”。当 HTTP 的成功不代表业务逻辑的成功时,你的 Agent 就无法获得 Schema 已将其引入歧途的信号。

Schema 腐化开始的三个地方

了解熵是从哪里进入系统的,就能知道应该在哪里部署防御。

外部服务。 你无法控制第三方 API 何时更改其 Schema。支付处理器会增加新的扣费状态,CRM 会演进其联系人模型。每一个外部工具依赖都是潜在的 Schema 腐化向量,而且当它们的 API 发生变化时,你不会收到 Webhook 通知。

跨团队边界的内部服务。 后端团队重构了一个服务端点。他们更新了 API 文档。但他们没有更新同事六个月前在另一个仓库中编写的 LLM 工具定义。这是实践中最常见的场景——工具定义存在于 Agent 代码附近,而它们描述的服务则存在于别处。

模型侧的变化。 模型提供商的更新可能会改变模型解析 Schema 的方式。强制执行严格模式(Strict mode)、对可选字段处理方式的变化、模型处理模糊枚举值时的行为差异——即使你的 JSON Schema 字节没有变化,这些也会改变实际生效的契约。

工具 Schema 的向后兼容规则

核心原则是非对称的:增加永远是安全的,移除永远是不安全的,而更改则取决于具体方向,有时安全。

安全变更:

  • 添加带有文档说明默认值的新可选参数
  • 添加新的枚举值(如果 agent 在遇到未知值时可以优雅地处理失败)
  • 在清单中添加全新的工具
  • 将之前的必填参数改为可选(前提是默认行为对于现有的调用模式是正确的)

破坏性变更:

  • 移除或重命名任何参数
  • 将可选参数改为必填
  • 更改现有参数的类型(即使在传输层面技术上是兼容的)
  • 移除 agent 目前可能正在发送的枚举值
  • 在不更改参数名称的情况下更改其语义

最后一点是最难检测的。一个过去接受 1-5 数值的 priority 字段,现在接受 low/medium/high ——类型变了,但字段名没变,Schema 差异对比(diff)中无法体现这种语义转变。

实践规则: 将对现有参数的任何更改都视为潜在的破坏性变更。新的行为应该放在新的参数或新的工具版本中。旧参数应在描述字段中明确标注停用日期(sunset dates)以示弃用,而不是悄无声息地重新定义用途。

为你的工具清单进行版本管理

对工具集合(而不仅仅是单个工具)应用语义化版本控制(Semantic Versioning),是向使用者传达变更影响最清晰的方式。

MAJOR(主版本)提升信号代表破坏性变更:参数被移除、类型更改、行为与描述不符。任何消耗该工具清单的 agent 在升级前都应重新评估。MINOR(次版本)提升则是在保留所有现有契约的同时,添加新的可选参数或新工具。PATCH(修订号)提升则是在不改变行为的情况下,修正文档、改进描述或澄清边缘情况。

具体来说,这意味着:

  • 将工具清单存储为版本化的产物,而不是 agent 提示词中的内联字符串
  • 在 agent 记录的每个事件中包含 schema_version 字段,以便你在事后分析中将故障与 Schema 版本关联起来
  • 在发布期间同时运行多个工具清单版本,像分配 API 版本流量一样分配版本流量

Snowplow 团队为事件 Schema 开发了一个类似的框架,称为 SchemaVer,它明确区分了模型破坏性变更、添加和修复。同样的分类法可以完美映射到工具定义中,且核心见解是一致的:版本控制不是官僚主义,而是让回滚成为可能的机制。

捕捉 Schema 腐化的集成测试

借鉴自微服务架构的契约测试(Contract testing)是这里的正确模型。核心思路是:为你 agent 暴露的每一个工具维护一套完全脱离 LLM 的测试用例。测试直接使用已知输入调用工具处理器,并对输出 Schema、错误行为和边缘情况进行断言。

这带给你两个重要的特性。首先,这些测试运行速度很快——它们不进行 LLM API 调用。其次,它们在底层服务发生变化时失败,而不是在你 agent 恰好执行该路径时才失败。

对于你无法控制的外部 API,策略有所不同:维护一个 Schema 快照,并定期与在线 API 进行差异对比。当在线 Schema 与你的快照发生偏离时,差异对比就是你的警报。然后你在更新工具定义之前——而不是在你 agent 在生产环境中表现异常之后——决定该变更是否向后兼容。

一个用于工具 Schema 健康状况的最小化 CI/CD 检查应验证:

  • 工具定义中的所有必填参数仍被目标服务接受
  • 你定义中记录的所有枚举值在接收端仍然有效
  • 响应 Schema 符合你工具的输出解析代码的预期
  • 现有的测试调用不会产生带有隐藏在 Body 中的失败信号的 2xx 响应

最后一项检查最容易被忽略,也最重要。“HTTP 200 但实际上失败了”这种模式非常普遍,值得设立一个专门的测试类别。

生产环境中的观测指标

预防措施处理你预料中的变更。观测(Instrumentation)则处理那些你没预料到的。

生产环境中的每一次工具调用都应记录工具名称、工具清单版本、调用参数(脱敏后)、响应状态,以及响应是否符合预期的输出 Schema。汇总这些日志可以让你将 Schema 漂移检测为一个统计信号,而不是等待严重的故障发生。

预示着 Schema 熵增(entropy)的模式包括:

  • 特定工具的成功率在滚动窗口内下降,且没有任何部署事件
  • 响应中开始出现你的 Schema 中未列出的新枚举值
  • 参数值的分布发生偏移——agent 开始发送超出预期范围的值
  • 工具调用延迟发生显著变化,提示服务侧的执行路径发生了变化

这些信号不会立即触发。Schema 熵增是一个逐渐发生的过程。你需要持续观察正确的指标,而不不仅仅是在部署时。

文化挑战

这一切在技术上都很简单,难点在于组织层面。

工具定义(Tool definitions)通常由编写 Agent 的人负责,但它们所描述的服务则归其他团队所有。当支付团队更新其交易状态枚举(enum)时,他们不会想到要通知 AI 团队。当目录团队添加新的产品类型时,没有人会去对比差异。Schema 存储在 Agent 的代码库中,而服务则位于其他地方,两者之间缺乏桥接机制。

解决方案并非技术性的——而是要将工具定义视为生产者和消费者团队之间的共享契约,像对待公共 API 一样严谨。这意味着:

  • 工具定义被签入到 Agent 团队和服务团队共同关注的位置
  • 当工具定义的更新涉及其 API 时,服务负责人需要进行审批
  • 后端服务的破坏性变更(Breaking changes)应遵循与 API 破坏性变更相同的流程,因为 Agent 只是另一个消费者

底层原则是:如果一个团队可以在不更新工具定义的情况下改变行为,熵增是必然的。Schema 必然会发生漂移。唯一的问题是,在 Agent 开始基于一个不再反映现实的契约做出“一本正经的错误决策”之前,还能撑多久。

一旦你看清了 Schema 熵增问题,它就不难解决。难点在于它被设计成了不可见的——没有异常,没有堆栈跟踪,只有一个 Agent 像往常一样努力工作,却在对抗一个已经悄然失效的世界模型。

References:Let's stay in touch and Follow me for more thoughts and updates