Skip to content

结构化输出

🎯 学习目标

  • 理解为何 Agent 与业务集成需要可解析的结构化输出
  • 使用 OpenAI JSON Schema / json_object 模式生成约束 JSON
  • 用 Zod(TypeScript)做运行时校验与类型推断
  • 实现「生成 → 校验 → 反馈修正 → 降级」错误恢复链

引言

自由文本适合人类阅读,不适合 程序可靠消费。表单自动填充、工单创建、Agent 工具参数、UI 组件渲染都需要 JSON、XML 或固定字段。让模型「只输出 JSON」仅靠 Prompt 不够——低概率仍会出现 Markdown 围栏、多余解释或字段类型错误。Structured Outputs(结构化输出) 在 API 层用 JSON Schema 约束生成语法,再配合 Zod / Pydantic 做应用层校验,形成双保险。

本节演示 OpenAI 系 response_format: { type: 'json_schema', ... }(或 json_object),编写 Zod schema → JSON Schema 转换思路,并实现 retry with error feedback 循环。目标不是 100% 免维护,而是把失败率压到可运维范围。

章节正文

第 1 步:为什么需要 Schema:失败案例

Prompt 写法:

提取姓名和邮箱,只输出 JSON。
文本:张三 <zhangsan@example.com>

模型可能返回:

```json
{"name":"张三","email":"zhangsan@example.com"}

或 `{"name":"张三","email":"zhangsan@example.com","note":"..."}` 或键名变 `userName`。

**后果**:`JSON.parse` 抛错、Agent 调错工具参数、前端白屏。

结构化输出要满足:

1. **语法合法**(可 parse)
2. **字段齐全、类型正确**
3. **枚举值合法**(如 status 只能是 open|closed)

第 1 层靠 API JSON mode;第 2–3 层靠 Zod。

### 第 2 步:json_object 与 json_schema 模式

**json_object**(较宽):保证内容是合法 JSON object,不保证字段。

```javascript
const res = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [
    { role: 'system', content: '输出 JSON object,包含 name 与 email 字符串字段。' },
    { role: 'user', content: '张三 zhangsan@example.com' },
  ],
  response_format: { type: 'json_object' },
  temperature: 0,
})
const data = JSON.parse(res.choices[0].message.content)

json_schema(更严,视模型支持):传入 schema 定义:

javascript
const res = await client.chat.completions.create({
  model: 'gpt-4o-mini',
  messages: [{ role: 'user', content: 'Extract: Jane jane@acme.com' }],
  response_format: {
    type: 'json_schema',
    json_schema: {
      name: 'contact',
      strict: true,
      schema: {
        type: 'object',
        properties: {
          name: { type: 'string' },
          email: { type: 'string' },
        },
        required: ['name', 'email'],
        additionalProperties: false,
      },
    },
  },
})

国内厂商兼容度不一——上线前用 Eval 验证 target model 是否支持 strict schema。

第 3 步:Zod 定义、校验与类型导出

javascript
import { z } from 'zod'

export const ContactSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
})

export type Contact = z.infer<typeof ContactSchema>

export function parseContact(raw) {
  const json = typeof raw === 'string' ? JSON.parse(raw) : raw
  return ContactSchema.parse(json)
}

集成:

javascript
async function extractContact(client, text) {
  const res = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: 'Extract name and email into JSON matching the schema.',
      },
      { role: 'user', content: text },
    ],
    response_format: { type: 'json_object' },
    temperature: 0,
  })
  return parseContact(res.choices[0].message.content)
}

Zod 失败抛 ZodError,含 路径与原因,可反馈给模型做第二次修正(见下一步)。

复杂 schema:扁平优于深层嵌套;长数组考虑分页生成或 stream 后拼接。

第 4 步:错误恢复:retry with feedback

javascript
async function extractWithRetry(client, text, maxAttempts = 3) {
  let lastError = ''
  const messages = [
    { role: 'system', content: 'Return JSON: { "name": string, "email": valid email }' },
    { role: 'user', content: text },
  ]

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await client.chat.completions.create({
      model: 'gpt-4o-mini',
      messages,
      response_format: { type: 'json_object' },
      temperature: 0,
    })
    const raw = res.choices[0].message.content
    try {
      return parseContact(raw)
    } catch (e) {
      lastError = e.message
      messages.push({ role: 'assistant', content: raw })
      messages.push({
        role: 'user',
        content: `JSON 校验失败:${lastError}。请只输出修正后的 JSON,不要 markdown。`,
      })
    }
  }
  throw new Error(`extract failed: ${lastError}`)
}

降级链

  1. strict schema + retry
  2. 放宽 schema(optional 字段)
  3. 规则正则抠 email
  4. 人工审核队列

记录 attempt_count 监控质量恶化。

第 5 步:Python Pydantic 对等模式与生产清单

Python 用 Pydantic

python
from pydantic import BaseModel, EmailStr
from openai import OpenAI
import json

class Contact(BaseModel):
    name: str
    email: EmailStr

client = OpenAI()
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "Li Si lisi@example.com"}],
    response_format={"type": "json_object"},
    temperature=0,
)
contact = Contact.model_validate_json(resp.choices[0].message.content)

生产清单

  • 字段 enum 写进 schema,不要靠 Prompt 记忆
  • 敏感输出走 Moderation
  • 日志 脱敏(email、身份证)
  • Eval 集测 parse 成功率 目标如 99.5%
  • 流式 冲突时:非实时场景优先非流式 JSON;或 stream 完再 parse

OpenAI SDK 部分版本提供 client.beta.chat.completions.parse 直接绑定 Zod——查阅当前 SDK 文档选用。

动手练习

  1. 定义 TicketSchema(title, priority enum, tags[]),实现 extract + Zod 校验。
  2. 故意让 Prompt 含糊,观察无 schema 与 json_schema 的 parse 失败率差异。
  3. 实现 extractWithRetry,模拟错误 email 看第二次是否修正。
  4. 写一条 Pydantic 或 Zod 单元测试:合法 JSON 通过,缺字段抛错。

常见问题

Q:json_schema strict 所有模型都支持吗?

否。以厂商文档为准;不支持时退 json_object + Zod + retry。

Q:能否流式输出 JSON?

可以流式接收,但 parse 通常等完整 JSON。UI 可显示「生成中」;Partial JSON parser 高级场景再用。

Q:Zod 与 JSON Schema 都要写吗?

API 层 JSON Schema(若支持)约束生成;应用层 Zod 表达业务规则(email、min length)。可探索 zod-to-json-schema 减少重复。

本节小结

结构化输出让 LLM 返回可程序消费的 JSON;API 层 json_schema/json_object 约束语法,Zod/Pydantic 约束业务类型。低温 + 明确 schema + retry with validation error 是标准恢复链。Eval 应跟踪 parse 成功率,schema 宜扁平,敏感字段脱敏日志。