模式就是契约,好的模式让错误的调用根本无法表达出来。
模型靠在上下文里对着模式做模式匹配来填你工具的参数,所以模式不是验证文书工作——它是指令集。你设为可选的每个字段,都是你委派给一个随机性调用方的决策;每个松散的 string,都是它可以塞任何东西的地方;每个缺失的默认值,都是它必须回答、且可能答错的一个问题。本文讲的是这样设计模式:让可表达的调用空间几乎恰好等于正确的调用空间——让误用在结构上不可能,而不是事后去捕捉。
在任何验证器运行之前,模式已经在教模型。
等到验证器拒掉一次坏调用,对循环的伤害已经造成:一个浪费的来回、一个困惑的模型,常常还有一次以同样方式失败的重试。模式是你第一道、也最便宜的干预,因为模型在发出调用之前就读它、并向它看齐。一个名为 iso_date、类型为带格式提示和一行描述的字符串的字段,产出正确日期的可靠性,远高于一个名为 date、靠模型从未见过的正则来验证的字段。把正确性上移进形状里;验证器是后挡板,不是老师。
让非法状态无法被表示,而不只是被拒绝。
最强的模式设计借鉴了类型驱动设计:如果某种参数组合是无效的,模式应让它无法被写出来,而不只是提交时无效。值已知时,用 enum 而非自由字符串。用一个判别联合({type: "email", address} 对 {type: "sms", phone}),而不是一个带互斥可选字段、模型可能两个都填或都不填的扁平对象。用最小/最大约束数字。你编码进去的每个约束,都是一类模型错误从此不再可能发生——被删除,而非被检测。
# Weak: every field optional, enum as free string, no bounds notify(channel: str = None, email: str = None, phone: str = None, retries: int = None) # Strong: union makes "email with no address" inexpressible notify(target: EmailTarget | SmsTarget, # exactly one, fields required retries: int = 2) # sane default, 0..5 bounded
必填还是可选是一个风险决策,不是便利决策。
本能是把字段设为可选「以求灵活」。但可选意味着模型来决定,而模型是在歧义下、飞快地、有时错误地决定。一个字段只有在「存在一个绝大多数情形下都正确的默认值」且「错误省略的代价很低」时,才正当地可选。任何与安全相关的东西——被扣款的账户、写入的目的地、一个确认标志——都应当必填且显式,迫使模型陈述意图,而不是落入一个它从未推理过的默认值。
在一个有副作用的工具上,一个没有默认值的可选字段是两头不讨好的最坏情况:模型常常省略它,工具于是要么瞎猜要么失败。如果它重要,就设为必填;如果它不重要,就给它一个真正的默认值。「可选、无默认」是一个潜伏的 bug。
默认值是你替模型做出的决策——做那个安全的。
每个默认值都是在模型什么都不说时触发的选择,所以它必须是「智能体没去想这事时你会想要」的那个选择。默认值偏向可逆的、有界的、空跑的。一个删除工具应默认软删除,或要求一个显式的硬删除标志;一个查询应默认返回一小页,而非整张表;任何破坏性操作都应默认 dry_run=true,让未经斟酌的调用成为安全的那个。原则是:最少说明的路径,应当是最少伤害的路径。
描述与范例是模式的一部分,因为模型会读它们。
JSON Schema 不只是类型——每个字段都能携带一段描述,而模型会用它。回报最高的工具修改,往往是消除歧义的一行字段描述(「金额以分计,不是以元计」「UTC,ISO-8601」「必须是 search 返回的 ID,而非名称」)。2025 年一项生态研究发现,97% 的 MCP 工具描述至少有一个质量问题、56% 的用途陈述不清——这意味着当今多数智能体最便宜可得的提升,就是把模式描述当作提示词来写,因为它就是。
给任何格式不显然的参数,在字段描述里放一个范例。描述里的「例如 order_8f3a」比任何验证器捕捉到的格式错误都防得多,且只花一行。
何时严格模式是错的工具。
过度约束有实打实的代价:一个僵硬到表达不出正当边界情形的模式,会逼模型扭曲变形,或让能力变得不可达;而一个深嵌套的联合,可能比一个带良好描述的更扁平的形状更难让模型正确填写。为删除错误类别而约束,不为炫耀类型系统而约束——如果更严的模式让常见调用更难表达,你优化错了对象。