结构化输出怎么保证模型按格式返回?先把“保证”拆开:一是响应能被解析,二是字段、类型和枚举符合约定,三是字段值在业务上正确。这三层不能只靠一句“请严格返回 JSON”同时解决。工程上要把模型生成、格式约束、服务端校验和失败处理连成一条链路。

为什么提示词写了格式,还是会出错?

让模型把客服工单归类为“故障、账单、其他”,并返回优先级和摘要。即使提示词给了示例,模型仍可能加上解释文字、漏字段、把优先级写成“紧急”、输出不完整 JSON,或者给出格式正确但分类错误的结果。提示词描述的是期望行为,不是应用程序的类型系统;采样、上下文变化、拒答和输出截断都可能影响结果。

先定义一个最小契约:

{
  "category": "bug",
  "priority": "high",
  "summary": "登录后页面持续报错",
  "needsHuman": true
}

字段名、类型、允许值、必填项和额外字段的处理方式都要明确。生产场景还需要版本号或清晰的接口版本管理,避免修改字段后下游仍按旧结构解析。

三种控制手段,强度不同

手段 能解决什么 仍需注意什么
提示词加示例 让模型理解任务和期望结构 不能保证合法 JSON 或固定字段
JSON 模式 通常约束输出为可解析 JSON 合法 JSON 仍可能缺字段、类型错误或语义错误
Schema 约束输出 / 工具参数 在支持的模型与接口中约束字段和类型 支持的 Schema 子集、拒答、截断和语义正确性仍需处理

“结构化输出”在不同服务商的 API 中含义并不完全相同。有的通过受约束解码限制生成的 token,有的把结果放在工具调用参数里。具体是否支持严格模式、哪些 JSON Schema 关键字有效,要以所用模型和接口文档为准。即使用了严格模式,也只应把完整且成功的响应送去解析;拒答、超时、达到输出上限或工具调用未完成,都不是一条可用业务记录。

先设计 Schema,再写提示词

以工单分类为例,可以定义对象必填字段、枚举和禁止多余属性:

{
  "type": "object",
  "properties": {
    "category": { "type": "string", "enum": ["bug", "billing", "other"] },
    "priority": { "type": "string", "enum": ["low", "medium", "high"] },
    "summary": { "type": "string" },
    "needsHuman": { "type": "boolean" }
  },
  "required": ["category", "priority", "summary", "needsHuman"],
  "additionalProperties": false
}

Schema 应尽量小:只要下游真会用到的字段。可选字段要统一缺失值的表示,别让一部分调用省略字段、另一部分返回空字符串或 null。枚举比自由文本更适合程序分支。摘要长度、日期格式等约束若供应商不支持,就放在服务端验证。

提示词负责说清判断标准,而不是重复一大段 JSON。例如写明:何时归入 billing,什么情况必须标记 needsHuman,以及证据不足时如何选择 other。输入的工单正文是待分析数据,不能让其中“忽略以上规则”之类的话覆盖系统规则。

服务端必须再验一次

模型返回后,按“响应完成 → JSON 解析 → Schema 校验 → 业务校验”的顺序处理。下面的 TypeScript 示例使用 Zod 演示应用侧校验;它独立于具体模型 SDK:

import { z } from 'zod'

const Ticket = z.object({
  category: z.enum(['bug', 'billing', 'other']),
  priority: z.enum(['low', 'medium', 'high']),
  summary: z.string().min(1).max(120),
  needsHuman: z.boolean(),
}).strict()

type Ticket = z.infer<typeof Ticket>

function parseTicket(raw: string): Ticket {
  const json: unknown = JSON.parse(raw)
  return Ticket.parse(json)
}

JSON.parse 可能抛语法错误,Ticket.parse 可能抛校验错误;调用方应捕获并记录错误类别,不能把失败当成空对象继续写库。这里的 Zod 规则也说明了一件事:模型接口接受的 Schema 与应用侧校验器可以各司其职,前者减少无效生成,后者守住自己的数据边界。

通过类型校验仍不代表内容为真。比如“无法登录”被分到 billing,或模型凭空写出未发生的退款承诺,都属于业务错误。对高影响字段增加规则检查、原文证据定位或人工复核;金额、权限和实际执行动作尤其不能只凭模型自由生成的值决定。

失败时怎么处理?

不要无限重试。一个实用策略是:对可恢复的格式错误或临时故障重试一到两次,向模型提供简短的校验错误;对拒答、持续失败或超出预算的请求,进入人工处理或明确的失败状态。重试要有超时、次数上限和成本上限,写库或触发动作前要考虑幂等性。不能悄悄把“解析失败”改成默认的 high 或 other,因为这会把系统故障混进正常业务数据。

流式输出时,传输中的片段可能不是完整 JSON。应等到响应完成并确认结束原因后再解析和校验;若需要逐步展示,可把预览和正式结果分开。日志记录请求 ID、模型版本、Schema 版本、校验错误与重试次数,避免把客户原文或敏感数据无必要地写进日志。

最后用一批真实样本做回归测试:统计解析成功率、Schema 通过率、业务准确率、人工接管率和平均重试成本。前两个指标只能说明“长得像正确数据”,第三个才更接近“做对了事”。结构化输出的可靠性来自多层约束和可观测的失败处理,而不是相信模型每次都会听话。