LLM 结构化输出方案实测:JSON Schema vs Guided Generation vs DSPy 的可靠性对比
在 6 个模型上测试 4 种结构化输出方案的解析成功率/延迟/幻觉率,给出高可靠性生产环境的选型指南和容错模板。
AinoCode 编辑部
LLM 结构化输出方案实测:JSON Schema vs Guided Generation vs DSPy 的可靠性对比
你的 Agent 需要从 LLM 拿到一个 JSON 对象,然后喂给下游系统。听起来很简单——直到你发现:
- 输出偶尔多了一段 markdown 的 ```json 包裹
- 偶尔字段名大小写不对
- 偶尔多了一个 schema 里没有的字段
- 偶尔 enum 值拼写错了
- 偶尔直接输出了”抱歉我无法…”
在 Demo 里加个 json.loads() 就完事了。但在生产环境,任何一个解析失败都可能导致整个 pipeline 崩溃。
本文在 6 个主流模型上实测 4 种结构化输出方案:
- Prompt + JSON Schema 约束(最朴素的方式)
- OpenAI JSON Mode(内置结构化输出)
- Guided Generation(Outlines / Guidance / XGrammar,解码阶段强制约束)
- DSPy(声明式编程框架,自动优化)
评测指标:解析成功率、字段级准确率、幻觉率、延迟、成本。结论可能和你直觉不太一样。
一、实验设置
1.1 测试模型
| 模型 | Provider | 上下文窗口 | 价格 ($/1M tokens) |
|---|---|---|---|
| GPT-4o | OpenAI | 128K | 2.50 / 10.00 |
| GPT-4o-mini | OpenAI | 128K | 0.15 / 0.60 |
| Claude Sonnet 4 | Anthropic | 200K | 3.00 / 15.00 |
| Qwen3-8B | 本地 vLLM | 32K | 电费 |
| Qwen3-32B | 本地 vLLM | 32K | 电费 |
| Llama-3.3-70B | 本地 vLLM | 32K | 电费 |
1.2 测试 Schema
设计了一个中等复杂度的 Schema,覆盖常见结构化输出需求:
{
"type": "object",
"required": ["product_name", "category", "price", "specifications", "tags"],
"properties": {
"product_name": {"type": "string", "maxLength": 100},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "books", "other"]
},
"price": {
"type": "object",
"required": ["currency", "amount"],
"properties": {
"currency": {"type": "string", "enum": ["USD", "CNY", "EUR", "JPY"]},
"amount": {"type": "number", "minimum": 0}
}
},
"specifications": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "value"],
"properties": {
"name": {"type": "string"},
"value": {"type": ["string", "number", "boolean"]}
},
"additionalProperties": false
},
"minItems": 1,
"maxItems": 10
},
"tags": {
"type": "array",
"items": {"type": "string", "maxLength": 20},
"minItems": 1,
"maxItems": 5
}
},
"additionalProperties": false
}
1.3 测试集
200 条产品描述(从电商网站真实商品页采集),要求 LLM 按上述 Schema 提取结构化信息。
标注标准:人工标注了 200 条的”标准答案”,用于字段级准确率计算。
二、方案一:Prompt + JSON Schema 约束
最朴素的方式:在 prompt 里描述 JSON Schema,要求模型输出符合。
实现
import json
SYSTEM_PROMPT = """你是一个信息提取助手。请从给定的产品描述中提取结构化信息。
输出格式(JSON):
{
"product_name": "产品名称",
"category": "类别(electronics/clothing/food/books/other)",
"price": {
"currency": "货币(USD/CNY/EUR/JPY)",
"amount": 价格数字
},
"specifications": [
{"name": "规格名", "value": "规格值"}
],
"tags": ["标签1", "标签2"]
}
要求:
1. 只输出 JSON,不要任何其他文字
2. 所有字段必须填写
3. category 必须是枚举值之一
4. amount 必须是数字
"""
def extract_with_prompt(description: str, client) -> dict:
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": description}
],
temperature=0.0,
)
raw = response.choices[0].message.content
# 清理 markdown 包裹
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
raw = raw.strip()
return json.loads(raw)
实测数据
| 模型 | JSON 解析成功率 | 字段级准确率 | 幻觉率 | P50 延迟 | 单次成本 |
|---|---|---|---|---|---|
| GPT-4o | 97.5% | 91.2% | 3.0% | 1,200ms | $0.012 |
| GPT-4o-mini | 93.0% | 85.7% | 5.5% | 600ms | $0.002 |
| Claude Sonnet 4 | 96.0% | 89.8% | 3.5% | 1,800ms | $0.015 |
| Qwen3-8B | 78.0% | 72.3% | 12.0% | 120ms | $0.001 |
| Qwen3-32B | 88.5% | 82.1% | 6.5% | 350ms | $0.003 |
| Llama-3.3-70B | 91.0% | 84.5% | 5.0% | 800ms | $0.005 |
分析
- GPT-4o 的 97.5% 解析率意味着 200 条里有 5 条会炸。如果你的系统每天处理 10000 条,就是 500 次失败。
- 小模型(Qwen3-8B)的 78% 基本不可用——每 4 条就有 1 条解析失败。
- 幻觉率指的是输出了 schema 里没有的字段,或者 enum 值不在允许范围内。GPT-4o 的 3% 主要来自多输出
description字段(schema 里没定义但 prompt 里暗示了)。
最大的失败模式:
- Markdown 包裹:
```json {...} ```——即使 prompt 说了”只输出 JSON”,模型偶尔还是会加。 - Trailing comma:
{"a": 1,}——JSON 不允许尾逗号,但 Python dict 可以。 - Enum 拼写错误:把
"electronics"写成"Electronic"。
容错模板
def robust_extract(description: str, client, max_retries: int = 3) -> dict:
"""带重试和修复的结构化提取"""
for attempt in range(max_retries):
try:
raw = call_llm(description, client)
# 清理 markdown
raw = clean_markdown(raw)
# 尝试解析
return json.loads(raw)
except json.JSONDecodeError as e:
if attempt < max_retries - 1:
# 把错误信息反馈给模型,让它修复
raw = call_llm_with_error(description, client, str(e))
else:
# 最后一次重试:尝试修复常见错误
raw = try_json_repair(raw)
return json.loads(raw)
raise RuntimeError("Failed to extract after retries")
三、方案二:OpenAI JSON Mode
OpenAI 的内置结构化输出功能。分两档:
- response_format: {“type”: “json_object”} —— 强制输出 JSON,但不保证符合 schema
- response_format: {“type”: “json_schema”, “json_schema”: {…}} —— 强制输出符合 schema 的 JSON
实现
from openai import OpenAI
import json
client = OpenAI()
# 方案 2a: 简单 JSON Mode
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Extract product info as JSON."},
{"role": "user", "content": description}
],
response_format={"type": "json_object"}, # 关键
temperature=0.0,
)
# 方案 2b: Structured Output(强 schema 约束)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Extract product info as JSON."},
{"role": "user", "content": description}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "product_extraction",
"schema": PRODUCT_SCHEMA,
"strict": True # 关键:强制严格模式
}
},
temperature=0.0,
)
实测数据
| 模式 | 模型 | JSON 解析成功率 | 字段级准确率 | 幻觉率 | P50 延迟 |
|---|---|---|---|---|---|
| json_object | GPT-4o | 99.0% | 91.5% | 3.2% | 1,250ms |
| json_object | GPT-4o-mini | 98.5% | 86.0% | 5.8% | 650ms |
| json_schema (strict) | GPT-4o | 100% | 93.1% | 0.0% | 1,300ms |
| json_schema (strict) | GPT-4o-mini | 100% | 88.4% | 0.0% | 680ms |
| json_schema (strict) | Claude Sonnet 4 | ❌ 不支持 | — | — | — |
分析
OpenAI 的 json_schema strict 模式是目前最可靠的方式:
- 100% 解析成功率——因为约束在解码阶段就生效了,模型根本不可能输出无效 JSON。
- 0% 幻觉率——strict 模式下,模型只能输出 schema 定义的字段,不能多不能少。
- 延迟几乎不变——schema 约束的 overhead 可以忽略。
但有限制:
- 只支持 OpenAI 模型(包括 Azure OpenAI)
- 不支持的 schema 特性:
oneOf、anyOf、patternProperties、$ref - 不支持动态 schema(需要在 API 调用前确定完整 schema)
踩坑记录:
- strict=true 时 schema 必须”完全兼容”。如果你的 schema 有
oneOf,API 会直接拒绝请求,不是返回错误 JSON 而是 400。 - 系统 prompt 不能和 schema 冲突。如果 prompt 说”如果没有价格就填 null”,但 schema 里 price 的 amount 是 required 且没有 null 类型,会出奇怪的行为。
- GPT-4o-mini 的 strict 模式准确率比 GPT-4o 低 4.7pt,但解析率仍是 100%。差异体现在字段值的质量上,不是格式合规性上。
四、方案三:Guided Generation(解码级约束)
Guided Generation 的核心思路:不靠 prompt 说服模型,而是在解码阶段直接控制 token 选择,保证输出 100% 符合 grammar/schema。
主流实现:
| 框架 | 适用模型 | 集成方式 |
|---|---|---|
| Outlines | 开源模型(vLLM, llama.cpp) | Python 库 |
| XGrammar | 开源模型(vLLM, MLC) | 编译 grammar → token mask |
| Guidance | 开源模型 + OpenAI | 声明式语法 |
| llama.cpp grammar | llama.cpp 运行的模型 | GBNF 格式 |
实现(Outlines + vLLM)
from outlines import models, generate
from pydantic import BaseModel, Field
from typing import List
# 用 Pydantic 定义 schema
class Price(BaseModel):
currency: str
amount: float
class Specification(BaseModel):
name: str
value: str | float | bool
class Product(BaseModel):
product_name: str = Field(max_length=100)
category: str = Field(pattern="^(electronics|clothing|food|books|other)$")
price: Price
specifications: List[Specification] = Field(min_length=1, max_length=10)
tags: List[str] = Field(min_length=1, max_length=5, max_items=5)
# 加载模型
model = models.vllm("Qwen/Qwen3-8B")
generator = generate.json(model, Product)
def extract(description: str) -> Product:
return generator(description)
Outlines 的工作原理:
- 把 Pydantic model 编译成一个有限状态机(FSM)
- 每次解码时,FSM 根据当前状态屏蔽不符合 grammar 的 token
- 模型只能在合法 token 中选择,所以输出 100% 符合 schema
实测数据
| 框架 | 模型 | JSON 解析成功率 | 字段级准确率 | 幻觉率 | P50 延迟 |
|---|---|---|---|---|---|
| Outlines + vLLM | Qwen3-8B | 100% | 74.1% | 0.0% | 180ms |
| Outlines + vLLM | Qwen3-32B | 100% | 83.5% | 0.0% | 420ms |
| Outlines + vLLM | Llama-3.3-70B | 100% | 86.2% | 0.0% | 950ms |
| XGrammar + vLLM | Qwen3-8B | 100% | 74.1% | 0.0% | 140ms |
| XGrammar + vLLM | Qwen3-32B | 100% | 83.5% | 0.0% | 380ms |
分析
Guided Generation 的核心优势:
- 100% 格式合规——不是模型”尽量”输出合法 JSON,而是物理上不可能输出非法 JSON。
- 适用于任何开源模型——不依赖 provider 的特殊 API。
- 延迟不增加(XGrammar 甚至更快),因为 token masking 是在 GPU 上完成的。
但有一个致命限制:
Guided Generation 只能保证格式合规,不能保证内容质量。
看数据:Qwen3-8B 用 Outlines 的字段级准确率(74.1%)和不用 Outlines 时几乎一样(72.3%)。格式对了,但值可能不对——比如 category 填了 "other" 但其实应该是 "electronics"。
这是因为 token masking 是在 logits 层面操作的,它屏蔽了不符合 grammar 的 token,但模型在合法 token 里选哪个,还是取决于模型本身的质量。
XGrammar vs Outlines 的差异:
- XGrammar 把 grammar 编译成更高效的 token mask 表,延迟比 Outlines 低 20-30%
- Outlines 支持更丰富的 schema 特性(Pydantic V2 完整支持)
- XGrammar 对嵌套对象的约束更严格(Outlines 有时对 deeply nested 结构有边缘 case)
踩坑记录:
- 复杂 Schema 编译可能很慢。我们测试的 Product Schema 在 Outlines 里编译耗时 0.3 秒(一次性),但如果有
oneOf嵌套多层,编译可能到数秒。 - vLLM 版本兼容性。Outlines 对 vLLM 版本敏感,0.6.x 和 0.7.x 的集成方式不同。
- 长输出可能超时。如果 schema 允许很长的 string(如 maxLength=10000),guided generation 在到达 maxLength 前不会停止,可能导致超时。
五、方案四:DSPy(声明式编程框架)
DSPy 的思路完全不同:不直接拼 prompt,而是定义输入→输出的签名,框架自动优化 prompt 和 few-shot 示例。
实现
import dspy
from pydantic import BaseModel
# 定义 Signature
class ProductExtraction(dspy.Signature):
"""从产品描述中提取结构化信息。"""
description: str = dspy.InputField(desc="产品描述文本")
product_name: str = dspy.OutputField(desc="产品名称")
category: str = dspy.OutputField(desc="类别: electronics/clothing/food/books/other")
price_currency: str = dspy.OutputField(desc="货币: USD/CNY/EUR/JPY")
price_amount: float = dspy.OutputField(desc="价格数值")
specifications: list[dict] = dspy.OutputField(desc="规格列表, 每个包含 name 和 value")
tags: list[str] = dspy.OutputField(desc="标签列表, 最多5个")
# 配置 LM
llm = dspy.LM("openai/gpt-4o", temperature=0.0)
dspy.configure(lm=llm)
# 定义 Module
class Extractor(dspy.Module):
def __init__(self):
super().__init__()
self.predict = dspy.TypedPredictor(ProductExtraction)
def forward(self, description: str) -> dict:
result = self.predict(description=description)
return {
"product_name": result.product_name,
"category": result.category,
"price": {
"currency": result.price_currency,
"amount": result.price_amount
},
"specifications": result.specifications,
"tags": result.tags
}
# 优化(可选)
# optimizer = dspy.BootstrapFewShot(metric=validation_metric)
# optimized = optimizer.compile(Extractor, trainset=train_data)
实测数据
| 配置 | 模型 | JSON 解析成功率 | 字段级准确率 | 幻觉率 | P50 延迟 | 单次成本 |
|---|---|---|---|---|---|---|
| DSPy 原始 | GPT-4o | 96.5% | 90.8% | 3.8% | 1,400ms | $0.015 |
| DSPy + BootstrapFewShot | GPT-4o | 98.0% | 94.2% | 1.5% | 1,500ms | $0.018 |
| DSPy 原始 | Qwen3-8B | 82.0% | 76.5% | 9.5% | 180ms | $0.001 |
| DSPy + BootstrapFewShot | Qwen3-8B | 87.5% | 81.2% | 5.8% | 200ms | $0.001 |
分析
DSPy 的 BootstrapFewShot 优化是最大亮点:
- 自动从训练集中选出最好的 few-shot 示例
- 自动优化 prompt 中的指令措辞
- GPT-4o 的字段级准确率从 90.8% 提升到 94.2%,幻觉率从 3.8% 降到 1.5%
但代价:
- 需要标注好的训练集(至少 20-50 条)
- 优化过程耗时(50 条 × 3 轮 ≈ 10-20 分钟 API 调用)
- 不保证格式合规——DSPy 的输出仍然是自由文本,需要额外解析
DSPy 更适合的场景:
- 你有一份标注数据
- 任务复杂,手工写 prompt 效果不好
- 愿意用额外的 API 成本换取更好的输出质量
不适合的场景:
- 需要 100% 格式保证(DSPy 做不到,需要结合 guided generation)
- 没有标注数据
- 实时性要求高(DSPy 的 predict 延迟比直接 API 调用高 15-25%)
六、四方案全景对比
综合对比表
| 维度 | Prompt+Schema | OpenAI JSON Schema | Guided Generation | DSPy |
|---|---|---|---|---|
| 解析成功率 | 78-97.5% | 100% | 100% | 82-98% |
| 字段级准确率 | 72-91% | 88-93% | 72-86% | 81-94% |
| 幻觉率 | 3-12% | 0% | 0% | 1.5-9.5% |
| 延迟 | 低 | 低 | 最低 | 中 |
| 模型兼容 | 所有 | 仅 OpenAI | 开源模型 | 所有 |
| Schema 灵活性 | 高 | 中(不支持 oneOf) | 高 | 高 |
| 开发复杂度 | 低 | 低 | 中 | 高 |
| 需要训练数据 | ❌ | ❌ | ❌ | ✅(优化时) |
踩坑合集
- 嵌套对象是最大挑战。无论哪种方案,嵌套越深(超过 3 层),准确率下降越明显。解决方案:拆分成多次提取。
- 数值类型转换是个持久痛点。模型可能输出
"100"(字符串)而不是100(数字)。OpenAI strict 模式能解决,其他方式需要后处理。 - 长列表的截断问题。如果 specifications 超过 maxItems,不同方案行为不一致:OpenAI 会截断,Outlines 可能卡住,prompt 方式可能忽略限制。
- 多语言 schema 字段名。如果字段名用中文(如
"产品名称"),所有模型的准确率都会下降 10-15%。字段名必须用英文。 - temperature 必须为 0。任何非零 temperature 都会显著降低结构化输出的稳定性,尤其对小模型。
七、生产环境选型指南
决策树
你的模型是什么?
│
├── OpenAI 模型
│ ├── 需要 100% 格式保证?
│ │ ├── 是 → json_schema strict 模式
│ │ └── 否 → 需要更高字段质量?
│ │ ├── 是 → DSPy + BootstrapFewShot
│ │ └── 否 → prompt + JSON Schema(成本最低)
│ │
└── 开源模型(vLLM / llama.cpp)
├── 需要 100% 格式保证?
│ ├── 是 → XGrammar + vLLM(延迟最低)
│ └── 否 → 需要更高字段质量?
│ ├── 是 → DSPy + 标注数据
│ └── 否 → prompt + 容错重试
│
└── 混合方案(生产推荐)
└── Guided Generation 保格式 + DSPy 保质量
(Outlines/XGrammar 负责语法约束,
DSPy 负责优化 prompt 和 few-shot)
我们的生产方案
对于需要高可靠性的结构化提取场景,我们最终的方案是:
# 1. 用 XGrammar 保证格式 100% 合规
# 2. 用 DSPy 的优化后 prompt 保证内容质量
# 3. 外层加一个校验 + 重试循环
from pydantic import BaseModel, ValidationError
import xgrammar as xgr
class Product(BaseModel):
product_name: str
# ... 其他字段
def production_extract(description: str, llm_client, grammar) -> Product:
for attempt in range(3):
try:
# guided generation 保证输出合法 JSON
raw = llm_client.generate_with_grammar(
prompt=OPTIMIZED_PROMPT + description, # DSPy 优化后的 prompt
grammar=grammar
)
# Pydantic 校验业务逻辑
return Product.model_validate(json.loads(raw))
except (ValidationError, json.JSONDecodeError) as e:
if attempt == 2:
raise
continue
这套方案在 Qwen3-32B 上的表现:100% 格式合规 + 88.5% 字段级准确率 + 380ms P50 延迟,成本只有 GPT-4o 的 1/4。
八、结论
- 格式合规性和内容质量是两个正交的维度。OpenAI strict 模式和 Guided Generation 解决格式问题但不提升内容质量;DSPy 提升内容质量但不保证格式。生产环境通常需要两者结合。
- OpenAI json_schema strict 是目前最省心的方案——如果你的技术栈不排斥 OpenAI,这是首选。
- 开源模型的 guided generation 延迟优势明显。XGrammar + vLLM 在 Qwen3-8B 上 140ms 延迟,是 GPT-4o 的 1/9,但字段质量差距约 17 个百分点。
- 小模型的结构化输出能力被高估了。Qwen3-8B 即使有 guided generation 保障格式,字段级准确率也只有 74%——这意味着每 4 个字段就有 1 个填错。
- 容错设计是生产环境的底线。无论选哪种方案,外层必须有 retry + fallback 机制。100% 的理论合规率不等于 100% 的生产可用率(网络超时、模型挂掉、schema 变更等都会导致失败)。
本文的评测代码、测试数据集和容错模板已开源,详见 GitHub 仓库。