首页 / 文章 / 结构化输出:让模型稳定返回 JSON

结构化输出:让模型稳定返回 JSON

对比提示词约束、json_object 与 json_schema 的差别,给出带校验和重试的完整代码,并整理 schema 设计里最常见的几个坑,让下游不再解析失败。

三种约束,强度不同

提示词里写"只返回 JSON"最弱,模型偶尔会加一句解释,或者包一层 markdown 代码块。response_format: {"type": "json_object"} 能保证返回合法 JSON,但不保证字段名和类型。json_schema 把字段、类型、枚举一起约束,是目前最稳的一档。

三者的兼容性不一样。不是每个模型、每条渠道都支持 json_schema,用之前看模型页的说明;不支持时退回 json_object 加提示词,再不行就在服务端做抽取和修补。

提示词约束的成本最低,也最容易被长上下文稀释:系统提示写到三千字以后,"只返回 JSON"这句话的权重会下降。schema 约束不依赖模型记性,代价是兼容性差一些。

json_schema 怎么写

python
import json
import requests
from jsonschema import Draft7Validator

BASE = "https://ai.idcidiot.com/v1"
KEY = "sk-..."

SCHEMA = {
    "type": "object",
    "properties": {
        "sentiment": {"type": "string", "enum": ["positive", "neutral", "negative"]},
        "score": {"type": "number", "minimum": 0, "maximum": 1},
        "keywords": {"type": "array", "items": {"type": "string"}, "maxItems": 5},
        "reason": {"type": ["string", "null"]},
    },
    "required": ["sentiment", "score", "keywords", "reason"],
    "additionalProperties": False,
}

def raw_ask(messages):
    r = requests.post(
        f"{BASE}/chat/completions",
        headers={"Authorization": f"Bearer {KEY}"},
        json={
            "model": "glm-5.3-flash",
            "messages": messages,
            "response_format": {
                "type": "json_schema",
                "json_schema": {"name": "review", "strict": True, "schema": SCHEMA},
            },
            "max_tokens": 500,
        },
        timeout=60,
    )
    r.raise_for_status()
    return r.json()["choices"][0]["message"]["content"]

response_format 放在请求体顶层,name 只是给这份 schema 起个名字,strict 打开后上游会按 schema 约束解码。有些实现要求每个 object 都写 additionalProperties: false,少一处就可能退回自由生成。模型不支持时接口通常会返回 400 或 422,捕获这个错误再降级到 json_object。

校验和重试

python
# 接上一段,复用 raw_ask / SCHEMA
def extract(text, tries=3):
    messages = [{"role": "user", "content": f"分析这条评论:{text}"}]
    for _ in range(tries):
        raw = raw_ask(messages)
        try:
            data = json.loads(raw)
            Draft7Validator(SCHEMA).validate(data)
            return data
        except Exception as e:
            reason = getattr(e, "message", str(e))
            messages += [
                {"role": "assistant", "content": raw},
                {"role": "user", "content": f"上面的输出不通过校验:{reason}。只返回修正后的 JSON,不要解释。"},
            ]
    raise ValueError("三次仍未通过校验")

重试时把模型的错误输出放回历史,再指出具体哪条规则没过,比重新问一遍有效。错误信息只保留第一条,全量错误会把上下文撑长,反而更容易跑偏。

重试的输入会翻倍,别把整段原文反复带上。定位到字段后,第二次只发原文和修正要求,历史里那条失败输出可以截断到前 500 字。

schema 设计的坑

  • 所有字段写进 required,可选字段用 ["string", "null"] 表达,strict 模式对可选字段的处理更严格。
  • 枚举写全,别让模型自由发挥。分类标签超过十个就该拆成两段式。
  • additionalProperties: false,否则模型会自作主张加字段。
  • 数字加 minimum / maximum,日期用 format 或正则约束,别指望模型记住格式。
  • 嵌套不超过三层,层级越深,漏字段的概率越高。

写完 schema 先自己构造五条极端输入跑一遍:空字符串、超长文本、纯表情、中英混排、带引号的 JSON 片段。模型在这些边界上的表现,比在正常样本上更能暴露问题。

流式与 JSON 冲突

流式返回的 JSON 是半截的,收尾的 chunk 到之前 json.loads 一定失败。三个选择:关掉流式、用增量 JSON 解析器、改成按行输出 NDJSON。做批量抽取时关流式最省事,用户盯着屏幕等字的时候再开。

需要流式又要结构,可以让模型按 NDJSON 一行一个对象输出,每收到一行就解析一次,解析失败的行丢掉重试。提示词里要写死"每行一个 JSON 对象,不要数组,不要 markdown 代码块",否则模型会自作主张包一层。

重试很便宜,脏数据很贵

glm-5.3-flash 在稳定国模分组输入 0.16、输出 0.56 美元每百万 token。1200 token 输入加 300 token 输出一次约 0.00036 美元,重试三次也就 0.001 美元出头。下游因为一个脏字段崩掉、人工补数据的成本,比这高得多。

模型清单和上下文长度见 模型总览,glm-5.3-flash 的参数在 模型页,价格以 定价页 为准,相关的错误处理可以接着看 文章列表。把 Draft7Validator 放在写库之前,脏数据就进不了表。

如果下游是数据库,再加一层类型转换:score 收到字符串 "0.8" 时先转成浮点数再入库,别让类型错误在更深的地方爆出来。

用 二五云API 跑通这些模型

OpenAI 兼容接口,一个 Key 调用 GPT、Claude、Gemini、DeepSeek、Kimi 等模型,按量计费。

免费注册查看模型价格

相关文章