当 tool_use 强制执行一个 JSON schema,其中某个 string 字段是 required 时,模型必须返回一个 string——结构上它不可能返回 null。如果源文档中根本不存在这个数据,模型就会编造一个看似合理的值。这不是模型的局限,是 schema 设计问题。
数据
| 字段可用性 | required 字段准确率 | nullable 字段准确率 |
|---|---|---|
| 总是存在 | 97-98% | 97-98% |
| 有时存在 | 74%(82% 是幻觉) | 90% |
| 很少存在 | 51%(93% 是幻觉) | ~90% |
对于总是存在的字段,required 和 nullable 没有区别。对于很少存在的字段,required 导致 93% 的幻觉率。schema 约束在强制编造。
规则
只有在每一份可能的输入文档中都确实存在的字段,才放进 required 数组。有时或很少存在的字段应该用 type: ["string", "null"],并排除在 required 之外。
{
"type": "object",
"properties": {
"customer_name": {"type": "string"},
"order_id": {"type": "string"},
"warranty_expiry": {"type": ["string", "null"]}
},
"required": ["customer_name", "order_id"]
}
warranty_expiry 现在可以在文档没有保修信息时返回 null。不需要编造。
Schema 约束覆盖一切
Prompt 指令(“缺失字段返回 N/A”)、展示 null 的 few-shot 示例、置信度评分——这些都无法覆盖 tool_use 的结构性要求。如果一个字段是 required 且 type: "string",模型就会生成一个 string,没有例外。
必须先改 schema。在结构性要求放宽之前,其他干预措施都不管用。
Null vs 哨兵值
用 0、"N/A" 或空字符串当”缺失”标记,会把”没有数据”和潜在的合法值混为一谈。0 可能是真实数量。"N/A" 可能出现在实际文本中。Null 是缺失数据在语义上正确的表示。
Enum + “Other” 逃生口
没有”other”选项的闭合 enum,在输入不匹配任何枚举值时会强制误分类。给 enum 加一个 "other" 再配一个 nullable 的 _detail 字段:
"category": {"type": "string", "enum": ["bug", "feature", "docs", "other"]},
"category_detail": {"type": ["string", "null"]}
这用最小的 schema 复杂度处理了无限的未来值。
一个 Schema 带 Nullable > 多个 Schema
按文档类型创建不同 schema 增加了路由复杂度和误分类风险。一个带 nullable 字段的 schema 能处理所有文档变体——有保修字段的发票、没有的发票、收据、报价单——用一个提取工具搞定。
一句话总结: 很少出现的字段在 schema 中要设为 nullable——required 字段强制模型为不存在的数据编造值,在源文档中不存在的字段上造成 93% 的幻觉率。