结构化输出

下游是代码(写入数据库、触发工单、喂给下一个节点)时,「看起来像 JSON 的散文」不够。结构化输出 把输出合同从自然语言升级为 schema:缺字段、多字段、类型不对,应在 API 层失败,而不是在业务里 silently 错下去。

概念上你仍在写提示词:schema 是 机器可执行的「输出格式」。提示词负责语义(不要编造 owner);schema 负责形状(priority 只能是那几个枚举)。


JSON 只是形状,schema 才是合同

口头要求「只返回 JSON」常见失败:

  • 前后包了 ```json 围栏或一句「好的,如下」
  • 字段时有时无
  • 数字写成字符串
  • 幻觉出一个文档里没有的键

更稳的层次:

层次作用
提示词语义规则、拒绝策略、字段含义
JSON Schema / Pydantic必填、类型、枚举、嵌套
厂商 structured output约束解码,降低「像 JSON 但非法」
你的校验业务不变量(due 不能早于今天)

LangChain 1.0 里对应 response_format=YourModelwith_structured_output(见 模型与消息)。Dify 工作流则用「解析节点 / 结构化输出」接在生成后面。


先写人能读的 schema

{
  "title": "string, 一句话标题,不要句号",
  "priority": "low | medium | high",
  "owner": "string | null,正文没点名负责人则为 null",
  "due": "YYYY-MM-DD | null"
}

把这段同时放进提示词 代码里的模型类。两处不一致时,以代码为准,并改提示词——否则 评估 会永远红。


最小 Pydantic + OpenAI 示例

下面用 Responses API 的 parse(Chat Completions 的 parse 同一思路:把 Pydantic 类交给 SDK)。模型名以你账号里可用的为准。

from pydantic import BaseModel, Field
from openai import OpenAI

class Ticket(BaseModel):
    title: str
    priority: str = Field(description="low, medium, or high")
    owner: str | None = None

client = OpenAI()
resp = client.responses.parse(
    model="gpt-4.1-mini",
    instructions="Extract a ticket. Do not invent owner. Priority from urgency words only.",
    input="Login fails after reset. Please have the platform team look today.",
    text_format=Ticket,
)
ticket = resp.output_parsed
print(ticket.model_dump())

提示词仍然必要:text_format 不会教模型「不要编造 owner」。schema 保证 owner 要么是字符串要么是 null,不保证 null 用得对。

解析失败时 重试或走人工,不要用正则从散文里「抢救」一半字段——那会把评估基准打烂。


提示词怎么配合 schema

从用户消息抽 Ticket。
- owner 仅当消息里出现人名或明确团队名
- 不要输出 schema 以外的键
- 不要在 JSON 外说话

有 structured output 通道时,不要再要求「用 Markdown 详细分析 + 同一条消息里再给 JSON」。一次调用一个合同。需要分析过程:要么用厂商的 reasoning / 隐藏思维通道,要么拆成两步(先分析文本,再结构化)。


常见误用

  • 用 JSON 当散文容器(一个巨大的 notes 字段里什么都有)——你并没有结构化
  • 枚举过于细(40 个优先级)——模型会乱跳;先粗后细
  • 把密钥、身份证放进抽取目标——能抽不等于该存;见 陷阱

下一步

评论