结构化输出:JSON Mode / Grammars / Pydantic
让 LLM 输出可靠的机器可读结果——工业界 Agent / 工作流不可或缺的能力。
工业界用 LLM 干活,80% 的时间需要 LLM 输出结构化数据(不是聊天文本):
- 从简历里抽取出
{name, email, skills[]} - 把用户问题分类成
{category, priority, sentiment} - Agent 决定下一步:
{action, args}
但 LLM 天生爱”自由发挥”——
返回 "{ 'name': 'John', ... }",外面包着 markdown ```json 围栏,多了一句解释……
这一篇讲:怎么让 LLM 可靠返回结构化数据。
三层手段
从轻到重:
| 手段 | 强制程度 | 兼容性 |
|---|---|---|
| Prompt 约定 | 弱(靠模型自觉) | 任何 LLM |
| JSON Mode | 中(保证是 JSON,但 schema 不强制) | OpenAI / Anthropic / Gemini |
| Schema 约束 / Grammars | 强(保证符合 schema) | OpenAI 结构化输出 / 部分开源框架 |
一、Prompt 约定(基线)
最简单:在 prompt 里写清楚格式。
prompt = """Extract the following from the resume:
{
"name": string,
"email": string,
"skills": list of strings
}
Output ONLY the JSON, no markdown, no explanation.
Resume: {text}
"""
问题:
- 模型可能加 markdown
```json围栏 - 字段可能漏
- 多了一句”Here is the JSON:”
需要后处理(正则提取、try/except 反复要求)。
适用:原型阶段、对可靠性要求不高。
二、JSON Mode(保证 JSON)
OpenAI、Anthropic 等 API 都提供 response_format:
OpenAI
response = openai.chat.completions.create(
model="gpt-4",
messages=[...],
response_format={"type": "json_object"}
)
效果:模型强制返回有效 JSON。不会有 markdown 围栏、无关文字。
但:不强制 schema——可能字段命名漂移(emailAddress 而不是 email),可能漏字段。
需要 prompt 里仍声明期望 schema,但解析时不会失败。
Anthropic
Claude 通过 tool_use 来达到类似效果(把”输出”当作”调用一个函数”):
response = anthropic.messages.create(
model="claude-3-5-sonnet",
messages=[...],
tools=[{
"name": "extract_resume",
"input_schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"skills": {"type": "array", "items": {"type": "string"}}
}
}
}],
tool_choice={"type": "tool", "name": "extract_resume"}
)
result = response.content[0].input # 直接拿到字典
tool_choice 强制 Claude 必须调用这个 tool —— 等于强制结构化输出。
三、严格 Schema(最稳)
OpenAI Structured Outputs(2024+)
from pydantic import BaseModel
class Resume(BaseModel):
name: str
email: str
skills: list[str]
response = openai.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[...],
response_format=Resume
)
result: Resume = response.choices[0].message.parsed
核心:OpenAI 在内部用 constrained decoding——在 token 采样时,只允许产生符合 schema 的下一个 token。
效果:
- 100% 符合 schema(不像 JSON Mode 只保证语法)
- 字段名、类型、required 全部强制
- 嵌套对象、enum、array 都支持
Outlines / lm-format-enforcer(开源)
对自建 LLM(vLLM、llama.cpp)也能做严格 schema:
import outlines
model = outlines.models.transformers("Llama-3-8B")
@outlines.json(Resume)
def extract(text):
return f"Extract resume info: {text}"
result = extract("...") # 保证是 Resume 类型
底层同样是 constrained decoding——在每一步采样时遮蔽不合法的 token。
实战:Pydantic 模式
最干净的做法:用 Pydantic 定义所有 schema,配合 OpenAI 结构化输出或 instructor 库:
from pydantic import BaseModel, Field
from typing import Literal
class TicketClassification(BaseModel):
category: Literal["billing", "technical", "general"]
priority: Literal["low", "medium", "high", "urgent"]
sentiment: Literal["positive", "neutral", "negative"]
summary: str = Field(description="One sentence summary")
needs_human: bool
class CustomerTicket(BaseModel):
classification: TicketClassification
suggested_response: str
estimated_resolution_minutes: int
import instructor
from openai import OpenAI
client = instructor.from_openai(OpenAI())
result = client.chat.completions.create(
model="gpt-4o",
response_model=CustomerTicket,
messages=[{"role": "user", "content": ticket_text}]
)
# result.classification.category → "billing"
# result.suggested_response → "..."
工业界 80% 的”LLM 做分类 / 抽取”任务用这个套路。
嵌套 + 列表 + 条件
复杂 schema 也行:
class Person(BaseModel):
name: str
age: int
class Event(BaseModel):
title: str
date: str
attendees: list[Person]
is_virtual: bool
meeting_link: str | None = None # 仅 is_virtual=True 时有
class Schedule(BaseModel):
week_of: str
events: list[Event]
LLM 会自动遵守嵌套结构——这比写正则去解析自由文本鲁棒 10 倍。
性能影响
| 手段 | 速度 | 质量 |
|---|---|---|
| Prompt 约定 | 最快 | 不稳定,需要后处理 |
| JSON Mode | 快 | 稳,字段名可能漂移 |
| Schema 严格 | 略慢(多 5-15%) | 100% 符合 |
为什么略慢:constrained decoding 要在每步过滤候选 token。 为什么值得:消除了”解析失败”这一类错误。
反模式
❌ 用正则解析自由文本
# 脆弱:模型一改格式就坏
match = re.search(r'name:\s*"([^"]+)"', llm_output)
❌ 多次重试拿”合法 JSON”
for _ in range(5):
out = llm.chat(...)
try:
return json.loads(out)
except:
continue
raise Error("Failed 5 times")
不用重试——用 schema 强制一次就对。
❌ schema 设计成”自由文本字段”
class Bad(BaseModel):
response: str # ← LLM 在这里又开始自由发挥
把 response 进一步拆开成 summary / action / details 等结构化字段。
3 个工程经验
1. 用 Literal 替代 str 做枚举
priority: Literal["low", "medium", "high"] # ✓
priority: str # ✗ 模型可能输出 "very high"
2. 用 Field 描述意图
summary: str = Field(description="One sentence summary, max 100 chars")
这个 description 会被传给 LLM 当做 prompt。
3. 用 Optional 处理”可能没有”的字段
phone: str | None = None
避免模型瞎编一个不存在的电话号码。
工业界全景
按 2026 现状选型:
| 场景 | 推荐 |
|---|---|
| OpenAI 用户,要稳定 | Structured Outputs + Pydantic |
| Claude 用户,要稳定 | Tool Use + tool_choice 强制 |
| 自建 LLM(vLLM / llama.cpp) | Outlines / instructor |
| 跨厂商抽象 | LangChain JsonOutputParser / DSPy |
| 极快原型 | Prompt + JSON Mode |
2024 前,“用 LLM 抽取数据”是不可靠工程。 2024 后,结构化输出让这件事成为生产级稳定。
这是 LLM 工业化的关键一跃: 从”问个问题、生成段文字” 到”作为某个数据流水线的可靠组件”。
如果你做 LLM 应用—— 结构化输出应该是你掌握的第一个”工程级”工具。
下一篇推荐:L4-15 Prompt Caching 或 L4-09 Tool Use 工程。
🚧 3 个常见坑
坑 1:用自然语言「请输出 JSON」 2024+ 模型有原生 JSON Mode / Schema 强制——别再用「请严格输出 JSON 格式」 这种祈祷式 prompt。
坑 2:Schema 写得太宽松 用 Pydantic / Zod 严格类型——optional 字段全写 required 然后给默认值,比「或者 null 或者字符串」 稳定。
坑 3:不处理 schema violation 即使有强制 schema,仍可能失败——必须 try parse / retry / fallback,不能裸 .parse()。
读到这里说明你认真在学 🎯
订阅每周精选 —— 下一篇新文章 / 新可视化第一时间送到邮箱。
讨论区
· 用 GitHub 账号登录评论src/components/Comments.astro 顶部填入
仓库 ID 和分类 ID(见组件注释里的配置步骤)。