结构化输出
🎯 学习目标
- 理解为何 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 定义:
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 定义、校验与类型导出
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)
}集成:
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
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}`)
}降级链:
- strict schema + retry
- 放宽 schema(optional 字段)
- 规则正则抠 email
- 人工审核队列
记录 attempt_count 监控质量恶化。
第 5 步:Python Pydantic 对等模式与生产清单
Python 用 Pydantic:
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 文档选用。
动手练习
- 定义 TicketSchema(title, priority enum, tags[]),实现 extract + Zod 校验。
- 故意让 Prompt 含糊,观察无 schema 与 json_schema 的 parse 失败率差异。
- 实现 extractWithRetry,模拟错误 email 看第二次是否修正。
- 写一条 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 宜扁平,敏感字段脱敏日志。