Grok 结构化输出怎么写:json_schema 与工具调用两条路

2026-08-25

数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。

一句话说完:xAI 官方文档给的结构化输出有两条路,主路是在请求里传 response_format,把 response_format.type 设成 "json_schema" 并在 response_format.json_schema 下给出你的 schema;副路是走工具调用——官方说明 xAI 模型生成的工具调用参数总是严格符合工具的入参 JSON Schema,strict 标记隐式恒为 true。真正会咬人的不是这两个入口怎么写,而是官方只支持 JSON Schema 的一个「实用子集」:有些关键字是硬保障,有些只是尽力而为,还有一小撮直接返回 400。写之前先把这三档分清楚,比事后加一层校验省事得多。

先决定走哪条路:response_format 还是工具

官方文档把这两种方式并列,但它们解决的问题不一样。

response_format 是「我要模型的最终回复长成这个样子」。除了 "json_schema",这个参数还接受 "json_object"——只要求是格式良好的 JSON,不约束具体结构;以及 "text",也就是自由文本,官方注明这是默认值(以官方文档当前版本为准)。所以当你只是想避免模型在 JSON 外面裹一层「好的,以下是结果」的寒暄,用 "json_object" 就够了;当你要把结果直接灌进数据库或者下游函数,才需要上 "json_schema"

工具调用那条路的性质不同:它约束的是模型调用你的函数时传的参数,不是最终回复。官方特别注明,工具的 schema 遵循的是同一套 JSON Schema 支持规则。这一点很有用——本文后面讲的所有限制,你在写 function calling 的 parameters 时同样要遵守,不用再学一遍。

官方文档还提到,schema 可以用 Pydantic 或者 Zod 这类库来定义,而不是手写 JSON。实际项目里这几乎是唯一合理的选择,因为你本来就需要一个类型来接收解析结果,用同一份定义生成 schema 能避免两边漂移。

支持哪些类型:这份清单值得贴在工位上

官方明确列出了受支持的类型与组合关键字(以官方文档为准):stringnumberintegerbooleannullenumconstarrayobjectanyOfoneOfallOf、以及 $ref / $defs

其中三条附带了限定条件,很容易被漏读:

  • oneOf 官方说明其行为与 anyOf 完全一致。也就是说你指望它做「有且仅有一个分支匹配」的排他校验,是拿不到的。
  • allOf 只支持单个子 schema。多个子 schema 的 allOf 被归到了「尽力而为」那一档。
  • $ref / $defs 只支持非循环引用。树形结构、自引用的评论嵌套这类模型,需要你自己拍平成有限层级。

官方文档里 schema 的 draft 版本也有说法:按 Draft 2020-12 写的 schema 效果最好,Draft-07 的 schema 也接受。如果你的 schema 是从某个老工具链导出的,先确认它输出的是哪个 draft。

三条容易踩空的默认约定

这一节是我认为整篇官方文档里最值得单独拎出来的部分,因为其中有和很多人的 JSON Schema 肌肉记忆相反的地方。

additionalProperties 默认是 false,要放开必须显式写成 true 标准 JSON Schema 里这个关键字默认是允许额外属性的,xAI 这边反过来了。好处是默认收紧,模型不会给你塞计划外的字段;代价是如果你的下游确实要接受动态键,忘了显式打开就会出问题。

可空字段不能靠省略来表达,要用类型数组或 anyOf 官方给的两种写法是 {"type": ["string", "null"]},或者写一个包含 null 分支的 anyOf

没写进 required 的字段就是可选的。 这条本身符合直觉,但要和上一条连起来看:可选(字段可能不出现)和可空(字段出现但值是 null)是两件事,下游代码要分别处理。很多解析报错都出在把这两者当成一回事。

哪些 schema 会被直接拒掉

官方列出了会返回 400 错误的几种情况,遇到了不用怀疑是模型的问题,是 schema 本身没过校验:

  • enumanyOf 里一个变体都没有(零变体)
  • 属性的 schema 直接写成布尔字面量 truefalse
  • 用了 maxContains / minContains
  • items 写成数组形式——官方说元组校验要改用 prefixItems

最后一条最常见,因为老版本 JSON Schema 里 items: [A, B] 就是元组的标准写法,很多代码生成器至今还这么输出。如果你上线时突然收到 400,先去看有没有元组类型的字段。排查请求级错误的通用套路,可以对照接口返回结构变化的排查方法那篇一起看。

pattern 正则:能用一个子集,而且语义和 JS 不完全一样

字符串字段上的 pattern 关键字,官方说明支持 ECMAScript 正则(ECMA-262)的一个实用子集。

支持的部分包括字面量与字符类、.、交替 |、分组和非捕获分组、* + ? 与重复区间、简写类 \d \w \s 及其否定形式,以及常见转义。

不支持的部分需要重点记:反向引用、Unicode 属性转义(\p{...})、单词边界 \b、前后向断言(lookahead / lookbehind)、内联修饰符、以及条件表达式等高级构造。前后向断言用得相当多——比如「不以某前缀开头」的写法通常就是负向先行断言,这里得换思路。

官方还专门列了一节「与标准 JavaScript 正则的语义差异」,其中这三条不留意就会写出静默不匹配的正则:

  1. . 会匹配换行符(标准 JS 里默认不匹配)。
  2. ^$隐式的,模式永远匹配整个字符串,你不需要自己加锚点。反过来说,如果你按 JS 习惯写了一个只想匹配子串的模式,在这里会变成全串匹配而失效。
  3. 捕获组 (...) 没有语义作用,行为等同于非捕获组。

另外,format 关键字并不是全都强制执行。官方列出被强制校验的取值是:datetimedate-timeemailuuidipv4ipv6uri。不在这个清单里的 format 值仍然会被接受,但归入尽力而为一档。

分清「保障」和「尽力而为」

这是官方文档里一条很诚实的分层,也是设计 schema 时最该照着做取舍的依据。

官方的原话是:使用受支持的 schema 特性时,响应保证符合你的 schema。而另有一组关键字属于「接受但不做结构性强制」——模型会去处理它们,但输出不保证满足这些约束,官方建议如果需要严格符合就自行校验。落在这一档的有:notif / then / else、多子 schema 的 allOf、不在强制清单里的 format 值,以及超出保障上限的约束。

至于数值型约束,官方给了一张「保障到多大」的表:minimum / maximum / exclusiveMinimum / exclusiveMaximum 没有上限;而 minLength / maxLengthminItems / maxItemsminProperties / maxProperties 各自有一个保障阈值,超过阈值的 schema 仍然会被接受,但符合与否就取决于模型行为了。具体阈值请以官方文档当前版本为准——它属于会随实现调整的数字,不适合抄进代码注释里当常量。

实践上的结论很清楚:把业务的硬性校验放在你自己这一侧,schema 只用来保证形状。 形状(有哪些字段、类型是什么、是不是数组)靠 schema 保障;业务规则(金额区间、字段间的互斥关系、条件必填)用 if/then/else 表达是靠不住的,落到代码里校验。

parse() 和 response_format + sample()/stream() 怎么选

用 xAI 的 Python SDK 时,官方给了两种取结构化结果的写法。

一种是 chat.parse(Model),返回一个元组:完整的响应对象,加上已经解析好的模型实例。官方示例里是这样的形态:

response, invoice = chat.parse(Invoice)
print(invoice.vendor_name)
print(response.content)

另一种是创建 chat 时直接把 Pydantic 模型传给 response_format 参数,然后用 sample()stream() 取结果。官方说明这时 SDK 会自动把 Pydantic 模型转成 JSON schema、约束模型输出符合该 schema,并把符合模型定义的 JSON 字符串放在 response.content 里,由你自己解析:

chat = client.chat.create(model="...", response_format=Invoice)
response = chat.sample()
invoice = Invoice.model_validate_json(response.content)

官方对两者的适用场景给了明确建议:想要最省事、自动解析,就用 parse();如果你需要更多解析过程的控制权、需要先拿到原始 JSON 字符串再处理、需要把结构化输出和流式一起用、或者要接入已有的、本来就基于 sample() / stream() 的代码,就用 response_format 那条路。

流式这一条尤其值得注意。官方对流式的说明是:各个 chunk 会逐步拼出这个 JSON 字符串。对应的示例写法也很直白——循环里只是把每个 chunk 的内容打印出来,等循环结束之后,才拿完整的 response.content 调一次 model_validate_json 得到模型实例。也就是说,按官方示例的写法,解析动作是放在流结束之后的一次性操作,而不是边收边解析。如果你想在前端做「边生成边渲染」,官方示例没有给出对应写法,需要你自己决定是上一套增量 JSON 解析,还是把结构设计成可以分块交付、每块单独成型。流式中断怎么处理,另有流式响应中断与续传一篇可参考。

如果你不用 xAI 自家 SDK,官方文档里也给了 OpenAI SDK 兼容写法的示例——用 OpenAI 客户端把 base_url 指向 xAI 的接口地址,再用它自带的 parse 系方法。这条兼容路径的通用注意事项,可以看OpenAI 兼容端点怎么用

结构化输出 + 工具:有模型范围限制

官方在这一节挂了一条明确的限定:结构化输出与工具组合使用,仅在受支持的 Grok 4 系列模型上可用。 这是能力边界,不是建议,选模型时要先确认。

组合的两种形态官方都给了:一种是「智能体式工具调用」,也就是由模型自主编排的服务端工具,官方举的例子包括网页搜索、X 搜索和代码执行;另一种是 function calling,工具由你提供、执行也在你这边。两种都能和结构化输出叠加,得到的是「模型先用工具收集信息,再把结果按你的 schema 吐出来」。

这两种形态官方各给了一段示例,写法并不一样,值得对照着看。

智能体式工具那段示例里,创建 chat 时就把 tools 一起传了进去,随后直接调用 chat.parse(Model) 拿结果——工具的编排由模型在服务端自己完成,你这边从头到尾只有一次调用,没有循环。

客户端工具那段示例则是先进循环:调用 sample() 拿一次响应,只要响应里还有 tool_calls,就把响应追加回消息序列、逐个执行工具、再把工具结果追加回去;直到某一次响应里不再有 tool_calls 才跳出循环,循环外面单独再调一次 chat.parse(Model),这一步才拿到结构化结果。

两段示例的差别来源于工具在哪里执行:服务端工具的往返不需要你参与,所以一次调用就能走完;客户端工具每一轮都要你亲手接一次、把结果喂回去,取结构化结果的那一步自然只能放在循环之外。至于「在带客户端工具的请求里一开始就指定结构化输出会怎样」,官方文档里没有找到相关说明,照着对应形态的官方示例写是最稳的。

最后:几个真会让你返工的点

把最容易栽的坑收一下。

第一,additionalProperties 默认关闭这件事,一定要在团队里说一声。从别处搬来的 schema 往往依赖「默认允许额外属性」,搬过来行为就变了。

第二,别把业务校验塞进 schema。if/then/else 和多子 schema 的 allOf 都在尽力而为那一档,写了会给人一种「已经保证了」的错觉,反而比不写更危险。

第三,正则里那条「^$ 是隐式的、永远全串匹配」,是最容易写出静默 bug 的地方——模式没报错,就是一直不匹配。

第四,工具入参 schema 和 response_format 的 schema 走同一套规则,所以你为结构化输出踩过的坑,在 function calling 那边会原样再来一次,不如一次性把 schema 生成的那段代码收成公共模块。

真正开始接之前,建议先把 Grok API 怎么接入那篇过一遍,把鉴权和基础请求跑通,再回来加 schema。顺序反过来的话,schema 报错和鉴权报错混在一起,排查成本会高不少。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。