
工具调用的标准做法是给每个工具写一份 parameters schema,模型返回实参之后先校验,通过了再交给函数。这套流程默认 schema 写完就等于约束完整,而 JSON Schema 里有两个默认值刚好和直觉相反:没写的关键字不是「禁止」,是「随便」。把规范原文翻出来,再用本机的校验器跑一遍。
规范里这两条怎么写的
JSON Schema 2020-12 的 core 规范在 10.3.2.3 定义 additionalProperties:
10.3.2.3. additionalProperties
The value of "additionalProperties" MUST be a valid JSON Schema.
Validation with "additionalProperties" applies only to the child values of
instance names that do not appear in the annotation results of either
"properties" or "patternProperties".
Omitting this keyword has the same assertion behavior as an empty schema.
关键在最后一句:省略这个关键字,断言效果等于一份空 schema,而空 schema 对任何实例都通过。所以 properties 里没声明的字段,默认是全部放行,不是全部拒绝。
同一次调用里负责判类型的 validation 规范,在 6.1.1 定义 type:
String values MUST be one of the six primitive types ("null", "boolean", "object",
"array", "number", or "string"), or "integer" which matches any number with a
zero fractional part.
integer 判的是小数部分是不是零,1.0 也算 integer。这条在后面的实测里会看到。
实测:同一批实参,两份 schema 差多少
脚本存档 tools/verify-tool-schema.py,环境 Python 3.11.16 加 jsonschema 4.26.0,校验器用的是 Draft202012Validator。两份 schema 只差一个关键字:
BASE = {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
"days": {"type": "integer"},
},
"required": ["city"],
}
STRICT = {**BASE, "additionalProperties": False}
十组实参分别过这两份 schema:
实参场景 宽松schema 严格schema 严格版的报错
------------------------------------------------------------------------------------------------
模型只给了必填字段 通过 通过
schema 里声明过的字段 通过 通过
多了一个推理过程字段 通过 拒绝 Additional properties are not allowed ('reas...
单位写成大写缩写 拒绝 拒绝 'F' is not one of ['celsius', 'fahrenheit']
整数位给了小数 拒绝 拒绝 3.7 is not of type 'integer'
整数位给了布尔值 拒绝 拒绝 True is not of type 'integer'
整数位给了 null 拒绝 拒绝 None is not of type 'integer'
城市给了邮编数字 拒绝 拒绝 518000 is not of type 'string'
多了一个值为 null 的字段 通过 拒绝 Additional properties are not allowed ('note...
一个字段都没给 拒绝 拒绝 'city' is a required property
------------------------------------------------------------------------------------------------
宽松 schema 放过 4 / 10,严格 schema 放过 2 / 10
整数位放过的值:1=过 1.0=过 1.5=拦 True=拦 False=拦 '3'=拦 None=拦
宽松 schema 放过的四组里,前两组本来就该过,多出来的两组恰好都是「多带了字段」的实参:一组多了 reasoning,一组多了一个值为 null 的 note。这两个字段名对业务毫无恶意,模型常常顺手加上去,用来解释自己的推理。
放过去之后发生在函数这一层
工具函数的签名只有三个形参,多出来的字段在展开时直接抛错:
把模型给的实参直接展开进函数
正常实参 -> city=深圳 unit=fahrenheit days=3
宽松 schema 放过的实参 ->
File "/Users/yxh/blog/tools/verify-tool-schema.py", line 97, in <module>
print(get_weather(**bad_payload))
^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: get_weather() got an unexpected keyword argument 'reasoning'
这个报错算运气好的版本。把工具函数写成用 **kwargs 收下全部字段,异常就会推迟到更深的调用里,或者在某个不太重要的分支被静默吞掉。还有一种情况连签名都拦不住:多出来的字段名和某个内部参数撞上,校验放过,函数照收,行为跟着变,日志里只留下一行看起来正常的记录。
严格之后要照顾的两个模型习惯
第一是 null。实测里 {"city": "深圳", "note": None} 在严格 schema 下被拦,报的是 Additional properties are not allowed。模型用 null 表示「这个字段没有」很常见,遇到这种情况有两种处理:schema 里显式允许 null,或者在提示词里写清不要出现空字段。
第二是整数位。1.0 在两份 schema 下都通过,这一条和规范一致,业务上如果必须是整数,得在校验之后再加一次判断。同一列里 True 被拦下了,Python 里布尔是 int 的子类,jsonschema 把它判成整数以外的类型,这属于实现层面的选择,换一门语言的实现未必一致。
边界
模型本身的行为没有实测。这台机器上没有可用的模型 API,脚本里用的是写死的实参桩,验证的只是校验器和函数调用这两层。模型会不会多给字段,取决于提示词和模型版本,这一层我没有数据。能确定的是校验器这一层的默认语义:schema 怎么写就怎么判,省掉 additionalProperties 那一行,等于把参数约束交给模型的自觉。