Structured Output 落地:JSON Schema、重试与前端校验

一周备稿 · 2026-08-10

上篇:w2-sse-vs-websocket · 下篇预告:w2-ai-sdk-tool-ui

让 LLM「输出 JSON」听起来简单,上线后常见画面是:

  • 少了一个逗号,整段解析失败
  • 模型自作主张加了 markdown 代码块
  • 字段类型对了,枚举值却乱写

结论:Structured Output = 模型侧约束(Schema) + 解析失败重试 + 前端 Zod/JSON Schema 二次校验。缺一环就靠运气。

你将学到

  • JSON Schema 在 OpenAI / AI SDK 里的两种接法
  • 重试策略:何时重 prompt、何时降温度
  • 前端校验与友好错误展示
  • 流式场景下「边收边 parse」的坑
  • 和 Tool Calling 的边界

一、先定边界:什么时候要 Structured Output

适合

  • 表单自动填充、票据抽取、路由分类
  • 生成式 UI(JSON → React 组件树)
  • 需要 稳定字段 落库或驱动下游 API

不适合硬上 Schema

情况 更合适的做法
开放式长文创作 纯文本流
字段经常变 先 stabil 产品协议再锁 Schema
复杂嵌套 + 强推理 拆成多步,别一个巨型 Schema

二、Schema 设计:少即是多

反例:一次要 30 个字段

模型会漏字段、填 null、或 hallucinate 枚举。

正例:分层 Schema

json 复制代码
{
  "type": "object",
  "properties": {
    "intent": { "type": "string", "enum": ["faq", "ticket", "chitchat"] },
    "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
    "slots": {
      "type": "object",
      "properties": {
        "order_id": { "type": "string", "pattern": "^[A-Z0-9]{8,16}$" }
      },
      "additionalProperties": false
    }
  },
  "required": ["intent", "confidence"],
  "additionalProperties": false
}

原则:

  • required 只放 真正必需 的字段
  • enum / pattern 收窄空间
  • additionalProperties: false 防模型加戏
  • 大对象拆 两步:先分类,再抽取

三、模型侧:AI SDK + Zod 示例

ts 复制代码
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

const RouteSchema = z.object({
  intent: z.enum(["faq", "ticket", "chitchat"]),
  confidence: z.number().min(0).max(1),
  reply_hint: z.string().optional(),
});

export async function routeUserMessage(text: string) {
  const { object } = await generateObject({
    model: openai("gpt-4o-mini"),
    schema: RouteSchema,
    prompt: `Classify the user message:\n${text}`,
    temperature: 0,
  });
  return object;
}

generateObject模型原生 structured output(支持时),比「请输出 JSON」可靠一个数量级。

OpenAI 风格(概念对齐)

  • response_format: { type: "json_schema", json_schema: {...} }
  • 与 AI SDK 的 schema: zodSchema 同源思想:约束在 API 层,不在 prompt 里赌人品

四、重试策略:解析失败怎么办

ts 复制代码
async function withStructuredRetry<T>(
  run: () => Promise<T>,
  max = 3
): Promise<T> {
  let lastErr: unknown;
  for (let i = 0; i < max; i++) {
    try {
      return await run();
    } catch (e) {
      lastErr = e;
      if (i === max - 1) break;
      await new Promise((r) => setTimeout(r, 300 * (i + 1)));
    }
  }
  throw lastErr;
}

分层重试

失败类型 动作
JSON 语法错 同 Schema 重试,temperature=0
枚举越界 重试 + prompt 强调 allowed values
缺 required 缩小 Schema 或拆步
连续 3 次失败 降级为纯文本 + 人工队列

不要无限重试:成本和安全都会炸。

五、前端校验:不信模型,信 Schema

ts 复制代码
import { z } from "zod";

const UiBlockSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("text"), content: z.string() }),
  z.object({
    type: z.literal("button"),
    label: z.string().max(40),
    action: z.enum(["submit", "cancel"]),
  }),
]);

export function safeParseBlocks(raw: unknown) {
  const arr = z.array(UiBlockSchema).safeParse(raw);
  if (!arr.success) {
    return { ok: false as const, error: arr.error.flatten() };
  }
  return { ok: true as const, data: arr.data };
}

前端必做:

  1. safeParse ,禁止直接 JSON.parse 后当 truth
  2. 校验失败展示「结构异常,请重试」,别把脏数据渲染进 DOM
  3. action 类字段做 白名单,防 prompt injection 借 JSON 发指令

六、流式 Structured Output 的坑

streamObject 可以边收边展示,但要注意:

  • 部分 JSON 未闭合时,预览 UI 只能显示 skeleton,别假 parse
  • 取消生成后 丢弃半成品,别 commit 到 store
  • 大 Schema 流式首字延迟可能更高,要 loading 态
ts 复制代码
import { streamObject } from "ai";

const result = streamObject({ model, schema: RouteSchema, prompt });
for await (const partial of result.partialObjectStream) {
  // partial 可能缺字段 --- UI 用 optional chaining
  updatePreview(partial);
}

七、和 Tool Calling 怎么分

需求 选型
模型要 调用外部 API Tool Calling
模型要 产出固定结构给前端/DB Structured Output
既要调工具又要结构化终态 Tool 执行 + 最后一步 generateObject

别用 Structured Output 模拟 HTTP 调用;别用 Tool 返回一大坨 JSON 给 UI 渲染。

八、成本与安全

  • Schema 越大,completion token 往往越高
  • 敏感字段(身份证、手机号)在 Schema 层 不要 required,用后置 NER + 脱敏
  • 日志里 全量 JSON 可能含 PII,采样 + 掩码

踩坑清单

  1. Prompt 里写「返回 JSON」却无 Schema API:格式稳定靠祈祷
  2. 前端不校验:XSS / 非法 action 直进 UI
  3. 巨型嵌套 Schema 一步完成:漏字段率指数上升
  4. 重试不加 backoff:429 雪崩
  5. 流式半成品当最终态入库:脏数据进库难洗

小结

Structured Output 落地三板斧:

  1. 小 Schema + enum/pattern 收窄输出空间
  2. generateObject / 原生 json_schema + 有限重试
  3. 前端 Zod 二次校验 + 失败降级

模型再强也会格式翻车;工程化的答案是 约束 + 校验 + 重试 ,不是更长的 prompt。

下一篇:w2-ai-sdk-tool-ui --- Tool 进度态、错误态与取消。


相关阅读ai-fe-05-generative-ui.md · w2-ai-sdk-tool-ui

相关推荐
子非鱼a2 小时前
【WEB】multiSQL
前端
lemon_sjdk3 小时前
DOM 节点
java·前端·javascript
nvd113 小时前
深入现代 Web 鉴权架构:网关统一代理 (Forward-Auth) vs 前端持有 JWT 的终极选型与边缘同域实践
前端·架构·状态模式
luckystar513~3 小时前
Geo + AI:【时空智能体】技术剖析
人工智能·ai·gis·geoai·空间智能体·时空智能体
leoZ2313 小时前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端
YWL3 小时前
OpenLayers弹窗Overlay深度实战
前端·vue·openlayers
程xu袁3 小时前
不会编曲只用微信小程序写中文歌,选「AI音乐生成」、MELO音乐、魔兔音乐还是AI写歌?
ai·suno·ai音乐·ai音乐生成·ai写歌·ai创作歌曲
dozenyaoyida4 小时前
AI与大模型新闻日报 | 2026-09-09
人工智能·ai·chatgpt·大模型·新闻
YWL4 小时前
OpenLayers+ECharts联动:地图点击联动图表,数据可视化大屏方案
前端·信息可视化·vue·echarts·openlayers
芭拉拉小魔仙4 小时前
Vue 2 门诊收费系统中的医保结算流程设计与实践
前端·javascript·vue.js