跳到主要内容

对你的工具接收的内容保持严格:波斯塔尔法则在智能体系统中失效

· 阅读需 12 分钟
Tian Pan
Software Engineer

“发送时要保守,接收时要开放。”波斯塔尔法则(Postel's Law)无疑是网络历史上最成功的设计原则——正是依靠它,来自不同厂商的 TCP 实现才得以在 20 世纪 80 年代实现互操作,并塑造了四十多年来的协议和 API 设计。然而,根据 IETF 自身的判断,这一原则会随着时间的推移而变质:RFC 9413 源于一份标题直白为《稳健性原则的有害后果》(The Harmful Consequences of the Robustness Principle)的草案,认为开放式的接收在短期内有助于互操作性,但从长远来看,却在悄悄腐蚀整个生态系统。

智能体(Agent)系统将这种长期的腐蚀缩短到了几周。当“发送者”是一个发出工具调用(tool calls)的大语言模型时,每一次开放式的接收——将 "5" 强制转换为 5、丢弃未知字段、对枚举拼写错误进行模糊匹配——都会破坏维持系统健康所需的精确信号。在智能体架构中,工具边界是唯一一处“大声报错”即是可靠性特性的地方,而大多数团队却反其道而行之。

这种直觉是可以理解的。你的智能体使用 {"limit": "10"} 而不是 {"limit": 10} 调用搜索工具,运行崩溃了,于是你收到了一个报错。显而易见的修复方法是添加一个强制转换层:将字符串形式的数字进行解析,丢弃未知字段,将 "asending" 纠正为 "ascending"。崩溃消失了,演示成功了,大家继续前进。但你实际构建的是一个将模式违规(schema violations)转化为语义损坏(semantic corruption)的系统——而语义损坏对于你现有的任何监控手段来说都是不可见的。

强制转换并不能修复错误,而是在洗白错误

格式错误的工具参数并非杂音。它是模型在向你传达其状态:它误读了模式(schema),工具描述存在歧义,上下文窗口发生了偏移,或者模型提供商在你不知情的情况下更新了底层模型。这些原因中的每一个都有不同的修复方法。而强制转换会一次性抹去所有这些证据。

看看常见的“稳健”行为实际上做了什么:

  • "5" 强制转换为 5 掩盖了模型已开始发出字符串类型的数字——这通常是模型版本升级或提示词更改导致模式超出模型注意力范围后,出现的第一个明显症状。
  • 丢弃未知字段 掩盖了模型正在幻觉参数,这通常意味着它正在与另一个工具的签名或旧版本的工具进行模式匹配。你丢弃的额外字段可能承载了用户的真实意图。
  • 对枚举拼写错误进行猜测 将一个清晰的失败变成了悄无声息的错误答案。将 "decending" 修正为 "descending" 看起来很安全;但在状态过滤器上将 "deleted" 修正为 "delete" 则是一个数据损坏漏洞,需要花费数天时间才能追溯到根源。

阴险之处在于损害发生的位置。崩溃会出现在你的错误仪表盘中。而经过强制转换的参数则会进入你的业务数据——获取了错误的行、更新了错误的记录、向用户返回了微妙的错误答案。你的评估(evals)永远看不到它,因为工具调用“成功”了。你的追踪(traces)显示的是绿色勾选。失败会在数周后作为无法解释的客户投诉浮现,而你的遥测系统中没有任何信息能将其与强制转换层触发频率比平时高出十倍的那一天联系起来。

这是智能体时代版本的协议工程师们通过惨痛教训学到的东西。RFC 9413 的核心论点是,容忍偏差并不能让偏差消失——它会让偏差变得永久化,因为发送者永远收不到能够纠正它们的反馈。每一个权宜之计(workaround)都会变成承重结构。区别在于,在协议领域,反馈给发送者实现者的循环需要数年时间。而在智能体循环中,发送者就在那里,只需一个回合即可纠正,准备在几毫秒内做出调整——只要你愿意告诉它。

重试循环是波斯塔尔假设失效的地方

波斯塔尔法则编写于一个发送者是你无法更改的远程实现的世界。拒绝一个轻微畸形的 TCP 段毫无意义:对方的协议栈不会自我修复,用户只会看到连接失败。开放式接收是唯一可用的杠杆。

智能体系统推翻了每一个假设:

  • 发送者是可以立即纠正的。 返回给模型的结构化验证错误会成为下一次尝试的上下文。模型非常擅长处理这类问题:只要给出字段名、预期类型和接收到的值,当前的尖端模型在绝大多数情况下都能在第一次重试时修复自己的工具调用。你不是在迁就一个外部实现,而是在上下文(in-context)中训练一个协作伙伴。
  • 拒绝的成本很低。 一次失败的工具调用只需一个往返——几毫秒的延迟和几百个 token。而一次悄然损坏的调用成本则是无限的:对真实系统执行了错误操作、向真实用户展示了错误数据,以及在没有任何标记损坏点的日志条目的情况下,花费大量工程时间来重建发生的事情。
  • 偏差率是一个生命体征。 每千次工具调用的验证失败率是智能体平台可以追踪的最有用的指标之一。当提供商发布隐秘的模型更新、当提示词重构掩盖了模式、当新工具的描述困扰模型时,它就会飙升。强制转换将这一指标永久归零——不是通过解决问题,而是通过拔掉烟雾报警器。

带有良好错误信息的严格拒绝是使循环自我纠正的关键。在生产环境中有效的模式在各个框架中都是一致的:在边界进行验证(Pydantic、Zod、JSON Schema——库并不重要,重要的是严格程度设置),并在失败时返回一个结构化的、模型可读的错误,说明确切的字段、违反的约束以及接收到的值。不是 HTTP 500。不是堆栈跟踪(stack trace)。不是 "invalid input"。而是模型可以采取行动的内容:limit 必须是整数,收到了字符串 "10";有效范围是 1–100

错误受众之间的区别比大多数团队意识到的更为重要。堆栈跟踪是写给拥有源代码访问权限的人类看的。模型可读的错误是写给只拥有对话上下文和工具模式的读者看的。

在 LangChain 和 MCP 上构建的团队一致报告称,当通用错误被结构化错误取代时,自我纠正率会大幅提升——模型无法修复它无法解析的东西。如果你的验证错误对模型来说是不可读的,那么严格性就会退化为崩溃循环,这就是严格性声名狼藉的原因。

失败应针对整个调用,而非单个字段

一个诱人的折中方案是部分接受:验证你能验证的部分,执行通过验证的字段,并对其余部分发出警告。这比任何一种极端情况都要糟糕。

部分接受意味着工具执行的内容与模型要求的并不一致,而模型对此一无所知。模型的心理状态现在认为“我使用了三个过滤器进行搜索”;而现实是只运行了两个过滤器。计划中的每一个后续步骤都会基于一个错误的前提进行推理,最终的失败——如果有人注意到的话——会在下游的几个回合后,在一个本无问题的步骤中显现。调试 Agent 的轨迹之所以困难,正是因为错误不仅在数据中传播,还在模型的信念中传播。原子化拒绝(Atomic rejection)能保持模型的信念与现实同步:要么调用完全按指令发生,要么调用没有发生且模型知道原因。

同样的逻辑也适用于未知字段,这些字段理应受到“严正对待”。JSON Schema 默认忽略额外属性的做法对工具调用(tool calls)来说是完全错误的——请在所有地方设置 additionalProperties: false。多余的字段绝非无害:它要么是幻觉(模型对你的 API 感到困惑),要么是无处安放的意图(模型想要你的工具不提供的行为,而丢弃该字段意味着在沉默中违背了它的初衷),或者是由于 Schema 版本不匹配导致的偏差。这三种情况你都希望在它们发生的那一回合立即暴露出来。

边界严苛,源头约束解码

这一切并不是为了辩解要容忍高失败率。它主张将鲁棒性放在不会破坏信息的地方——而行业的演进轨迹也说明了这一点。

两大主要供应商现在都在提供这样的功能:OpenAI 的结构化输出(structured outputs)通过 strict: true 约束解码(constrained decoding),使函数参数与提供的 Schema 匹配——他们的发布评估显示,Schema 合规率达到了 100%,而单纯依靠提示词在复杂 Schema 上的合规率不足 40%——Anthropic 则提供了严格的工具使用(strict tool use),确保工具名称和输入与声明的 Schema 一致。约束解码使得畸形的 语法(syntax) 在源头上几乎不可能出现:模型从字面上就无法生成违反语法的 Token 序列。

请注意这是什么:这是极早阶段的严苛,使其从不失败,而非宽容。供应商并没有构建一个更智能的强制转换层;他们将 Schema 约束移动到了 Token 采样阶段。这是正确的推进方向——严苛地生成,严苛地验证,不让任何中间层去“帮忙”。

然而,约束解码并不能取代验证层,将其视为万能良药本身就是一个陷阱。它保证了语法有效性(syntactic validity)——类型、必填字段、枚举成员——但不能保证语义有效性(semantic validity):例如 endstart 之前的日期范围、指向不存在内容的 ID、在上限为 25 的端点上设置 100 的 limit,或者是语法完美但完全选错工具的调用。跨字段约束、引用检查和业务规则仍然存在于你的验证层中,并且它们应该以同样的结构化、模型可读的错误形式返回失败。这两种机制是互补的:约束解码处理语法,边界验证处理意义。

这里还隐藏着一条乏味但至关重要的工程准则:只使用唯一的 Schema 定义,验证器和面向模型的工具描述都应从同一源头生成。对现实世界 MCP 故障的分类研究不断发现同一个根本原因——模型看到的 Schema 与运行时强制执行的 Schema 发生了偏离,导致破坏出现在运行时而非集成时。在这种失败模式下,模型相对于一个 Schema 是“错误”的,而相对于另一个是正确的,任何重试都无法解决你自身产物之间的一致性矛盾。

检查清单

对于正在构建或审计 Agent 工具层的团队,这些原则可以浓缩为以下几项决策:

  • 在执行前,根据严格的 Schema 验证每一个工具调用。 设置 additionalProperties: false,无隐式类型转换,枚举精确匹配。
  • 原子化拒绝。 绝不执行部分有效的调用;绝不为了使调用有效而丢弃字段。
  • 返回结构化、模型可读的错误:字段、约束、接收值以及有效范围或选项。编写这些错误时,要假设读者无法访问源代码。
  • 限制重试循环。 进行两到三次验证重试,然后升级处理——转人工、使用备选方案或直接报告失败。重试格式错误;不要重试需要不同处理方式的语义失败。
  • 将每个工具的验证失败率作为核心指标进行追踪。 这是模型漂移、提示词退化和 Schema 描述歧义的最早预警。
  • 利用供应商的严格模式(约束解码)从源头上消除语法故障,并在边界保留语义验证。
  • 从同一个定义生成面向模型的 Schema 和运行时验证器,以确保它们不会产生偏差。

波斯塔尔法则(Postel's law)之所以在历史上占有一席之地,是因为它解决了一个现实问题:无法与彼此作者沟通的实现方案之间的互操作性。Agent 系统并没有这个问题——边界另一侧的“实现”是一个能够阅读你的错误信息并在下一回合自我修正的模型。在那个世界里,宽容地接受并非慷慨,而是对你唯一的反馈渠道的蓄意破坏。对于你的 Agent 发送的内容要保守——约束解码能帮你完成大部分工作——而对于你的工具接受的内容要严苛、精确且表达清晰。整个循环的可靠性取决于边界拒绝保持“礼貌”。

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