---
url: /frontend-ai/structured-output.md
description: >-
  讲前端接大模型怎么拿到能直接用的 JSON：OpenAI Structured Outputs（json_schema 加 strict）与 AI SDK
  的 Output.object 有什么区别、严格模式为什么强制 additionalProperties 设 false 和全字段
  required、可选字段怎么用 null 联合类型绕，以及我踩过的两个坑。
---

# 让大模型稳定吐 JSON 的两条路，一条先写死一条靠运气

做分类、打标签、抽字段，这些活都要模型返回 JSON。裸调 API 拿回来的是一段文本，偶尔夹一句「好的，以下是 JSON」，`JSON.parse` 直接炸。这篇文章讲两套解法，事实部分核对于 2026-10-10。

## 一条路：把解码器写死

OpenAI 的 Structured Outputs（结构化输出）是服务端层面的约束。它随 `gpt-4o-2024-08-06` 和 `gpt-4o-mini-2024-07-18` 两个快照上线（2024-08-06）。

思路是把你的 JSON Schema 编译进解码器。每生成一个 token，只允许采样那些不违反 schema 的 token，所以结构上不可能出错。

用法是把 `response_format` 设成 `json_schema` 并给 `strict: true`。Responses API 里参数挪到了 `text.format`。Batch API 和 function calling 也都支持，后者只需在函数定义里加 `strict: true`，覆盖面更宽（gpt-4-0613 之后的模型都行）。

老一点的 JSON mode（`json_object`）是另一回事。它只保证吐合法 JSON，不保证字段齐全、类型正确。我建议只要模型支持，一律用 Structured Outputs。

## 严格模式的三个限制

第一，每个对象节点必须写 `additionalProperties: false`。漏一个就整个请求被拒，错误信息会指到具体路径，比如 `additionalProperties must be false at #/properties/address`。

第二，`properties` 里声明的每个字段都必须进 `required`，没有原生可选字段。想要可选，用 null 联合类型：`"type": ["string", "null"]`。

第三，schema 是 JSON Schema 的子集。`oneOf`、`allOf`、`not` 一直不支持。硬限制：整个 schema 最多 100 个属性、嵌套最多 5 层、enum 总共 500 个值。

还有一点容易漏。Structured Outputs 不支持并行工具调用，开了要同时设 `parallel_tool_calls: false`。

## 另一条路：AI SDK 的 Output

前端视角更省事的写法是 Vercel AI SDK。但要注意版本，6.0 起 `generateObject` 和 `streamObject` 标记弃用。

现在推荐 `generateText` 配 `output: Output.object({ schema })`，schema 用 zod（TypeScript 的 schema 校验库）写。

```ts
import { generateText, Output } from 'ai';
import { z } from 'zod';

const { output } = await generateText({
  model: 'openai/gpt-4o-mini',
  output: Output.object({
    schema: z.object({
      sentiment: z.enum(['positive', 'negative', 'neutral']),
      keywords: z.array(z.string()),
      reason: z.string().nullable(), // 可选字段在应用层判 null
    }),
  }),
  prompt: '分析这条评论：' + comment,
});

console.log(output.sentiment);
```

`output` 直接是带类型的对象。zod 负责校验和补默认值，比如自动注入 `additionalProperties: false`。

流式场景用 `streamText` 加同样的 `output`，配合 `partialOutputStream` 能拿到逐步补全的半成品对象。

## 两条路怎么选

我的判断是：模型支持 Structured Outputs 就让它兜底，AI SDK 的 Output 只是省你写 schema 翻译的糖。

注意 AI SDK 遇到不支持的厂商时会退化成 prompt 工程加 zod 重试。此时「保证」就变成了「大概率」。

## 我踩过的两个坑

一个是 OpenAI 响应里的 `refusal` 字段。模型可能因为安全策略拒绝输出，此时 `content` 是 null，`refusal` 里有原因。

我只判了 content 就上线，用户偶尔收到一个空卡片。排查半天才发现要先看 refusal。

另一个是 null 联合类型的位置。字段标了 `"type": ["string", "null"]` 但它仍在 `required` 里，前端拿到的对象永远有这个 key，值可能是 null。

判空逻辑要写在每一处读取点。不能依赖「字段可能不存在」。
