02.05概念更新于 2026年9月17日

结构化输出

让模型稳定输出 JSON:Schema 约束、校验与失败重试。

02.05 · 课时讲解

结构化输出

用 JSON Schema 约束格式,但格式正确不等于内容正确。

1 / 11

一句话理解:结构化输出就是用 JSON Schema 约束模型的输出格式,让结果能被程序直接解析;但“格式正确”不等于“内容正确”,业务校验与失败处理仍然要由程序完成。

Agent 的输出很少只给人看。客服工单分类 Agent 的判断要写进工单系统,SEO 诊断结果要生成报表,销售分析结论要触发补货任务。如果模型有时输出 JSON、有时在 JSON 前面加一句“好的,以下是结果”、有时少一个字段,下游系统就会随机崩溃。

早期大家靠在提示词里写“请只输出 JSON”加上正则清洗来应付,效果时好时坏。现在主流 API 都提供了原生的结构化输出能力。这一课讲它怎么用、能保证什么、不能保证什么。

三种做法的可靠性对比

做法 原理 可靠性 适用场景
提示词要求 在指令里描述格式,附一个示例 多数时候可用,但偶尔多字、缺字段、类型错 原型验证、只给人看的输出
工具调用 把输出定义成一个工具的参数,模型“调用”它 较高;开启严格模式后参数符合 Schema 模型本来就要调用工具的 Agent
原生结构化输出 API 在生成时按 Schema 约束 Token 选择 高:输出是合法 JSON 且符合 Schema 需要稳定对接下游系统的结果

原生结构化输出的原理可以直觉地理解为:模型每生成一个 Token 时,API 会把不符合 Schema 的候选排除掉。比如 Schema 要求下一个位置是布尔值,模型就不可能写出 "是的"。

用一张图表示这个过程:

原生结构化输出的原理:Schema 要求布尔值时,不符合的候选 Token 在生成时被排除 已生成的 JSON 前缀 "need_human_review": 候选 Token(Schema 要求布尔值) true 保留 false 保留 "是的" 排除 "我觉得" 排除 只在合法候选中采样 输出一定符合 Schema
图 1 · 约束解码:不符合 Schema 的候选在生成时就被排除

Claude:output_config.format

以客服工单分类为例。我们希望模型读完工单内容和订单数据后,输出一个固定结构的判断:

import anthropic

client = anthropic.Anthropic()

schema = {
    "type": "object",
    "properties": {
        "ticket_id": {"type": "string"},
        "issue_type": {
            "type": "string",
            "enum": ["发货延迟", "商品质量", "退款申请", "账单问题", "无法判断"],
        },
        "confidence": {"type": "string", "enum": ["高", "中", "低"]},
        "evidence": {"type": "array", "items": {"type": "string"}},
        "suggested_action": {"type": "string"},
        "need_human_review": {"type": "boolean"},
    },
    "required": [
        "ticket_id", "issue_type", "confidence",
        "evidence", "suggested_action", "need_human_review",
    ],
    "additionalProperties": False,
}

response = client.messages.create(
    model="claude-opus-5",
    max_tokens=4096,
    system="你是客服工单分析助手。只依据提供的数据判断;证据不足时 issue_type 选“无法判断”。",
    messages=[{"role": "user", "content": "工单内容:……\n订单数据:……"}],
    output_config={"format": {"type": "json_schema", "schema": schema}},
)

返回的 text 内容块就是一段符合 Schema 的 JSON:

{
  "ticket_id": "T-10231",
  "issue_type": "发货延迟",
  "confidence": "中",
  "evidence": ["订单于 9 月 12 日 08:14 支付成功", "此后 72 小时仍无发货记录"],
  "suggested_action": "核实仓库是否缺货,并向客户同步预计发货时间",
  "need_human_review": false
}

如果模型本来就要调用工具,也可以在工具定义上加 strict: true,让工具参数严格符合 input_schema(同样要求 additionalProperties: false)。工具调用的完整机制见 Function Calling 原理。

OpenAI:text.format

OpenAI Responses API 的对应写法是 text.format,需要给 Schema 起一个名字,并通过 strict 开启严格模式:

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-6-astra",  # 模型名以官方文档为准
    input=[
        {"role": "developer", "content": "你是客服工单分析助手。只依据提供的数据判断。"},
        {"role": "user", "content": "工单内容:……\n订单数据:……"},
    ],
    text={
        "format": {
            "type": "json_schema",
            "name": "ticket_classification",
            "schema": schema,  # 复用上面定义的 Schema
            "strict": True,
        }
    },
)
print(response.output_text)

OpenAI 的文档还说明:当模型出于安全原因拒绝时,输出中会出现 refusal 类型的内容,而不是符合 Schema 的 JSON,代码需要单独处理这一分支。两家的 SDK 也都提供了配合 Pydantic 等类型库直接解析成对象的辅助方法,具体用法以各自 SDK 文档为准。

“格式保证”之外,还要做什么

原生结构化输出解决的是 语法层 的问题。一个生产级实现至少还要处理下面四件事:

1. Schema 本身有能力边界。 以 Claude 为例,官方列出了不支持的 JSON Schema 特性,包括:递归 Schema、数值约束(minimum、maximum、multipleOf)、字符串长度约束(minLength、maxLength)、minItems 为 0 或 1 以外的数组约束,以及 additionalProperties 不为 false 的情况。这意味着“金额必须大于 0”“摘要不超过 200 字”这类规则 不能指望 Schema 保证,必须写进程序校验。

2. 非正常结束时,内容可能不完整或不存在。 输出被 max_tokens 截断、模型拒答、触到上下文上限时,拿到的可能不是完整 JSON。先检查停止原因,再解析。

3. 业务语义需要程序校验。 JSON 合法、字段齐全,不代表内容对:工单号可能和输入的不一致,证据可能是编造的,need_human_review 可能在高风险情况下被设成了 false。

4. 失败要有明确的重试与兜底策略。 完整流程如下:

结构化输出的校验与重试流程:检查停止原因、解析与类型校验、业务规则校验,失败分别处理,通过后写入工单系统 调用模型 带 Schema 检查停止原因 stop_reason 解析与类型校验 JSON + Pydantic 业务规则校验 由程序执行 写入工单系统 下游消费 按原因处理 提高上限 / 转人工 非正常结束 重试 ≤ N 次 仍失败则转人工 失败 标记待复核 不自动写入下游 失败 附上错误信息重试 业务规则示例:Schema 保证不了,由程序检查 ticket_id 等于输入工单号 evidence 时间可在订单中找到 退款申请须转人工复核
图 2 · 格式保证之外:停止原因、类型、业务规则三道检查,各有失败出口

用 Python 表达“解析 + 业务校验”的核心部分(示意):

import json
from typing import Literal
from pydantic import BaseModel, ValidationError

class TicketClassification(BaseModel):
    ticket_id: str
    issue_type: Literal["发货延迟", "商品质量", "退款申请", "账单问题", "无法判断"]
    confidence: Literal["高", "中", "低"]
    evidence: list[str]
    suggested_action: str
    need_human_review: bool

def parse_result(response, input_ticket_id: str) -> TicketClassification | None:
    if response.stop_reason != "end_turn":
        return None  # 截断、拒答等情况交给上层处理
    text = next(b.text for b in response.content if b.type == "text")
    try:
        result = TicketClassification.model_validate(json.loads(text))
    except (json.JSONDecodeError, ValidationError):
        return None
    # 业务规则:Schema 保证不了的,由程序保证
    if result.ticket_id != input_ticket_id:
        return None
    if result.issue_type == "退款申请" and not result.need_human_review:
        result.need_human_review = True
    return result

Schema 设计的几个经验

  • 用 enum 代替自由文本。 问题类型、置信度、状态这类字段,枚举值能直接对接下游逻辑,也方便统计和评测。
  • 一定要有“兜底值”。 比如 无法判断、数据不足。没有兜底选项时,模型只能在错误选项里硬选一个。
  • 把证据和结论分开。 evidence 字段让结论可追溯,也方便程序核对,这是降低 幻觉 风险的常用手段。
  • 字段名和描述要自解释。 模型通过字段名和指令理解每个字段的含义,flag1、type2 这类命名会直接降低准确率。
  • 别把所有东西都塞进一个巨大的 Schema。 字段越多,每个字段的质量越难保证;复杂任务可以拆成多次调用。更系统的方法见 输出契约。

常见误区

  • “用了结构化输出,就不需要校验了。” 它保证的是格式,不是事实和业务规则。
  • “在 Schema 里写了 maxLength,模型就不会超长。” 不少约束并不被支持或不会被强制执行,要查文档,并在程序里再校验一遍。
  • “字段越细越好。” 过多字段会增加 Token 成本和出错面,只保留下游真正会消费的字段。

产品经理检查清单

  • 模型输出最终被谁消费?下游需要哪些字段、什么类型、哪些枚举值?
  • 每个枚举字段是否有“无法判断 / 其他”这样的兜底值?
  • 哪些业务规则 Schema 无法表达,必须由程序校验?
  • 解析失败、业务校验失败时,是重试、转人工,还是标记待复核?
  • 是否记录了解析失败率和校验失败率,作为质量监控指标?

练习

为「SEO 诊断 Agent」设计一个结构化输出(约 45 分钟):

  1. 定义 Schema:每个页面输出 url、issues(数组,每项含 type 枚举、severity 枚举、evidence、fix)和 overall_score_level(枚举)。
  2. 挑 3 个真实网页的 HTML 片段,用 Claude 或 OpenAI 的结构化输出跑一遍。
  3. 写下至少 3 条 Schema 无法保证、需要程序校验的规则(例如 evidence 里引用的标签必须真实出现在 HTML 中),并说明校验失败时的处理方式。

延伸阅读

下一步

格式可以被保证,内容却不行。下一课「幻觉」专门讲模型为什么会编造,以及如何用检索、引用、验证与拒答设计把风险降下来。

试试:上下文、MCP、沙箱、Prompt 注入、外链