去年接了一个客服工单自动分类的项目,大模型需要从用户对话里抽取出客户姓名、订单号、问题类型、紧急程度。最开始我直接在提示词里写“请输出JSON”,结果模型返回的字段名一会儿是customer_name,一会儿是客户,还有一次把订单号输出成了"ORD-12345"这种多一个字符的格式,下游系统直接崩了。后来我把JSON Schema加上,问题当场解决了一大半。这个经历让我意识到:在人工智能应用里,JSON Schema不是一份可有可无的说明书,而是AI和业务系统之间真正的“数据契约”。这篇文章就围绕json schema和人工智能这个组合,聊聊我踩过的坑、验证过的方法,以及怎么用Schema把大模型的输出变得可控、可用、可验收。
如果你正在做大模型应用开发、智能体工具调用、训练数据清洗,或者做AI相关的大作业/毕业设计,这篇内容应该能帮你省下不少调试时间。哪怕你只是听说过“结构化输出”这个概念,也能从这里拿到一套可以直接照着做的方案。
1. 为什么AI应用需要一份数据契约(而不是靠运气)
1.1 模型输出的"自由发挥"带来的麻烦
大模型天生是在“预测下一个词”,它并不像传统程序一样对数据结构有硬性保证。你让它返回JSON,它大概率会返回,但字段名、嵌套层级、值类型、是否多出额外字段,这些都是概率事件。很多人在调试AI应用时遇到过类似场景:本地测试好好的,一到线上,模型突然给某个字符串字段返回了null,或者把数字100写成了"100.00",然后整个解析逻辑就炸了。
这个问题的本质是:模型的输出是一个“自然语言表达”,而业务系统需要的是一个“严格可解析的数据结构”。两者之间缺一个转换层和约束层。如果你只靠提示词去约束,相当于让模型猜你的潜规则;而JSON Schema恰好能把这种潜规则显式化,让模型在生成时、系统在解析时都有一份明确的规则可依。
1.2 JSON Schema在AI链路中的三个位置
按我自己的实践经验,JSON Schema在AI应用里通常出现在三个位置,对应三种完全不同的职责。
第一,前置约束。在请求大模型时,通过提示词或API参数把Schema传给模型,让模型在生成阶段就按这个结构输出。比如OpenAI的结构化输出(response_format)支持传入json_schema,Google Gemini也支持类似的response_schema。这是最直接的方式,能明显降低格式错误率。
第二,中置工具调用。在智能体(Agent)场景里,模型需要调用外部工具,比如查天气、下单、查数据库。平台要求每个工具都提供一个JSON Schema格式的参数声明,模型看到这个Schema才知道该填什么参数、参数类型是什么。这个位置上的Schema实际上是“模型理解外部世界接口”的桥梁。
第三,后置校验与纠错。无论前面做了多少约束,模型输出仍然可能不合法。这时候需要在进入业务逻辑之前,用JSON Schema做一次硬校验,不合格就让模型重新生成,或者走兜底逻辑。这是最常见、也最稳妥的做法。
1.3 JSON Schema到底是什么(给第一次接触的朋友)
JSON Schema本身只是一个JSON文档,用来描述另一个JSON文档应该长什么样。它定义了这个JSON对象里有哪些字段、字段的类型、哪些必填、取值范围、数组元素结构、嵌套层级等。它不是代码,而是“元数据”,但几乎所有主流语言都有对应的校验库,比如Python的jsonschema、JavaScript的ajv、Java的everit-json-schema等。
你可以把JSON Schema理解成一张“试卷的答题卡”:模型负责往空格里填内容,Schema负责告诉它每个空格能填什么样的内容,系统最后拿红笔对着答题卡批改。有了这张答题卡,AI输出就不再是“听天由命”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 把模型输出"锁死":定义Schema与校验实战
2.1 从需求到Schema:把业务规则翻译成结构
一个常见的错误是:直接抄一个简单Schema去用,比如只写type: object加几个字段,然后发现模型还是乱输出。真正可用的Schema一定是从业务规则中推导出来的。
拿前面提到的客服工单抽取来举例。业务上要求必须返回:订单号(必须以ORD-开头加6位数字)、客户姓名(非空字符串)、问题类型(只能从“售后、物流、发票、其他”里选)、商品明细(至少包含一件商品,每件商品有名称和数量)、总金额(大于0的数字)。这些规则翻译成JSON Schema,大概是这样的:
json复制{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^ORD-[0-9]{6}$"
},
"customer_name": {
"type": "string",
"minLength": 1
},
"issue_type": {
"type": "string",
"enum": ["售后", "物流", "发票", "其他"]
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"quantity": {
"type": "integer",
"minimum": 1
}
},
"required": ["name", "quantity"]
}
},
"total_amount": {
"type": "number",
"exclusiveMinimum": 0
}
},
"required": ["order_id", "customer_name", "issue_type", "items", "total_amount"],
"additionalProperties": false
}
这里有三个容易被忽略的点。第一,required数组决定了哪些字段模型必须给,缺失直接判失败。第二,additionalProperties: false用来禁止模型自己创造额外字段,比如它很爱加一个summary总结字段,但下游根本用不到。第三,pattern用正则把订单号格式锁死,省去下游再做一次正则匹配。
初次设计时,建议把业务规则逐条列出来,再逐条对应到Schema关键字上。每一条规则都要有来源,不要拍脑袋加约束,否则后面会给模型造成不必要的生成压力。
2.2 我的校验工具链:Python jsonschema 与错误反馈闭环
平时我用得最多的是Python的jsonschema库。安装很简单:
bash复制pip install jsonschema
然后可以这样校验模型返回的数据:
python复制import json
from jsonschema import Draft7Validator
schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"order_id": {"type": "string", "pattern": "^ORD-[0-9]{6}$"}
},
"required": ["order_id"],
"additionalProperties": False
}
data = json.loads(model_response_text)
validator = Draft7Validator(schema)
errors = sorted(validator.iter_errors(data), key=lambda e: list(e.path))
for error in errors:
print(f"字段: {list(error.path)} 错误: {error.message}")
iter_errors比validate更好用,因为它会把所有错误一次性列出来,而不是遇到第一个错误就抛异常。在调试阶段,我通常会把错误信息转成中文提示,拼接成一段文本,发给模型让它修正。这个“校验失败 → 把错误喂给模型 → 重新生成”的循环,我把它叫做“智能校验闭环”,实测下来能把格式通过率从70%拉到99%左右。
举个例子,如果模型漏了order_id,反馈给模型的错误信息可以这样构造:
code复制生成结果未通过JSON Schema校验,请根据以下错误修改后重新输出:
- 缺少必填字段: order_id
- 字段 total_amount 的类型应为 number,当前是 string
模型看到带有具体字段名和原因的错误提示,基本都能准确修正。这里有一个关键细节:不要只把原始校验异常堆栈丢给模型,要把错误信息转成模型能看懂的自然语言。否则模型会越来越糊涂。
2.3 定义Schema时最容易踩的五个坑
第一个坑:只写type: object和properties,不写required。我见过很多次,模型返回一个空对象,校验居然通过了。因为Schema默认所有字段都可选。在设计时,必须从业务上判断哪些字段缺失会出问题,然后把它们全部加进required。
第二个坑:additionalProperties: false和模型“过度生成”之间的冲突。模型很喜欢在JSON里加一些原文没有的字段,比如confidence、reason。如果你开了additionalProperties: false,这些字段会被直接判错。解决方法是:要么在Schema里显式定义这些可选字段,要么在Prompt里强调“仅输出Schema中定义的字段”。
第三个坑:number和integer混用。模型可能把整数3输出成3.0,如果Schema定义的是integer,有些严格实现会判错。建议根据业务需求选择:如果数量一定是整数,用integer并配合multipleOf: 1;如果只是数值,用number字段,避免误伤。
第四个坑:字符串枚举大小写不一致。比如enum: ["Refund", "refund"],模型输出REFUND就会被判错。处理办法:在enum里把模型可能出现的候选值都列全,或者用pattern做大小写不敏感匹配。但最稳妥的还是控制Prompt,明确告诉模型“只能从以下选项中选择,一个字母都不能改”。
第五个坑:过度约束导致模型反复重试,token消耗飙升。我见过一个团队在Schema里塞了很多pattern,一个字段写了三个正则,结果模型输出经常不满足其中一条,反复调用了五次才通过,Token成本翻了三倍。Schema的粒度要适中:核心字段严格约束,边缘字段允许放宽。记住,校验规则是用来保证系统安全的,不是用来折磨模型的。
3. 智能体与工具调用:JSON Schema是AI的"接口说明书"
3.1 让大模型正确调用工具的底层逻辑
今天的主流大模型平台,比如OpenAI、Claude、通义千问、文心一言,都支持“函数调用”或“工具调用”。本质上,它们不是让模型直接执行代码,而是让模型在合适的时候输出一个“调用某个工具的请求”,然后由你的业务系统去执行真正的工具,再把结果返回给模型。
这个“工具调用请求”能不能被正确生成,完全取决于你给模型提供的工具定义是否清晰。以OpenAI的tools参数为例,每个工具都要写一个function,里面有name、description和parameters。而parameters就是一个标准JSON Schema。你可以把它理解成工具的使用说明书:模型在阅读这个说明书之后,才知道要不要调用、传什么参数。
如果parameters写得含糊,模型就会自己发挥。比如你定义一个查询天气的工具,声明了一个city字符串字段,但没写格式,模型就可能传"北京市",也可能传"Beijing",而你的天气API只接受中文不带“市”的城市名。这时候就需要在description里写明规则,并且在Schema里用pattern或enum约束。
3.2 工具参数Schema的进阶设计
工具参数Schema的设计,和普通数据校验不太一样。它既要保证模型容易理解,又要保证业务方拿到参数后可以直接执行。我总结了几条很实用的设计经验。
第一,能枚举的字段尽量用enum。比如订单状态,模型如果不知道可选项,就会凭空想出一个状态值。与其让它猜,不如把可选值直接列出来。enum对模型来说相当于“选择题”,比“填空题”更稳。
第二,用description写清楚模型会关心的细节。在JSON Schema里,description字段对模型是有实际影响的,因为平台会把整个Schema放进模型上下文里,模型会阅读description来理解参数。我在description里通常会写:单位、格式、默认值、举例。比如:
json复制{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名,使用中文,不带省份和市后缀,例如:北京、上海"
},
"days": {
"type": "integer",
"description": "预报天数,可选1到7,默认3"
}
},
"required": ["city"],
"additionalProperties": false
}
第三,注意参数之间的依赖关系。有的工具参数之间是有约束的,比如“如果填写了end_date,就必须同时填写start_date”。JSON Schema里有dependencies和if-then等关键字可以表达这种依赖。但这里要提醒一下,不是所有模型平台都完整支持这些高级关键字。很多平台只支持基础的properties、type、required、enum,高级约束可能不会生效。所以我会在description里再补一句“start_date和end_date必须一起出现”,双保险。
第四,同一个工具尽量减少必填参数数量。模型的选择越少,出错概率越低。非必须字段都设为可选,给一个合理的默认值,让模型可以少填。这能显著降低调用失败率。
3.3 多个Skills并存时的Schema管理
最近“AI skills”这个概念非常火,很多智能体平台支持把一个个小技能打包,让AI调用。这些skills本质上就是一组带JSON Schema的工具定义集合。当你的Agent里有五六个skills时,Schema管理的复杂度会明显上升。
我遇到过的最典型问题是:两个skills的工具名或参数名发生冲突。比如一个“查订单”的skill和另一个“查物流”的skill,都定义了order_id参数,但一个要求"ORD-123456"格式,另一个要求"123456"纯数字。模型在调用时就很容易混淆,传错参数。我的解决办法是:在Schema的description里把参数格式写清楚,并把工具命名改为更具体的名字,比如query_order_by_id和track_logistics_by_order_number,让模型一眼看出差别。
多个Skills并存时,我还会在参数Schema里加一个_version字段(设为隐藏的常量),用来追踪当前协议版本。这样即使后面改了格式,也能在日志里快速定位是哪个skill在生效。
另外,不同skills之间的Schema风格要尽量统一。比如日期时间字段,有的用"2026-08-01",有的用"2026/08/01",模型很难稳地切换。建议团队内部定一套统一的工具Schema规范,把日期、金额、ID、枚举的书写格式全部固定下来。这个小投入能换来很大的稳定性提升。
4. 训练数据与评测场景:用Schema守住质量线
4.1 微调数据清洗中的结构校验
除了应用调用,JSON Schema在大模型训练数据管理中也很有价值。特别是做指令微调(SFT)时,训练数据通常是一行一个JSON样本的JSONL文件,每条样本包含instruction、input、output等字段。如果其中某条样本缺了output,或者output本身不是合法JSON,训练时候很容易出奇怪问题。
我在准备微调数据时会先写一个针对样本结构的Schema,然后对所有训练样本做一遍批量校验。一个简单的样本Schema示例:
json复制{
"type": "object",
"properties": {
"instruction": {"type": "string", "minLength": 1},
"input": {"type": "string"},
"output": {
"type": "object",
"minProperties": 1
}
},
"required": ["instruction", "output"],
"additionalProperties": false
}
批量校验时,我会记录每一条失败样本的行号和错误字段,直接生成一个清洗报告。这样不用人肉翻几千行JSONL,几分钟就能筛出问题数据。这个步骤在训练管线里应该做成自动化的CI检查,每次数据变更后自动跑一遍,而不是等到训练时才发现异常。
4.2 自动化评测的格式判分
做大模型应用评估时,我们经常要判断模型回答格式是否符合要求。人工一个个看太慢,而且标准不统一。用JSON Schema作为“自动判分器”的效果很好。
比如“人工智能训练师”里经常出现实操题,要求学员编写的程序输出固定格式的JSON。这类题目用Schema做判分非常合适。先定义一个正确答案的Schema,再把考生输出解析成JSON,然后用jsonschema校验,还要加上“是否包含必需字段”“字段类型是否正确”“枚举值是否合法”这几个打分维度。相比让老师逐个人工看,效率高很多,而且评分标准完全一致。
你还可以把校验错误按照严重程度分层:字段缺失是致命错误,类型错误是主要错误,内容不规范是次要错误。不同错误对应不同的扣分规则,这就成了自动化评测的一部分。
4.3 token开销与校验效率的平衡
很多做AI应用的人一听到“把Schema传给模型”,第一反应是“这得多花多少token”。确实,一个复杂的Schema可能有几百行JSON,折算成token可能几百甚至上千。但要注意,这通常是一次性输入,而换来的是更低的失败重试率。如果因为不用Schema导致输出格式错误率20%,每次失败都要重新调用一次,多花的token往往比传Schema的成本更高。
我实测过一个常见的抽取场景:一个大约80行的Schema,转化为token大概400多。如果不传Schema,靠纯Prompt约束,失败率在25%左右,平均每4次就有1次需要重试。传Schema后失败率降到5%,总体token消耗反而节省了约15%。当然,如果你的字段特别多,可以用response_format的结构化输出特性,它在部分平台上有更优的实现,不会像把Schema塞进Prompt那么费token。
另一个平衡点是:前置约束和后置校验该用哪个。我的经验是,对稳定性要求高的核心链路,两者都要用:前置约束降低出错的概率,后置校验保证出错必被发现。对低成本链路,比如只是做一次简单的意图判断,可以只做后置校验,让模型自由输出,再用Schema兜底。这样能在效果和成本之间找到比较舒服的平衡点。
5. 我在实战中反复踩过之后留下的几条经验
5.1 Schema要跟着代码一起做版本管理
JSON Schema不是一份临时文档,它就是你系统的接口定义。改Schema会和改API一样对下游产生影响。我在代码库里会单独建一个schema/目录,每个Schema文件都带上版本号,比如order_extract.schema.v2.json。所有变更都走代码评审,不直接改线上。这样即使模型升级或者需求变更,也能追溯到合同是哪个版本。
5.2 校验失败后给模型的反馈信息要“说人话”
我发现很多人把jsonschema报出的原始错误直接拼进Prompt让模型改,结果模型根本看不懂“'foo' is a required property”是什么意思。我会写一个把错误转换成自然语言的小函数,比如把缺少字段转换成“你漏了订单号字段,请补上”,把类型错误转换成“总金额应该是数字,不是字符串”。模型对这个反馈的理解效率会高很多。这个小函数只需要几十行代码,但效果立竿见影。
5.3 先松后严,迭代式加约束
刚开始做一个AI功能时,不要把所有规则一次性写死。我会先放一个宽松的Schema,只校验必填字段和核心类型,保证功能能跑通。然后根据线上日志,看模型在哪个字段上出问题最多,再逐步加严约束。比如如果发现模型经常把金额写成字符串,就在Schema里加上type: number并配合Prompt强调。这种方式比一次到位更符合实际,因为你前期很难预料模型的全部“自由发挥”。
5.4 换模型版本后一定要重新回归测试
不同版本的大模型,对JSON Schema的服从程度可能完全不一样。同一个Schema在GPT-4上很稳,换了一个小参数量的开源模型,可能就频繁出错。所以每次更换模型或升级模型版本,我会拿一批历史真实样本重跑一遍Schema校验回归,看看通过率有没有明显下降。这个动作能提前暴露问题,而不是等线上报警了才查。
5.5 最后分享一个小技巧:用examples字段给模型“抄作业”
JSON Schema的examples关键字虽然不会参与校验,但在很多模型平台上会作为示例被模型参考。每个字段写一个符合业务要求的例子,模型模仿起来会非常准。比如order_id的示例写"ORD-123456",模型大概率就会按这个格式生成。这比在Prompt里单独写例子要省事,因为它是跟着Schema走的,结构化更强。
这些年做AI应用,我最大的感受是:模型能力越来越强,但越强的模型越需要清晰的边界。JSON Schema恰好能提供这个边界。它不挑语言、不挑平台,是模型和业务系统之间的公共语言。如果你正在被模型输出的随机性折磨,不妨从一份Schema开始,把混乱收敛起来。
