AB
AiBoss
Tutorials

用 JSON Schema 约束 AI 分类输出并做校验

Tutorials

用 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 一类字段里,有的用独立的参数名,有的只接受简化后的字段描述。具体写法请以所用服务商官网当前文档为准,这里不给出可能过时的参数名。

需要强调的是:模型侧的格式约束和应用侧的校验是两个不同的角色,不能互相替代。 模型侧负责「尽量按这个形状生成」,应用侧负责「确认它真的符合形状」。只做前者,一旦模型偶发偏离,脏数据就直接进了业务流程;只做后者,会频繁触发失败重试,浪费调用。

第四步:在接收端做校验,失败就中断

拿到模型返回的文本后,处理顺序应该是:

  1. 先尝试把文本解析成 JSON。解析失败说明返回的根本不是合法 JSON,直接进入失败路径。
  2. 解析成功后,用第二步的 Schema 做校验。校验失败说明结构不对,同样进入失败路径。
  3. 两步都通过,才把字段值交给后续流程使用。

失败路径不要设计成「忽略错误继续跑」。合理的做法是停止自动处理,或者转成待人工确认的任务。把一条结构不合法的数据硬塞进下游,往往比直接报错更难排查。

第五步:把「格式校验」和「内容复核」分成两道关

这是整篇教程最需要记住的一点。完整的处理链条应该是这样的:

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 能保证字段存在、类型对、取值在允许范围内,但保证不了分类结果符合原意。对于影响较大的自动动作,务必保留与原输入比对或人工确认的环节。把这两件事混为一谈,系统看起来跑得很顺,实际却在稳定地产生错误结果。

价格与配额以官网为准

模型调用通常涉及计费和调用频率限制,具体价格、免费额度、速率上限会随时间调整,本文不给出具体数字。上线前请查阅所用服务商官网的当前信息,并按实际调用量估算成本。