AI AinoCode AI 工具与基础设施
AI教程 4 分钟

LLM 结构化输出方案实测:JSON Schema vs Guided Generation vs DSPy 的可靠性对比

在 6 个模型上测试 4 种结构化输出方案的解析成功率/延迟/幻觉率,给出高可靠性生产环境的选型指南和容错模板。

AinoCode 编辑部

LLM 结构化输出方案对比

LLM 结构化输出方案实测:JSON Schema vs Guided Generation vs DSPy 的可靠性对比

你的 Agent 需要从 LLM 拿到一个 JSON 对象,然后喂给下游系统。听起来很简单——直到你发现:

  • 输出偶尔多了一段 markdown 的 ```json 包裹
  • 偶尔字段名大小写不对
  • 偶尔多了一个 schema 里没有的字段
  • 偶尔 enum 值拼写错了
  • 偶尔直接输出了”抱歉我无法…”

在 Demo 里加个 json.loads() 就完事了。但在生产环境,任何一个解析失败都可能导致整个 pipeline 崩溃

本文在 6 个主流模型上实测 4 种结构化输出方案:

  1. Prompt + JSON Schema 约束(最朴素的方式)
  2. OpenAI JSON Mode(内置结构化输出)
  3. Guided Generation(Outlines / Guidance / XGrammar,解码阶段强制约束)
  4. DSPy(声明式编程框架,自动优化)

评测指标:解析成功率、字段级准确率、幻觉率、延迟、成本。结论可能和你直觉不太一样。


一、实验设置

1.1 测试模型

模型Provider上下文窗口价格 ($/1M tokens)
GPT-4oOpenAI128K2.50 / 10.00
GPT-4o-miniOpenAI128K0.15 / 0.60
Claude Sonnet 4Anthropic200K3.00 / 15.00
Qwen3-8B本地 vLLM32K电费
Qwen3-32B本地 vLLM32K电费
Llama-3.3-70B本地 vLLM32K电费

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-4o97.5%91.2%3.0%1,200ms$0.012
GPT-4o-mini93.0%85.7%5.5%600ms$0.002
Claude Sonnet 496.0%89.8%3.5%1,800ms$0.015
Qwen3-8B78.0%72.3%12.0%120ms$0.001
Qwen3-32B88.5%82.1%6.5%350ms$0.003
Llama-3.3-70B91.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 里暗示了)。

最大的失败模式

  1. Markdown 包裹```json {...} ```——即使 prompt 说了”只输出 JSON”,模型偶尔还是会加。
  2. Trailing comma{"a": 1,} ——JSON 不允许尾逗号,但 Python dict 可以。
  3. 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_objectGPT-4o99.0%91.5%3.2%1,250ms
json_objectGPT-4o-mini98.5%86.0%5.8%650ms
json_schema (strict)GPT-4o100%93.1%0.0%1,300ms
json_schema (strict)GPT-4o-mini100%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 特性:oneOfanyOfpatternProperties$ref
  • 不支持动态 schema(需要在 API 调用前确定完整 schema)

踩坑记录

  1. strict=true 时 schema 必须”完全兼容”。如果你的 schema 有 oneOf,API 会直接拒绝请求,不是返回错误 JSON 而是 400。
  2. 系统 prompt 不能和 schema 冲突。如果 prompt 说”如果没有价格就填 null”,但 schema 里 price 的 amount 是 required 且没有 null 类型,会出奇怪的行为。
  3. 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 grammarllama.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 的工作原理:

  1. 把 Pydantic model 编译成一个有限状态机(FSM)
  2. 每次解码时,FSM 根据当前状态屏蔽不符合 grammar 的 token
  3. 模型只能在合法 token 中选择,所以输出 100% 符合 schema

实测数据

框架模型JSON 解析成功率字段级准确率幻觉率P50 延迟
Outlines + vLLMQwen3-8B100%74.1%0.0%180ms
Outlines + vLLMQwen3-32B100%83.5%0.0%420ms
Outlines + vLLMLlama-3.3-70B100%86.2%0.0%950ms
XGrammar + vLLMQwen3-8B100%74.1%0.0%140ms
XGrammar + vLLMQwen3-32B100%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)

踩坑记录

  1. 复杂 Schema 编译可能很慢。我们测试的 Product Schema 在 Outlines 里编译耗时 0.3 秒(一次性),但如果有 oneOf 嵌套多层,编译可能到数秒。
  2. vLLM 版本兼容性。Outlines 对 vLLM 版本敏感,0.6.x 和 0.7.x 的集成方式不同。
  3. 长输出可能超时。如果 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-4o96.5%90.8%3.8%1,400ms$0.015
DSPy + BootstrapFewShotGPT-4o98.0%94.2%1.5%1,500ms$0.018
DSPy 原始Qwen3-8B82.0%76.5%9.5%180ms$0.001
DSPy + BootstrapFewShotQwen3-8B87.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+SchemaOpenAI JSON SchemaGuided GenerationDSPy
解析成功率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)
开发复杂度
需要训练数据✅(优化时)

踩坑合集

  1. 嵌套对象是最大挑战。无论哪种方案,嵌套越深(超过 3 层),准确率下降越明显。解决方案:拆分成多次提取。
  2. 数值类型转换是个持久痛点。模型可能输出 "100"(字符串)而不是 100(数字)。OpenAI strict 模式能解决,其他方式需要后处理。
  3. 长列表的截断问题。如果 specifications 超过 maxItems,不同方案行为不一致:OpenAI 会截断,Outlines 可能卡住,prompt 方式可能忽略限制。
  4. 多语言 schema 字段名。如果字段名用中文(如 "产品名称"),所有模型的准确率都会下降 10-15%。字段名必须用英文。
  5. 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。


八、结论

  1. 格式合规性和内容质量是两个正交的维度。OpenAI strict 模式和 Guided Generation 解决格式问题但不提升内容质量;DSPy 提升内容质量但不保证格式。生产环境通常需要两者结合。
  2. OpenAI json_schema strict 是目前最省心的方案——如果你的技术栈不排斥 OpenAI,这是首选。
  3. 开源模型的 guided generation 延迟优势明显。XGrammar + vLLM 在 Qwen3-8B 上 140ms 延迟,是 GPT-4o 的 1/9,但字段质量差距约 17 个百分点。
  4. 小模型的结构化输出能力被高估了。Qwen3-8B 即使有 guided generation 保障格式,字段级准确率也只有 74%——这意味着每 4 个字段就有 1 个填错。
  5. 容错设计是生产环境的底线。无论选哪种方案,外层必须有 retry + fallback 机制。100% 的理论合规率不等于 100% 的生产可用率(网络超时、模型挂掉、schema 变更等都会导致失败)。

本文的评测代码、测试数据集和容错模板已开源,详见 GitHub 仓库。