Agent AtlasAI Agent 知识库
02.03提示与上下文 · Prompt & Context

结构化输出与 JSON Schema

让模型稳定返回可程序化消费的数据:Schema 设计、严格模式、校验重试与流式解析。

如果 Agent 的下游是代码,那么「模型说了什么」就必须变成「程序能解析什么」。结构化输出是 Agent 从玩具走向系统的第一道门槛。

为什么需要它

自由文本适合给人看,不适合给程序用:

text
模型:好的,我建议退款,金额大概是 299 元,应该 1 到 3 天到账。
程序:???

结构化输出把同一件事变成:

json
{
  "action": "refund",
  "order_id": "A1024",
  "amount": 299.0,
  "currency": "CNY",
  "eta_days": 3,
  "confidence": 0.92
}

程序可以校验、可以入库、可以触发审批。模型仍然负责判断,但接口是确定的。

三种约束强度

方式做法稳定性适用
提示要求「请返回 JSON」低原型、内部试验
JSON 模式强制输出合法 JSON中结构简单、字段少
Schema 严格模式按 JSON Schema 约束字段与类型高生产、工具参数

严格模式(不同厂商叫法不同:strict、json_schema、response_format)会约束解码过程,显著降低「多一个字段」「少一个引号」的问题。但它通常有约束:

  • 所有字段必须 required(可选字段用 ["string", "null"] 表达);
  • 不支持所有 JSON Schema 特性(如复杂的 oneOf、递归);
  • 首次使用有编译/缓存开销;
  • 模型仍可能填错语义,格式对不等于内容对。
严格模式不等于业务正确

Schema 能保证 amount 是数字,但不能保证它等于订单金额。金额、权限、状态流转必须由服务端校验。

设计 Schema 的七个原则

1. 扁平优先

嵌套越深,模型越容易丢字段。能扁平就扁平;确实需要嵌套时,控制在两层以内。

2. 用枚举代替自由文本

json
{
  "category": {
    "type": "string",
    "enum": ["billing", "technical", "refund", "other"]
  }
}

枚举既约束输出,也让下游分支更清晰。不要用「billing 或账单相关」这种模糊描述。

3. 必填项就是必填项

如果某个字段在业务上必须存在,不要给它默认值糊弄过去。让校验失败,比让空值流入数据库安全。

4. 可选字段用 null,不要用空字符串

json
{ "coupon_code": { "type": ["string", "null"] } }

空字符串会被当成有效值,导致「用了优惠券但没填」这类脏数据。

5. 描述写给模型看

json
{
  "amount": {
    "type": "number",
    "description": "退款金额,单位为元,最多两位小数,不得超过订单实付金额"
  }
}

字段名之外,description 是模型理解语义的主要来源。把单位、范围、边界写清楚。

6. 避免 oneOf 和深层递归

很多严格模式不支持。用「类型字段 + 可选字段」的扁平结构替代:

json
{
  "kind": { "enum": ["text", "image", "file"] },
  "text": { "type": ["string", "null"] },
  "image_url": { "type": ["string", "null"] },
  "file_id": { "type": ["string", "null"] }
}

7. 给模型留「不知道」的出口

json
{
  "answer": { "type": ["string", "null"] },
  "confidence": { "type": "number" },
  "missing_info": { "type": "array", "items": { "type": "string" } }
}

没有出口,模型只能编。允许 null 和 missing_info,能显著降低幻觉。

一个完整的 Schema

json
{
  "name": "support_ticket",
  "strict": true,
  "schema": {
    "type": "object",
    "properties": {
      "category": {
        "type": "string",
        "enum": ["billing", "technical", "refund", "other"],
        "description": "工单主分类;无法判断时用 other"
      },
      "priority": {
        "type": "string",
        "enum": ["low", "normal", "high", "urgent"],
        "description": "urgent 仅用于服务不可用或资金损失"
      },
      "summary": {
        "type": "string",
        "description": "一句话概括,不超过 80 字,不包含用户隐私"
      },
      "order_id": {
        "type": ["string", "null"],
        "description": "订单号;用户未提供时为 null,不要猜测"
      },
      "needs_human": {
        "type": "boolean",
        "description": "是否涉及退款、法律、账号安全等必须人工处理的场景"
      },
      "confidence": {
        "type": "number",
        "minimum": 0,
        "maximum": 1
      }
    },
    "required": ["category", "priority", "summary", "order_id", "needs_human", "confidence"],
    "additionalProperties": false
  }
}

校验与修复循环

即使有严格模式,也要在服务端校验。失败时不要直接抛错,而是把错误喂回模型修复一次:

python
from pydantic import BaseModel, Field, ValidationError
import json

class Ticket(BaseModel):
    category: str = Field(pattern="^(billing|technical|refund|other)$")
    priority: str
    summary: str = Field(max_length=80)
    order_id: str | None
    needs_human: bool
    confidence: float = Field(ge=0, le=1)

def parse_ticket(raw: str) -> Ticket:
    try:
        return Ticket.model_validate_json(raw)
    except ValidationError as e:
        repair = client.chat.completions.create(
            model=MODEL,
            messages=[
                {"role": "system", "content": "只修正 JSON 格式与字段,不要改变语义。"},
                {"role": "user", "content": raw},
                {"role": "user", "content": f"校验错误:{e.errors()}"},
            ],
            response_format={"type": "json_object"},
            temperature=0,
        )
        return Ticket.model_validate_json(repair.choices[0].message.content)

三个细节:

  • 只修复一次。两次还失败,说明任务或 Schema 有问题,应该降级或转人工。
  • 保留原始输出。修复失败时用于排查。
  • 记录失败率。按字段统计,能发现是哪个字段设计得不合理。

流式输出下的结构化

流式返回 JSON 时,内容是不完整的:

text
{"category": "bill

不要尝试 JSON.parse。两种处理方式:

  1. 用增量解析器(如 ijson、json-stream),只消费已经完整的字段;
  2. 让模型先输出一个短的状态字段,完整结果在最后一次性给出。

对 UI 而言,通常只需要流式展示 summary 这类文本字段,其他字段等完整后再生效。

工具参数也是结构化输出

Function Calling 本质上就是「按工具 Schema 生成结构化输出」。所以本文的原则同样适用:

  • 参数名清晰、描述完整;
  • 必填项真实必填;
  • 枚举代替自由文本;
  • 不要让模型传它不知道的 ID(先查再传);
  • 服务端必须重新校验参数。

常见失败与修复

现象可能原因修复
多出未声明字段未开严格模式 / 描述不清additionalProperties: false,精简 Schema
枚举值拼错枚举太多或含义重叠减少选项,加「其他」出口
数字变字符串类型约束弱严格模式 + 服务端转换
该为 null 却编造没有 null 出口允许 null,并在描述中说明
嵌套字段丢失结构太深扁平化,拆成两次调用
格式对但语义错缺少业务校验服务端校验金额、ID、权限
修复循环不收敛错误信息太模糊返回字段级错误与允许值

什么时候不该用

  • 纯给人看的创作:散文、营销文案,硬套 JSON 只会伤害质量;
  • 结构极度开放:让模型先自由探索,再用第二次调用抽取;
  • 超长输出:一次生成巨大 JSON 容易截断,改为分页或分步;
  • 需要精确计算:让模型输出表达式,由代码计算,而不是让它算。

小结

  • 结构化输出让模型成为可编程组件,严格模式是基础。
  • Schema 设计要扁平、用枚举、真实必填、给 null 出口、写清描述。
  • 格式正确不代表语义正确,业务校验不可省。
  • 失败时做一次定向修复,并统计字段级失败率。
  • 工具参数就是结构化输出,同一套原则适用。

下一篇讲工具本身:如何设计模型愿意用、也用得对的接口。

读完了?进度会保存在本地浏览器。
内容会持续修订。发现错误或想补充,欢迎反馈。 遇到问题?查排查手册 →