模型返回非法 JSON——因为 schema 是描述、不是强制

你让它按 schema 返 JSON。95% 合法,3% 在 JSON 外面包了散文,1% 漏字段,1% 类型错。描述 vs 强制,在 API 层修。

你 prompt 里写 “respond in JSON matching this schema: {name: string, age: number, tags: string[]}”。95% 的调用返回合法 JSON。3% 返回 “Sure, here’s the JSON you requested: json\n{...}\n”。1% 漏 tags 字段。1% 返回 age: "thirty"——因为 schema 没强制类型。production 里这意味着你的 JSON parser 5% 失败、下游代码崩、到处加 try-catch。模型没违抗——它跟的是 schema 的英文描述,不是一份被强制的契约。

现代模型 API 都支持真正的 schema 强制(OpenAI structured outputs、Anthropic tool use、Gemini response schema)。还把 schema 写在 prompt 里然后祈祷的话,你在白白浪费免费的可靠性。

常见原因

1. schema 是英文描述、不是声明

Return JSON: {name, age, tags}.

模型把这当软引导。有时加 wrapper 文字、有时漏字段。没有 API 层强制就是不可靠。

怎么判断:prompt 里 schema 是自然语言描述、API 调用没传 response_format=tools=

2. markdown code-block 包裹

模型返回:

Here is the JSON:
```json
\{"name": "Alice"\}
```

parser 读整个字符串,JSON.parse() 失败。即使设了 response_format={"type":"json_object"},跟其他 instruction 冲突时模型仍可能加 prose。

怎么判断:输出里有反引号、或前缀 “Here is” / “Sure” / “Of course”。

3. schema 指定了字段没指定类型

prompt 写 {age: number}、模型返 "age": "30"。描述本身允许歧义,模型以为 “30” 是个 number-shaped 字符串。

怎么判断:用严格 JSON Schema validator 校验。类型错就是 schema 没强制。

4. 可选字段被当必填

schema 是 {name, email, phone}。用户输入只有 name。模型返 {"name": "Alice", "email": null, "phone": null} 或干脆省略。下游期望字符串拿到 null 就崩。

怎么判断:访问 null 字段崩、或漏字段时 KeyError

5. 嵌套对象被拍平或展开

schema 是 {user: {name, age}}。模型有时直接返 {name, age}、有时展开成 {user_name, user_age}。嵌套丢了。

怎么判断:顶层 keys 跟你声明的不一致。

6. 对象数组塌成逗号分隔字符串

schema 是 tags: string[]。模型返 "tags": "blue, red, fast"。字符串、不是数组。输入里有逗号分隔值时常发。

怎么判断:按 schema 类型校验每个字段,发现是 string 不是 array。

7. enum 字段不被遵守

schema 是 sentiment: "positive" | "neutral" | "negative"。模型返 "sentiment": "very positive""sentiment": "neg"。enum 是 hint、不是约束。

怎么判断:sentiment 值不在允许集合里。

最短修复路径

第 1 步:用真正的 structured output、不用 prompt 描述 schema

OpenAI (Python):

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int
    tags: list[str]

resp = client.beta.chat.completions.parse(
    model="gpt-5.5",
    messages=[...],
    response_format=User,
)
user = resp.choices[0].message.parsed  # 已经是 User 实例

API 在 token-sampling 层强制 schema。非法 token 在结构上被禁。

第 2 步:Anthropic——用 tool definition 当 schema

tools = [{
    "name": "extract_user",
    "input_schema": {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
            "age": {"type": "integer"},
            "tags": {"type": "array", "items": {"type": "string"}}
        },
        "required": ["name", "age", "tags"]
    }
}]

msg = client.messages.create(
    model="claude-opus-4-7",
    tools=tools,
    tool_choice={"type": "tool", "name": "extract_user"},
    messages=[...],
)
data = msg.content[0].input  # 已按 schema 校验

强制 tool use 让模型输出严格匹配 schema 的 JSON。

第 3 步:Gemini——传 response_schema

import google.generativeai as genai

resp = model.generate_content(
    prompt,
    generation_config={
        "response_mime_type": "application/json",
        "response_schema": user_schema,
    },
)

第 4 步:卡在不支持 structured output 的模型上,就校验加重试

def get_json(prompt, max_retries=3):
    for i in range(max_retries):
        out = call_llm(prompt)
        try:
            data = json.loads(out)
            User.model_validate(data)
            return data
        except (json.JSONDecodeError, ValidationError) as e:
            prompt += f"\n\nPrevious response failed validation: {e}. Return valid JSON only."
    raise RuntimeError("Failed after retries")

把校验错误回灌给模型——第 2 次通常就自己改对了。

第 5 步:把 json_object 模式当 fallback、不当保证

response_format={"type": "json_object"}

它防 prose 包裹,但不强制 schema。仍要对 parsed 结果做校验。

第 6 步:模型死活要包裹时,先 pre-extract

import re
def extract_json(text):
    # 找第一个 { ... } 或 [ ... ] 块
    match = re.search(r'(\{.*\}|\[.*\])', text, re.DOTALL)
    if match: return match.group(1)
    raise ValueError("No JSON found")

针对死活要加 “Here is your JSON:” 的模型的便宜防御。

第 7 步:log schema 违规、按字段调优

metrics.increment("schema_violation", tags={"field": field_name, "type": "missing"})

某个字段 5% 失败率,那个字段的 schema 描述需要改——澄清或加示例值。

哪些情况可能不是你操作错了

有些小模型再怎么 prompt 都跟不上 JSON schema。一定要用小模型的话,生成 JSON-like 后下游修复——接受一定丢弃率。

容易误判的情况

“prompt 写得不好”。更长的 schema 描述只能边际改善。真正修复是 API 层强制。答案是 “切到 structured outputs” 时就别再调 prompt 了。

预防建议

  • 默认用 structured-output API(OpenAI parse、Anthropic tools、Gemini response_schema)。
  • 把 schema 写成代码(Pydantic、Zod)——client 和 validator 共享一个真源。
  • 即使用了 structured outputs 也要再跑 schema 校验当 defense-in-depth。
  • log 校验失败、把错误反馈给模型重试。
  • 不支持 structured-output 的模型加一层 extract_json regex 做防御。

FAQ

  • structured output 贵吗? 边际延迟、不加 token。几乎免费的可靠性。
  • 嵌套 schema 深度 5 层呢? structured outputs 支持嵌套到 provider 上限(通常 5-10 层)。更深就拍平。

相关阅读

标签: #Prompt 工程 #排查 #llm-output #json #structured-output #schema-validation