← 研究笔记English
技术研究 · 2026-07-07 · 23 分钟阅读 · 精研

如何确保 LLM 返回的 JSON 严格符合预期格式

把 LLM 接进生产系统的人,迟早会在深夜遇到同一个报错:JSON.parse 炸了。模型昨天还乖乖返回结构化数据,今天突然多包了一层 markdown 围栏、少了一个闭括号,或者干脆用散文向你道歉。这篇文章系统梳理了这个问题的全部已知解法——各家厂商的原生能力(海外与国内)、约束解码的原理与边界、TypeScript 生态的应用层方案、修复策略与生产可靠性工程——最后给出一套可以直接抄的「分层防御」架构。

文中所有厂商能力结论,均核验自 2026-07-07 当日抓取的各厂商现行官方文档,并已剔除第三方 SEO 站的不实宣称。

01核心结论

02三档保证强度与约束解码原理

市面上所有做法按「保证从哪来」分成三档,强度差异是机制性的,不是程度性的。

档位机制保证内容典型形态
① Prompt 引导 把 schema/示例写进提示词,靠模型自觉 无保证。OpenAI 内部评测 gpt-4-0613 仅 <40% 合规 schema-in-prompt、few-shot、prefill
② JSON mode 约束输出为合法 JSON 语法,不看 schema 仅语法合法;截断时连这条也失效 json_object(DeepSeek/Qwen/GLM 仅有此档)
③ 约束解码 schema 编译为文法,逐 token 掩码非法 token 结构 100% 合规(截断/拒答除外);值的语义不保证 OpenAI/Anthropic/Gemini/方舟 strict、自托管 XGrammar 等

约束解码怎么工作

OpenAI 的公开描述:把提交的 JSON Schema 转换成上下文无关文法(CFG),推理时每一步用文法计算「当前位置合法的 token 集合」,把非法 token 的采样概率置 0。选 CFG 而不是正则/FSM 的原因是 FSM 无法表达递归结构(深层嵌套的括号匹配),CFG 可以支持 $ref: # 递归 schema。OpenAI 官方致谢了 outlines、jsonformer、instructor、guidance、lark 等开源项目作为灵感来源。

工程代价是首次编译延迟:新 schema 第一次请求要预处理(OpenAI 2024 年数字:典型 schema 10 秒内,复杂 schema 最长 1 分钟),之后缓存复用零额外延迟。Anthropic 的文法编译产物自最后使用起缓存 24 小时,schema 结构或请求内工具集一变缓存即失效——这意味着 schema 版本化要克制,频繁变更 schema 等于持续吃首编译延迟

关键数字

OpenAI 复杂 schema 遵循内部评测:纯 prompt(gpt-4-0613)<40% → 纯训练改进(gpt-4o-2024-08-06)93% → 同模型 + 约束解码 100%。OpenAI 自己的评价:93%「仍不满足开发者构建健壮应用所需的可靠性」。注意 100% 仅指结构合法,不含值正确性;该数字出自厂商发布博客,置信度标为 medium。

03厂商能力对比

全部结论基于 2026-07-07 实时抓取的厂商一手文档,每条经 3 票对抗验证。国内厂商的第三方站(SEO 中转页/API 分销商)宣称的能力一律以官方文档为准核实过。

厂商 / 接口schema 级强制可用模式关键限制与坑
OpenAI 支持 json_schema strict(2024-08 GA)+ 旧 json_object 仅支持 JSON Schema 子集(全字段 required、additionalProperties:false 等);strict 与并行函数调用有兼容性限制(建议 parallel_tool_calls:false);首请求编译延迟
Anthropic 支持(GA) output_config.format(JSON outputs)+ strict tool use(strict:true),可独立或组合用 不支持:递归 schema、min/max、minLength/maxLength、正则反向引用/前瞻;上限 20 个 strict 工具、24 个可选参数、16 个 anyOf,超限 400;SDK 会自动剥离不支持的约束改为客户端校验(意味着数值/长度约束并不受文法强制);文法缓存 24h
Google Gemini 支持 responseSchema(Gemini 3 系为 responseJsonSchema);支持流式结构化输出 不支持的 schema 关键字被静默忽略(对比其他家显式 400,是独有陷阱);复杂/深嵌套 schema 可能 400;官方明文要求应用侧校验值:「schema-compliant but semantically incorrect」被列为常态失败模式
火山方舟(豆包) 支持,beta json_schema + strict:true(官方定位为 json_object 的演进版并明确推荐)+ json_object 官方原话「受资源与平台负载影响,服务可用性可能随访问情况产生波动,请谨慎在生产环境使用」;不承诺 100% 正确;仅限文档列出的模型;Responses API 下参数名是 text.format;文档自己推荐用 Zod/Pydantic 做客户端校验
DeepSeek json_object(API 枚举只有 text / json_object) prompt 必须含 "json" 一词并给出格式示例;官方自曝已知问题:可能偶发返回空内容,缓解手段仅为改 prompt → 空内容检测 + 重试必须自备;变通:beta 的 strict tool calling 可强制函数入参 schema
阿里云百炼(通义) json_object(5 个官方页面 grep json_schema 零命中) System/User 消息里必须出现 "JSON" 关键词,否则直接 400(有生产事故佐证);官方生产指南点名 Ajv 校验 + 失败重试/换模型修复;开源 Qwen 权重在 vLLM 等宿主上可获文法级约束(宿主能力,非平台能力)
智谱 GLM(Z.AI / bigmodel) json_object(API 将 response_format 封闭枚举为两值) schema 须写进 system prompt;官方文档自己开出多层防御处方:「多层验证:Schema 验证 + 业务逻辑验证」「准备简化的备用 Schema」「详细记录错误信息」,官方示例同时 catch JSONDecodeError 和 ValidationError
值得玩味

只有 json_object 的三家国内厂商(DeepSeek/通义/智谱),官方文档反而写得最像「防御性工程指南」——等于厂商以文档形式承认:json_object 的输出不能直接信任。这也是为什么应用层防御无论对接哪家都不能省。

04strict 模式也逃不掉的残余失败

这是跨 OpenAI / Anthropic / Google / 方舟四家一致的结论,四家文档都给出了对应的检测手段——意味着这些分支在任何厂商下都必须写。

失败模式表现检测方法
max_tokens 截断 输出是不完整、违反 schema 的 JSON(连 JSON mode 的语法保证也一并失效) OpenAI:finish_reason === "length"(Responses API 为 status incomplete);Anthropic:stop_reason === "max_tokens"
安全拒答 拒答文本优先于 schema 约束;仍返回 200 且照常计费 OpenAI:独立的 refusal 字段;Anthropic:stop_reason === "refusal"
值级 / 语义错误 结构完全合法,但字段内容是错的(约束解码原理上无法防止) 业务规则校验(Zod refine)、下游一致性检查、evals
schema 不被支持 用了厂商子集之外的关键字:或显式 400(OpenAI/Anthropic/方舟),或被静默忽略(Gemini) 上线前对照厂商 schema 支持清单;Gemini 需专门测试约束是否真的生效
beta 可用性波动 方舟 json_schema 官方明示可用性随负载波动 保留 json_object 降级开关,灰度上线
一个流传很广但站不住的说法

「OpenAI Structured Outputs 保证输出符合 schema,因此不再需要校验和重试循环」——把它和四家厂商文档逐条对照就会发现:strict 只消灭了「schema 违规」这一类失败,截断、拒答、值级错误仍需应用层兜底,OpenAI 自己的文档甚至给出了 finish_reasonrefusal 的官方处理代码。任何「开了 strict 就可以裸 parse」的架构,都建立在一个厂商自己都不认的前提上。

05自托管约束解码

自己部署开源模型时,schema 强制能力来自推理框架而非模型。这条路能把 json_object-only 的开源权重(Qwen、GLM、DeepSeek 蒸馏系)升级成文法级强制。

自托管也不是零失败

基准数据两面都要看:重复性 schema(book-info)上,无约束最高只有 72% 结构正确率,开 XGrammar / LLGuidance 后都到 100%;但动态 schema 测试(github_easy)里 XGrammar 仍产出 2.21% 非法 JSON、LLGuidance 0.12%。选型经验:重复固定 schema → XGrammar(吃文法缓存红利);动态复杂 schema → LLGuidance。无论哪个,业务层解析校验兜底不能拆。

06TypeScript 应用层生态

四个代表性方案,各自代表一种思路:SDK 抽象、重试循环、容错解析、类型即 schema。

Vercel AI SDK —— SDK 抽象层

Instructor-JS —— 带错误反馈的重试循环

BAML(Schema-Aligned Parsing)—— 容错解析路线

TypeChat(微软)—— 类型即 schema 的先驱

用 TypeScript 类型定义充当 schema,校验失败后携带类型错误自动修复重试。工程上已被上面几家超越,但「以类型系统为契约 + 校验修复循环」的范式来自这里。

07解析与修复策略

在重新调用模型(贵、慢)之前,确定性修复(快、免费)应该先上。

08Prompt 层技巧

用了 strict 也别删这一层:它决定「值」的质量,而且是向 json_object-only 厂商迁移时的可移植性保障。

09生产可靠性工程

恢复阶梯(便宜的先上)

  1. 确定性修复:围栏剥离 → jsonrepair → 再 parse。零成本零延迟。
  2. 原样重试:相同请求 + 指数退避加抖动(参考实现:最多 3 次)。很多失败是瞬时的,第二次就好了。
  3. 纠错重试:把失败输出和精确的解析器/Zod 错误信息回填进下一次请求,重申 schema,让模型改错(Instructor 模式)。有限次数,通常 1-2 次。
  4. 降级:换简化版备用 schema(智谱官方处方)/ 换一个模型修复输出(阿里云官方处方)。
  5. 业务兜底:上一次的好结果(缓存)、默认内容、或功能静默隐藏——绝不把 500 甩给用户。

观测

参数与版本

10前沿与争论

11分层防御参考架构

每一层只兜住上一层明确漏掉的失败模式;去掉任何一层,该层对应的失败就直达用户。

L0 提示层
schema + 示例写进 prompt,含 "json" 关键词 字段顺序安排推理在前、结论在后;低温用于提取类任务。 兜住:值质量、跨厂商可移植性(DeepSeek/Qwen/GLM 只有这层可用)
L1 请求层
能开约束解码就开:json_schema + strict:true 对照厂商 schema 子集清单设计 schema;max_tokens 按最坏情况留余量;beta 能力配降级开关灰度。 兜住:结构违规(从「大概率对」变「机制保证」)
L2 门禁层
parse 前查 finish_reason / 空内容,parse 后必过 Zod safeParse 业务规则用 refine 挂在同一个 schema 上;partial 流式对象不校验、完整才校验。 兜住:截断、拒答、空内容、值级错误(strict 模式明确不管的全部)
L3 恢复层
修复 → 原样重试 → 带错误反馈重试 → 降级 → 业务兜底 确定性修复(jsonrepair)最先;纠错重试限 1-2 次;备好简化 schema 或备用模型;最终兜底是缓存/默认值,不是 500。 兜住:瞬时失败、顽固格式漂移、厂商可用性波动
L4 观测层
失败率 / 截断率 / 重试率 / 降级率,按端点 × schema 版本打点 + 告警 失败留存原始输出;schema 版本化;换模型先跑固定用例回归。横跨 L0–L3,是其余四层有效性的唯一证明。

最小实现骨架(Bun / TypeScript)

async function callStructured<T>(schema: z.ZodType<T>, body: ChatBody): Promise<T> {
  for (let attempt = 0; attempt <= MAX_RETRY; attempt++) {
    const res  = await chat(body);                          // L1: json_schema strict(方舟 beta,失败自动回落 json_object)
    const choice = res.choices?.[0];
    if (choice?.finish_reason === "length") { metric("llm_truncated"); continue; }   // L2: 截断门禁
    const raw = choice?.message?.content ?? "";
    if (!raw.trim()) { metric("llm_empty"); continue; }                              // L2: 空内容(DeepSeek 已知问题)

    let parsed: unknown;
    try { parsed = JSON.parse(stripFences(raw)); }
    catch { try { parsed = JSON.parse(jsonrepair(raw)); }                            // L3: 确定性修复
            catch { metric("llm_unparseable", { raw }); continue; } }

    const v = schema.safeParse(parsed);                                              // L2: schema + refine 业务规则
    if (v.success) return v.data;
    body = withErrorFeedback(body, raw, v.error);                                    // L3: 纠错重试(Instructor 模式)
    metric("llm_schema_fail", { issues: v.error.issues });
  }
  throw new StructuredOutputError();                                                // 调用方接 L3 兜底:缓存 last-good / 降级隐藏
}

12主要来源

所有厂商能力结论均于 2026-07-07 对官方文档实时抓取验证;第三方宣称(如「DeepSeek/GLM 已支持 json_schema」的 SEO 站)已核实为不实或非官方能力。