#AI#TypeScript

让 LLM 稳定输出结构化数据:JSON、Schema 与容错

"让模型吐 JSON,是 AI 应用最常见的工程需求,也是最常见的翻车点——多一个字段、少一个逗号、混进一句解释,解析就崩。本文按可靠性递进讲四档方案:prompt 约束、JSON mode、Function Calling、受限解码(grammar),再给一套解析+容错的兜底,让结构化输出从'大多数时候行'变'生产稳'。"

本文要点

  • 主题聚焦:AI、TypeScript
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年4月13日·更新于 2026年7月23日

只要你的 AI 应用要把模型输出接进下游代码(基本上都有这个需求),你就逃不开"让模型吐结构化数据"。而只要你做过,就一定被它坑过:要求 JSON,它给你 JSON 外面裹一段"好的,结果是:";要求字段 A,它给你字段 B;偶尔心情好还送你一段 Markdown 解释。

这一篇按可靠性递进讲四档方案,你按场景选到够用的那一档就行。

一、为什么"在 prompt 里说'只输出 JSON'"不够

最朴素的方案:

text
请把以下内容提取为 JSON,只输出 JSON,不要任何其他文字。
{name, age, email}

它在 demo 里 90% 能成,但在生产里那 10% 的失败就是事故——因为这是靠模型自觉,没有任何强制。模型偶尔会:

  • 多输出一句解释。
  • 用单引号或尾随逗号(非法 JSON)。
  • 编一个 schema 里没有的字段。
  • 输出到一半截断。

靠 prompt 约束的 JSON,不能用于关键路径。 它是最低档,只适合原型或可容忍失败的场景。

二、第二档:JSON Mode / Response Format

主流厂商 API 现在都提供"JSON 模式"(OpenAI response_format: {type:"json_object"}、Gemini 的 responseMimeType、Anthropic 没有原生 JSON mode 但可用工具间接实现)。

它保证输出是合法 JSON(语法层面不会错),但不保证 schema——字段名、类型、嵌套结构仍可能偏离你的期望。也就是说:"是个 JSON"被保证了,"是你想要的那个 JSON"没有被保证。

适用:schema 简单、容忍字段出入的场景。比纯 prompt 强一档,但别指望它保证结构。

三、第三档:Function Calling / Tool Use(推荐主力)

这是目前生产里最实用的结构化输出方式。你定义一个"工具",它的参数 schema 就是你想要的结构:

ts
const tools = [{
  type: "function",
  function: {
    name: "extract_person",
    description: "Extract a person's info from the text.",
    parameters: {
      type: "object",
      required: ["name", "email"],
      properties: {
        name:  { type: "string" },
        age:   { type: "number" },
        email: { type: "string", format: "email" },
        role:  { type: "string", enum: ["user", "admin"] },
      },
    },
  },
}];

模型被训练成"调用工具时填参数",而填参数天然是结构化的——厂商会在这一路做约束,字段名、类型、enum 基本对得上。你拿到的 tool_call.arguments 就是符合 schema 的 JSON。

为什么它比 JSON mode 强?因为:

  • schema 显式约束(required、enum、type 都生效)。
  • 模型天然理解"填表"这件事,比"吐 JSON"更稳。
  • 厂商专门对这一路做了强化训练。

适用:绝大多数生产场景。哪怕你并不真的要"调工具",也可以借 function calling 拿到结构化输出——这是业界通行技巧。

四、第四档:受限解码(Grammar / Outlines)

如果你要接近 100% 的 schema 保证(比如字段必须 enum、数字范围、嵌套结构),用受限解码:在解码时用语法强制,模型只能在合法 token 里走。

代表项目:Outlines、lm-format-enforcer、各大推理框架的 grammar 约束(GBNF)。它们的原理是:你给一个 schema/语法,解码器在每一步屏蔽掉会让输出非法的 token,从根上保证输出符合结构。

python
# Outlines 示例(本地模型)
from outlines import models, generate
model = models.transformers("mistral-7b")
schema = '{"name": str, "age": int, "email": str}'
generator = generate.json(model, schema)
result = generator("Extract: ...")   # 100% 符合 schema

代价:只对自托管/开源模型可行(闭源 API 不开放解码层)。所以这一档适合你自建模型 + 对结构正确性要求极高的场景。

五、解析与容错:不管哪一档都要有

无论你选哪档,下游解析都要做容错——因为现实里总会遇到意外:

ts
function safeParse(raw: string): Record<string, unknown> {
  // 1. 直接解析
  try { return JSON.parse(raw); } catch {}

  // 2. 提取首个 {...} 或 [...] 片段(剥掉解释文字)
  const m = raw.match(/[{[][\s\S]*[}\]]/);
  if (m) { try { return JSON.parse(m[0]); } catch {} }

  // 3. 修复常见问题(尾随逗号、单引号)后重试
  try { return JSON.parse(m[0].replace(/,\s*([}\]])/g, "$1").replace(/'/g, '"')); } catch {}

  throw new Error("unparseable model output");
}

加上校验(zod / ajv):解析出来不等于符合 schema,用 schema validator 验一遍,不通过就带着错误信息重试(把"你输出的字段缺了 email"回灌给模型让它改)。

这套"解析 → 提取 → 修复 → 校验 → 带错误重试"的兜底链,是把任何一档方案推向"生产稳"的必备。别假设模型永远输出正确,假设它偶尔会错,并让代码能恢复。

六、按场景选档

场景推荐档位
原型 / 容忍失败第一档(prompt)
schema 简单、不关键第二档(JSON mode)
生产主力第三档(function calling)
自托管模型 + 必须零错误第四档(受限解码)
任何档位,下游必加解析+校验+重试

小结

让 LLM 输出结构化数据,可靠性是从"靠嘴说"到"解码层强制"逐档提升的:

  • prompt 约束:最弱,原型用。
  • JSON mode:保证语法,不保证结构。
  • Function calling:生产主力,schema 基本对得上。
  • 受限解码:接近 100% 保证,但要自托管模型。

无论选哪档,下游都必备解析 → 修复 → 校验 → 带错误重试的容错链。生产系统里,"模型偶尔输出错"是必然事件,工程的责任是让它即便错了也能恢复——这才是"结构化输出"从 demo 走向生产的分水岭。

Related

相关推荐