
用 JSON Schema 约束 AI 分类输出并做校验
用 JSON Schema 约束 AI 分类输出并做校验
AI 返回的分类结果每次措辞都不一样,程序很难稳定读取。这篇教程用 JSON Schema 给 AI 的输出定好格式,讲清字段类型、必填项、枚举值和多余字段的处理方式,并演示接收端如何先校验再分流,同时说明格式正确不等于判断正确。
让 AI 对用户咨询做分类,再根据分类结果走不同的处理流程,是自动化里很常见的一环。麻烦在于模型的回答往往每次措辞都略有不同:有时说「分类是退货」,有时说「属于退货」,程序不知道该读哪几个字。这篇教程讲的是用 JSON Schema 把 AI 的输出约束成固定结构,并在接收端做校验,让后续程序能稳定地取出字段值。适合正在把大模型接入业务流程、需要程序化消费模型输出的开发者阅读。
核心思路可以概括成一句话:把「格式正确」和「内容正确」当成两件事分别处理。JSON Schema 只负责前者,后者要靠业务规则和人工复核。
准备工作
动手之前,先确认下面几件事,否则后面容易卡住。
- 一个能返回结构化输出的模型调用方式。 不同服务商对结构化输出的支持程度不一样,有的支持完整的 JSON Schema,有的只支持「返回 JSON」这种较弱的约束,有的只接受简化的字段描述。具体支持到哪一步,以所用服务商官网的当前文档为准。
- 一个 JSON Schema 校验库。 主流语言都有成熟实现,例如 JavaScript 生态里的 Ajv、Python 生态里的 jsonschema。选哪个不影响本文的思路,只要能在接收端对返回的 JSON 做校验即可。
- 一份明确的分类口径。 在写 Schema 之前,先把分类名定死。比如把咨询分成「配送」「退货」「其他」三类,每个类别对应什么情形要写清楚。分类名一旦进入
enum,改动成本就会变高。 - 一条失败处理路径。 校验不通过时是重试、转人工,还是直接终止流程,需要提前想好。没有这条路径,校验就只是摆设。
操作步骤
第一步:先确定程序真正要读的字段
不要一上来就设计 Schema。先问自己:后续流程到底需要哪几个值?
假设场景是把用户咨询分流给不同团队。程序需要知道两件事:这条咨询属于哪个类别,以及模型为什么这么判断(便于人工抽查)。那么输出可以设计成两个字段:
{
"category": "退货",
"reason": "用户在询问寄回商品的方式"
}
这里 category 放分类结果,reason 放判断理由。可以把它想成一张纸质申请表:表上印好了「项目名」和「填写栏」,填表人只能往对应的栏里写。
字段越少越好。每多一个字段,接收端就多一条要处理的规则;而后续流程用不到的字段,只会增加出错面。
第二步:写出最小可用的 JSON Schema
JSON Schema 是用来描述 JSON 数据结构与取值条件的规范。针对上面的输出,一份最小可用的规则如下:
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["配送", "退货", "其他"]
},
"reason": {
"type": "string"
}
},
"required": ["category", "reason"],
"additionalProperties": false
}
逐条看每个关键字在约束什么:
| 关键字 | 约束内容 |
|---|---|
type: object | 整体必须是一个带具名字段的对象,而不是数组或字符串 |
properties | 声明 category 与 reason 两个字段各自的取值类型 |
enum | 把分类名限定在三个固定值之内,杜绝「退货」「退款」「退回商品」这类写法混入 |
required | 两个字段都必须出现 |
additionalProperties: false | 不接受未定义的字段 |
这里有两个特别容易踩的坑:
- 写进
properties不等于必填。properties只是描述「如果这个字段出现,它应该长什么样」。要让它必须出现,必须在required里再列一遍。只写properties而漏掉required,模型少给一个字段时校验依然会通过。 - 禁止多余字段要显式声明。 默认情况下,Schema 并不排斥未定义的字段。想让
additionalProperties: false生效,必须自己写上去。否则模型顺手多返回一个confidence或note,校验照样通过,而下游代码可能因为字段对不上而出错。
第三步:把 Schema 交给模型,要求按结构输出
Schema 写好后,要在调用模型时把它作为输出格式的约束传进去。不同服务商的传参方式差别很大,有的把 Schema 放在请求体的 response_format 一类字段里,有的用独立的参数名,有的只接受简化后的字段描述。具体写法请以所用服务商官网当前文档为准,这里不给出可能过时的参数名。
需要强调的是:模型侧的格式约束和应用侧的校验是两个不同的角色,不能互相替代。 模型侧负责「尽量按这个形状生成」,应用侧负责「确认它真的符合形状」。只做前者,一旦模型偶发偏离,脏数据就直接进了业务流程;只做后者,会频繁触发失败重试,浪费调用。
第四步:在接收端做校验,失败就中断
拿到模型返回的文本后,处理顺序应该是:
- 先尝试把文本解析成 JSON。解析失败说明返回的根本不是合法 JSON,直接进入失败路径。
- 解析成功后,用第二步的 Schema 做校验。校验失败说明结构不对,同样进入失败路径。
- 两步都通过,才把字段值交给后续流程使用。
失败路径不要设计成「忽略错误继续跑」。合理的做法是停止自动处理,或者转成待人工确认的任务。把一条结构不合法的数据硬塞进下游,往往比直接报错更难排查。
第五步:把「格式校验」和「内容复核」分成两道关
这是整篇教程最需要记住的一点。完整的处理链条应该是这样的:
AI 返回
↓
能否解析为 JSON
↓
是否符合 JSON Schema
↓
分类依据是否成立、是否需要人工确认
↓
分流等后续处理
举个具体的例子。{"category":"退货","reason":"用户在询问寄回商品的方式"} 完全符合前面的 Schema,格式上挑不出毛病。但如果用户原话是「我的包裹到现在还没送到」,那这个分类本身就是错的——Schema 校验通过,语义却是错的。
Schema 校验不保证含义正确。 对于把握不大的分类,或者后果较重的处理动作(比如自动退款、自动关闭工单),必须另外安排与原输入比对、或者人工确认的环节。
一个完整示例
下面把前面的步骤串成一个能跑通的最小例子。示例用伪代码描述流程,校验部分给出可直接套用的 Schema 与判断逻辑。
场景
用户提交一条咨询文本,系统调用模型判断它属于「配送」「退货」还是「其他」,并给出理由。分类为「退货」的进入退货处理队列,其余进入通用队列。所有结果都先过校验,校验失败则转人工。
Schema 定义
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["配送", "退货", "其他"]
},
"reason": {
"type": "string"
}
},
"required": ["category", "reason"],
"additionalProperties": false
}
处理流程
function handleInquiry(text):
raw = callModel(text, outputSchema = SCHEMA) # 把 Schema 作为输出约束传入
# 第一道关:能否解析为 JSON
try:
data = parseJson(raw)
catch:
return routeToHuman(text, reason = "返回内容不是合法 JSON")
# 第二道关:是否符合 Schema
errors = validate(data, SCHEMA)
if errors is not empty:
return routeToHuman(text, reason = "结构校验失败", detail = errors)
# 第三道关:业务侧复核(示例:低置信或高风险类别转人工)
if needsReview(text, data):
return routeToHuman(text, reason = "需人工确认分类")
# 通过全部关卡,进入分流
if data.category == "退货":
return enqueue(returnQueue, text, data.reason)
else:
return enqueue(generalQueue, text, data.reason)
几个校验结果对照
用同一份 Schema 去校验不同的返回,可以直观看出每道关在拦什么:
| 模型返回 | 结果 | 原因 |
|---|---|---|
{"category":"退货","reason":"询问寄回方式"} | 通过 | 字段齐全、类型正确、取值在枚举内 |
{"category":"退货"} | 不通过 | 缺少 reason,违反 required |
{"category":"退款","reason":"..."} | 不通过 | 退款 不在 enum 允许的取值内 |
{"category":1,"reason":"..."} | 不通过 | category 不是字符串 |
{"category":"退货","reason":"...","confidence":0.9} | 不通过 | 出现未定义字段,违反 additionalProperties: false |
分类是退货 | 不通过 | 不是合法 JSON,解析阶段即失败 |
最后一行值得注意:如果只在提示词里写「请返回 JSON」,模型仍有可能回一句自然语言。所以解析失败必须被当作一种正常会发生的分支来处理,而不是异常情况。
注意事项
不要只要求 JSON 就结束
在提示词里要求模型输出 JSON,只是第一步。接收端仍然要做解析和 Schema 校验,失败时停止自动处理或转人工复核。把「模型会守规矩」当成前提,是这类系统最常见的故障来源。
只把后续真正用到的字段设为必填
必填字段缺失会导致流程无法继续,所以该必填的要写进 required。但反过来,字段加得越多,需要满足的条件也越多,模型出错的概率随之上升。只保留下游真正消费的字段。
能枚举的值就用 enum 收紧
「退货」「退款」「把商品寄回来」这类同义不同形的写法,如果不在 Schema 层面收住,接收端就得每次自己解释一遍。用 enum 把取值限定成固定几个,能省掉大量归一化代码。代价是分类口径要提前定清楚,后期增删类别需要同步改 Schema 和下游逻辑。
确认所用 AI 服务的支持范围
并非所有服务都完整支持 JSON Schema。有的只支持一部分关键字,有的要求把 Schema 转成特定格式,有的对嵌套深度或字段数量有限制。接入前先查清楚当前支持到什么程度,以服务商官网的当前信息为准。同时记住:模型侧的输出指定和应用侧的校验是两件事,前者不能替代后者。
格式正确不等于判断正确
Schema 能保证字段存在、类型对、取值在允许范围内,但保证不了分类结果符合原意。对于影响较大的自动动作,务必保留与原输入比对或人工确认的环节。把这两件事混为一谈,系统看起来跑得很顺,实际却在稳定地产生错误结果。
价格与配额以官网为准
模型调用通常涉及计费和调用频率限制,具体价格、免费额度、速率上限会随时间调整,本文不给出具体数字。上线前请查阅所用服务商官网的当前信息,并按实际调用量估算成本。