芥末
发布于 2026-04-26 / 0 阅读
0
0

用 JSON Schema 让大模型稳定输出结构化数据

让大模型输出 JSON(JavaScript Object Notation)看起来很简单,只要在提示词里加一句:

请严格输出 JSON,不要输出任何解释,不要输出 Markdown。

手动测试时,这种写法经常有效。模型会返回一段看起来很规整的 JSON,复制出来也能解析。

问题会在接入真实系统后暴露出来。比如批量抽取简历、生成日报、整理会议纪要、把用户输入转换成数据库字段时,模型可能出现这些情况:

  • JSON 前面多一句“好的,以下是结果”;
  • 字段名从 action_items 变成 actions
  • 本来应该是数组的字段变成了字符串;
  • 多输出了后端不认识的字段;
  • 少了必填字段;
  • JSON 语法合法,但结构不符合业务要求。

根本问题不在于提示词写得不够“严格”,而在于提示词只是提醒,不能成为工程约束。要让模型稳定进入软件系统,需要把输出格式交给 Schema,而不是交给一句自然语言要求。

稳定格式不等于稳定内容

结构化输出解决的是格式可靠性,不是内容绝对正确。

假设用户反馈是:

我已经等了三天了,订单状态一直没有更新。客服每次都说帮我催一下,但没有任何结果。
如果今天还不处理,我就要退款了。

期望得到类似结构:

{
  "sentiment": "negative",
  "reason": "物流太慢",
  "priority": "high"
}

真正需要稳定的是这些东西:

约束点需要避免的问题
字段名固定sentiment 不能变成 emotion
字段类型固定数组不能变字符串,对象不能变列表
枚举值固定priority 只能是 lowmediumhigh
必填字段不能丢后端依赖的字段必须存在
不输出额外文本JSON 外不能包 Markdown 或解释说明
不输出多余字段后端不认识的字段不要混进来

reason 里面写“物流慢”还是“订单处理时间过长”,仍然可能不同。结构化输出保证的是“像一份合格表单”,不是保证每个判断都百分百正确。

所以在工程上要拆成两层:

层级负责什么常用手段
格式层JSON 是否可解析、字段是否存在、类型是否正确、枚举是否合法JSON Schema、Structured Outputs、Function Calling、validator
业务层判断是否准确、事实是否可靠、是否满足业务规则规则校验、数据库约束、重试、人工审核、失败队列

不要指望一个提示词同时负责格式约束和业务正确性。

为什么只靠提示词不稳定

大模型生成文本时,本质上是在不断预测下一个 token。你写“只输出 JSON”,模型会尽量遵守,但它并不会天然理解后端马上要执行 json.loads(),也不会因为提示词里出现“严格”两个字就变成数据库接口。

提示词更像口头要求:

这份表格一定按模板填。

Schema 更像受控表单:

只能填这些字段,每个字段只能填允许的值。

前者靠模型配合,后者靠机制约束。

提示词和 Schema 的差别可以用这张图概括:

提示词 vs Schema 对比图

图里的关键区别是:提示词只能影响模型“倾向于”怎么回答,而 Schema 会参与输出结构的定义。接入 API(Application Programming Interface,应用程序编程接口)后,系统可以根据 Schema 限制模型的输出空间,并在生成完成后得到可校验的数据。

在一些结构化输出实现里,还会用到 constrained decoding / constrained sampling(约束解码 / 约束采样)。它的思路是:生成过程中并不是所有 token 都能被选择,只有符合当前 Schema 状态的 token 才是合法候选。

flowchart LR
    A[JSON Schema] --> C[生成状态]
    B[已生成的 JSON 前缀] --> C
    C --> D[计算合法的下一个 token]
    D --> E[模型只在合法集合中选择]
    E --> F[追加 token]
    F --> C
    F --> G[完成符合 Schema 的 JSON]

这类机制把“请你按格式来”变成了“你只能在合法格式里选择”。

JSON Schema 是什么

JSON Schema 可以理解成 JSON 数据的结构说明书。它用声明式方式规定:

  • 顶层是对象还是数组;
  • 对象里有哪些字段;
  • 哪些字段必填;
  • 每个字段是什么类型;
  • 字段是否只能从枚举值中选择;
  • 是否允许额外字段;
  • 字符串长度、数组长度、数字范围等限制。

例如,要把用户反馈转换成固定结构,可以定义这样的 Schema:

{
  "type": "object",
  "properties": {
    "sentiment": {
      "type": "string",
      "enum": ["positive", "neutral", "negative"]
    },
    "summary": {
      "type": "string"
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high"]
    },
    "action_items": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["sentiment", "summary", "priority", "action_items"],
  "additionalProperties": false
}

这份 Schema 表达了几条明确规则:

  • sentiment 只能是 positiveneutralnegative 三选一;
  • summary 必须是字符串;
  • priority 只能是 lowmediumhigh
  • action_items 必须是字符串数组;
  • 四个字段都必须存在;
  • additionalProperties: false 表示不允许模型额外添加字段。

additionalProperties: false 很关键。很多结构化输出事故不是字段少了,而是模型“主动帮忙”多加了字段。后端如果没有处理这些额外字段,就可能出现解析失败、数据污染或业务逻辑异常。

Structured Outputs 和 JSON mode 的区别

很多模型 API 都有 JSON mode(JSON 模式),但 JSON mode 和 Structured Outputs(结构化输出)不是一回事。

能力保证合法 JSON保证符合指定字段结构保证枚举值合法控制额外字段适合场景
普通提示词不稳定不稳定不稳定不稳定手动试验
JSON mode不一定不一定不一定只要求返回 JSON 文本
Structured Outputs可控制接入业务系统、自动化流程
Function Calling / Tool Calling可控制让模型调用函数或工具

JSON mode 主要解决“返回内容是不是合法 JSON”。Structured Outputs 进一步解决“返回 JSON 是否符合你定义的 Schema”。

如果后续系统依赖字段名、字段类型、枚举值和必填字段,就不要只停留在 JSON mode。

用 Pydantic 定义结构化输出

在 Python 里,可以用 Pydantic 定义数据结构,再让支持结构化输出的模型 API 返回解析后的对象。

安装依赖:

pip install openai pydantic

定义一个用户反馈分析器:

from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, Field

import json


client = OpenAI()


class FeedbackAnalysis(BaseModel):
    sentiment: Literal["positive", "neutral", "negative"] = Field(
        description="用户反馈的整体情绪"
    )
    summary: str = Field(
        description="用一句话概括用户反馈"
    )
    priority: Literal["low", "medium", "high"] = Field(
        description="处理优先级"
    )
    action_items: list[str] = Field(
        description="建议后续处理动作"
    )


raw_feedback = """
我已经等了三天了,订单状态一直没有更新。
客服每次都说帮我催一下,但没有任何结果。
如果今天还不处理,我就要退款了。
"""

response = client.responses.parse(
    model="gpt-4o-2024-08-06",
    input=[
        {
            "role": "system",
            "content": (
                "你是一个客服反馈分析助手。"
                "请把用户反馈转成结构化数据,不要扩展不存在的信息。"
            )
        },
        {
            "role": "user",
            "content": raw_feedback
        }
    ],
    text_format=FeedbackAnalysis,
)

result = response.output_parsed

print(json.dumps(result.model_dump(), ensure_ascii=False, indent=2))

可能得到这样的结果:

{
  "sentiment": "negative",
  "summary": "用户因订单状态长期未更新且客服处理无结果而感到不满,并提出退款威胁。",
  "priority": "high",
  "action_items": [
    "查询订单当前状态",
    "升级给人工客服或物流专员处理",
    "主动向用户反馈明确处理时间",
    "必要时提供退款或补偿方案"
  ]
}

这段代码里最重要的不是 system prompt,而是这个类型定义:

class FeedbackAnalysis(BaseModel):
    sentiment: Literal["positive", "neutral", "negative"]
    summary: str
    priority: Literal["low", "medium", "high"]
    action_items: list[str]

它把模型的输出从“随便写一段回答”变成了“填写一个固定对象”。字段叫什么、类型是什么、哪些值能选,都在代码里定义清楚了。

不能使用 Structured Outputs 时,至少要校验

有些系统可能还在用旧接口、本地模型或开源模型,暂时没有 Structured Outputs 能力。这种情况下,仍然不应该让模型输出直接进入业务系统。

最低限度要做三件事:

  1. 让模型尽量输出 JSON;
  2. 用代码解析 JSON;
  3. 用 JSON Schema 校验结构。

Python 可以使用 jsonschema

pip install jsonschema

校验脚本如下:

import json

from jsonschema import ValidationError, validate


FEEDBACK_SCHEMA = {
    "type": "object",
    "properties": {
        "sentiment": {
            "type": "string",
            "enum": ["positive", "neutral", "negative"]
        },
        "summary": {
            "type": "string"
        },
        "priority": {
            "type": "string",
            "enum": ["low", "medium", "high"]
        },
        "action_items": {
            "type": "array",
            "items": {"type": "string"}
        }
    },
    "required": ["sentiment", "summary", "priority", "action_items"],
    "additionalProperties": False
}


def parse_and_validate_json(raw_text: str) -> dict:
    """
    解析并校验模型返回的 JSON。
    不合格就抛错,避免脏数据进入后续系统。
    """
    try:
        data = json.loads(raw_text)
    except json.JSONDecodeError as e:
        raise ValueError(f"模型输出不是合法 JSON: {e}") from e

    try:
        validate(instance=data, schema=FEEDBACK_SCHEMA)
    except ValidationError as e:
        raise ValueError(f"模型输出不符合 Schema: {e.message}") from e

    return data


if __name__ == "__main__":
    model_output = """
    {
      "sentiment": "negative",
      "summary": "用户对订单处理延迟不满",
      "priority": "high",
      "action_items": ["查询订单", "联系用户", "升级处理"]
    }
    """

    result = parse_and_validate_json(model_output)
    print(result)

这层校验解决的是一个生产系统里非常现实的问题:模型 99 次输出正确,不代表第 100 次不会把数组写成字符串,或者多塞一个后端不认识的字段。

结构化输出流程可以拆成生成、解析、校验、重试、入库几个环节:

结构化输出流程图

图里的重点是:模型输出不是流程终点。只有通过解析和校验的数据,才应该进入数据库、表格、任务系统或业务 API。失败的输出可以进入重试逻辑,多次失败后再放入人工处理或失败队列。

加上重试机制

生产环境不能默认“一次生成一定成功”。模型是一个可能犯错的组件,组件可能犯错,就需要错误处理。

常见流程是:

flowchart LR
    A[输入文本] --> B[调用模型生成 JSON]
    B --> C{解析 JSON}
    C -- 失败 --> F[带错误信息重试]
    C -- 成功 --> D{Schema 校验}
    D -- 失败 --> F
    D -- 成功 --> E[进入业务系统]
    F --> G{超过最大重试次数?}
    G -- 否 --> B
    G -- 是 --> H[失败队列或人工处理]

可以写成伪代码:

import json


def ask_model(prompt: str) -> str:
    """
    替换成实际的大模型调用。
    返回模型原始文本。
    """
    raise NotImplementedError


def generate_valid_json(base_prompt: str, max_retries: int = 3) -> dict:
    prompt = base_prompt

    for attempt in range(1, max_retries + 1):
        raw_output = ask_model(prompt)

        try:
            return parse_and_validate_json(raw_output)
        except ValueError as e:
            prompt = f"""
你上一次输出的 JSON 不合格。

错误信息:
{str(e)}

请只修复 JSON,不要解释,不要输出 Markdown。
必须符合这个 Schema:
{json.dumps(FEEDBACK_SCHEMA, ensure_ascii=False, indent=2)}

原始输出:
{raw_output}
"""

    raise RuntimeError("多次重试后,模型仍然没有输出合格 JSON")

重试提示词里要带上具体错误信息。比如“priority 不在枚举值中”比“格式错了”更容易让模型修复。

不过重试次数不能无限增加。超过 2 到 3 次仍然失败,通常说明输入太复杂、Schema 设计不合理,或者任务需要拆分。此时应该进入失败队列,而不是继续消耗模型调用成本。

Structured Outputs 和 Function Calling 怎么选

Structured Outputs 适合让模型返回一个固定结构的结果。Function Calling(函数调用)或 Tool Calling(工具调用)适合让模型决定是否执行某个动作。

需求推荐方式例子
抽取结构化信息Structured Outputs从简历中抽取姓名、技能、工作年限
生成固定 JSONStructured Outputs把会议纪要整理成待办事项数组
分类与打标签Structured Outputs判断用户反馈情绪和优先级
查询外部数据Function Calling / Tool Calling调用搜索接口、查数据库
执行业务动作Function Calling / Tool Calling创建订单、发起退款、写入 CRM(客户关系管理)系统
多步骤 Agent 工作流Function Calling / Tool Calling + Structured Outputs先查资料,再生成结构化报告

判断标准很简单:

  • 要模型“返回一个结果”,用 Structured Outputs;
  • 要模型“调用你的系统做动作”,用 Function Calling / Tool Calling。

例如,把会议纪要整理成待办事项,这是结构化结果,适合 Structured Outputs。

但如果整理完之后还要自动创建飞书任务、Notion 页面或 Jira issue,那就涉及外部系统调用,应该把创建动作设计成工具函数,由模型通过 Function Calling / Tool Calling 发起。

Prompt 仍然重要,但不负责格式兜底

有了 Schema,不代表提示词没用了。Prompt 负责业务语义,Schema 负责输出结构。

Prompt 应该说明:

  • 任务目标是什么;
  • 哪些信息可以推断,哪些不能编造;
  • 信息缺失时怎么处理;
  • 摘要字段要多长;
  • 分类标准是什么;
  • 动作建议要具体到什么程度。

一个相对干净的 system prompt 可以这样写:

你是一个结构化信息抽取助手。

任务:
从用户提供的文本中抽取信息,并填入指定结构。

规则:
1. 不要编造文本中没有的信息。
2. 如果信息缺失,请使用空字符串、空数组或 Schema 中允许的默认值。
3. 分类字段必须严格按照 Schema 中的枚举值选择。
4. summary 字段控制在 50 字以内。
5. action_items 只写可执行动作,不写泛泛建议。

这个 prompt 没有把重点放在“请输出 JSON”。JSON 格式已经由 Schema 处理,prompt 只描述业务规则。这样的分工更清晰,也更适合维护。

设计 JSON 输出结构时的 7 条规则

1. 字段名要具体

不要写这种含糊字段:

{
  "data": "...",
  "result": "...",
  "info": "..."
}

后端不知道字段含义,模型也容易填得很散。

更好的写法是:

{
  "user_intent": "...",
  "risk_level": "...",
  "next_actions": []
}

字段名越贴近业务含义,模型越容易稳定填充,后端代码也更容易维护。

2. 能用枚举就用枚举

不要让模型自由发挥优先级:

{
  "priority": "比较着急"
}

应该限制成固定值:

{
  "priority": "high"
}

同时要在字段描述里写清楚判断标准,例如:

  • high:用户明确投诉、退款、流失风险或影响交易;
  • medium:需要人工跟进,但没有紧急风险;
  • low:普通咨询、低风险反馈。

3. 尽量禁止额外字段

如果后端不需要扩展字段,就设置:

{
  "additionalProperties": false
}

这样可以避免模型输出类似 confidenceexplanationextra_notes 之类后端不认识的字段。

4. 复杂任务拆成多个小结构

不要让一个 JSON 同时承载用户画像、风险判断、营销建议、执行计划、邮件正文、SQL 条件。

结构越大,失败概率越高。可以拆成多步:

flowchart LR
    A[用户输入] --> B[抽取事实]
    B --> C[风险分类]
    C --> D[生成处理动作]
    D --> E[生成对外回复]

每一步只输出一个小 JSON,校验通过后再进入下一步。

5. 字段描述要写清楚

Schema 不只限制类型,也可以承载字段说明。尤其是分类字段,描述越明确,模型越不容易乱选。

例如:

priority: Literal["low", "medium", "high"] = Field(
    description=(
        "处理优先级。high 表示用户明确投诉、退款、流失风险或订单阻塞;"
        "medium 表示需要人工跟进但没有紧急风险;"
        "low 表示普通咨询或低风险反馈。"
    )
)

6. 不要混合 Markdown 和 JSON

这样的输出对人友好,对程序不友好:

## 分析结果

```json
{
  "summary": "用户对物流延迟不满"
}
```

接入系统时,最好只处理纯 JSON 或解析后的结构化对象。展示层需要 Markdown,可以在数据进入系统后再生成。

7. 永远保留校验层

即使用了 Structured Outputs,也建议在关键业务入口再做一层校验。Schema 能保证字段类型和结构,但不能保证业务事实一定准确。

例如:

  • 模型可能把普通咨询误判为高风险投诉;
  • 模型可能生成不符合内部政策的处理动作;
  • 模型可能从模糊文本里过度推断用户意图。

业务校验可以包括关键词规则、数据库状态检查、权限判断、人工审核阈值等。

把模型输出当成接口设计

大模型一旦接入业务系统,它就不再只是聊天窗口,而是一个上游组件。组件之间传递的数据必须稳定、可解析、可校验。

可靠的结构化输出通常由几部分组成:

flowchart LR
    A[业务输入] --> B[Prompt 描述任务]
    B --> C[Schema 定义结构]
    C --> D[模型生成]
    D --> E[解析与 Schema 校验]
    E --> F[业务规则校验]
    F --> G{是否通过}
    G -- 是 --> H[写入系统或触发后续流程]
    G -- 否 --> I[重试、失败队列或人工审核]

这套分工可以概括为:

  • 用 Prompt 说明任务;
  • 用 Schema 定义结构;
  • 用 API 约束输出;
  • 用代码解析和校验;
  • 用重试处理偶发错误;
  • 用业务规则兜住内容质量。

稳定性不是来自模型“更听话”,而是来自工程约束。只要输出还是自由文本,模型更像聊天助手;当输出变成稳定 JSON,并且通过 Schema 和校验层进入系统,它才真正成为可以被后端、数据库、任务流和自动化平台消费的组件。


评论