大模型很擅长生成自然语言,但业务系统真正需要的,通常不是一段“看起来正确”的回答,而是一份可以直接被程序消费的数据。

例如做意图识别,我们希望模型返回:

{
  "intent": "查询天气",
  "confidence": 0.96,
  "slots": {
    "city": "北京"
  }
}

而不是:

用户应该是想查询北京的天气,置信度比较高。

前者可以直接进入业务流程,后者还需要正则提取、JSON 修复和异常重试。

这就是结构化输出的意义:让大模型不仅回答正确,还要按照程序规定的格式回答。

本文以 vLLM 为例,介绍如何部署大模型,并分别通过离线推理和在线服务实现 JSON 结构化输出。


一、只在 Prompt 中要求返回 JSON,为什么不够

最简单的做法,是在 Prompt 中加一句:

请只返回 JSON,不要输出其他内容。

这种方式可以提高模型返回 JSON 的概率,但不能提供强保证。

模型仍然可能输出:

好的,结果如下:

{
  "intent": "查询天气"
}

也可能出现字段缺失、类型错误、枚举值错误,甚至 JSON 没有闭合。

在测试环境里,这些问题可能偶尔出现;到了线上,只要请求量足够大,就一定会遇到。

所以,结构化输出不能只依赖 Prompt,而应该在模型生成 Token 的过程中直接施加约束。


二、vLLM 的结构化输出是怎么实现的

vLLM 支持通过 XGrammar 或 llguidance 作为结构化输出后端,并且可以根据请求内容自动选择合适的后端。

它的核心流程可以概括成一句话:

把 JSON Schema 编译成语法约束,在每一步生成 Token 前屏蔽不合法的候选 Token。

假设模型需要输出:

{
  "name": "张三",
  "age": 20
}

当模型已经生成:

{"age":

根据 JSON Schema,接下来只能生成整数。

此时模型原本的候选可能是:

20
"二十"
null
hello

结构化输出后端会计算一个 Token Mask,将不合法 Token 的概率设置为负无穷:

20       保留
"二十"   屏蔽
null     屏蔽
hello    屏蔽

然后再进行采样。

这个过程会持续到整个 JSON 生成完成。

XGrammar 和 llguidance 的具体实现有所不同,但基本思路一致:维护当前语法状态,计算下一步允许出现的 Token,再把非法 Token 从采样空间中排除。

需要注意的是:

结构化输出保证的是格式和类型约束,不保证内容在业务上一定正确。

例如 Schema 可以保证 confidence 是数字,但不能自动保证这个置信度真的可靠。因此,Prompt 设计和业务校验仍然不能省。


三、先定义统一的数据结构

实际开发中,推荐使用 Pydantic 定义返回结构,再自动生成 JSON Schema。

下面以意图识别为例:

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field


class IntentResult(BaseModel):
    model_config = ConfigDict(extra="forbid")

    intent: Literal["查询天气", "播放音乐", "其他"]
    confidence: float = Field(ge=0, le=1)
    slots: dict[str, str]

生成 JSON Schema:

json_schema = IntentResult.model_json_schema()

这样做有两个好处:

第一,vLLM 可以使用 Schema 约束模型生成;第二,模型输出后还可以继续使用 Pydantic 做解析和校验。


四、方式一:离线推理

离线推理指的是直接在 Python 程序中加载 vLLM:

Python 程序
    ↓
vLLM Engine
    ↓
本地模型

这种方式不需要启动 HTTP 服务,适合批量数据生成、模型测试、离线评测和内部算法脚本。

1. 安装依赖

pip install -U vllm pydantic

2. 完整代码

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field
from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams


class IntentResult(BaseModel):
    model_config = ConfigDict(extra="forbid")

    intent: Literal["查询天气", "播放音乐", "其他"]
    confidence: float = Field(ge=0, le=1)
    slots: dict[str, str]


def main() -> None:
    # 1. 加载模型
    llm = LLM(
        model="Qwen/Qwen2.5-3B-Instruct",
        max_model_len=2048,
    )

    # 2. 从 Pydantic 模型生成 JSON Schema
    json_schema = IntentResult.model_json_schema()

    # 3. 配置结构化输出
    structured_outputs = StructuredOutputsParams(
        json=json_schema
    )

    # 4. 配置采样参数
    sampling_params = SamplingParams(
        temperature=0,
        max_tokens=256,
        structured_outputs=structured_outputs,
    )

    prompt = """
你是一个意图识别模型。

请分析下面的用户输入,并返回:
- intent:只能是“查询天气”“播放音乐”或“其他”
- confidence:0 到 1 之间的数字
- slots:提取到的槽位,没有则返回空对象

用户输入:帮我查一下明天北京的天气。
"""

    # 5. 执行推理
    outputs = llm.generate(
        prompts=[prompt],
        sampling_params=sampling_params,
    )

    raw_output = outputs[0].outputs[0].text

    print("模型原始输出:")
    print(raw_output)

    # 6. 使用 Pydantic 再次校验
    result = IntentResult.model_validate_json(raw_output)

    print("解析后的 Python 对象:")
    print(result)

    print("意图:", result.intent)
    print("置信度:", result.confidence)
    print("槽位:", result.slots)


if __name__ == "__main__":
    main()

输出类似:

{
  "intent": "查询天气",
  "confidence": 0.98,
  "slots": {
    "city": "北京",
    "date": "明天"
  }
}

离线模式中,真正起作用的是这两层配置:

structured_outputs = StructuredOutputsParams(
    json=json_schema
)

sampling_params = SamplingParams(
    structured_outputs=structured_outputs
)

StructuredOutputsParams 描述输出结构,SamplingParams 将这个约束交给 vLLM 推理引擎。官方离线示例使用的也是这种调用方式。


五、方式二:在线服务

在线服务指的是先启动一个 vLLM Server,再通过 HTTP 请求调用模型:

业务客户端
    ↓
OpenAI 兼容接口
    ↓
vLLM Server
    ↓
本地模型

这种方式更适合正式部署,因为前端、后端、Java 服务和其他 Python 服务都可以通过 HTTP 调用,不需要直接依赖 vLLM。

1. 启动 vLLM 服务

vllm serve Qwen/Qwen2.5-3B-Instruct \
  --host 0.0.0.0 \
  --port 8000

结构化输出默认已经启用。vLLM 默认使用 auto 选择后端,也可以在启动时手动指定:

vllm serve Qwen/Qwen2.5-3B-Instruct \
  --host 0.0.0.0 \
  --port 8000 \
  --structured-outputs-config.backend=xgrammar

通常直接使用默认的 auto 即可,除非需要固定后端做性能测试或兼容性验证。

2. 客户端完整代码

安装客户端:

pip install -U openai pydantic

调用代码:

from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field


class IntentResult(BaseModel):
    model_config = ConfigDict(extra="forbid")

    intent: Literal["查询天气", "播放音乐", "其他"]
    confidence: float = Field(ge=0, le=1)
    slots: dict[str, str]


client = OpenAI(
    base_url="http://127.0.0.1:8000/v1",
    api_key="-",
)

model = client.models.list().data[0].id
json_schema = IntentResult.model_json_schema()

response = client.chat.completions.create(
    model=model,
    messages=[
        {
            "role": "system",
            "content": (
                "你是一个意图识别模型。"
                "请提取意图、置信度和槽位。"
            ),
        },
        {
            "role": "user",
            "content": "帮我查一下明天北京的天气。",
        },
    ],
    temperature=0,
    max_tokens=256,
    extra_body={
        "structured_outputs": {
            "json": json_schema
        }
    },
)

raw_output = response.choices[0].message.content

if raw_output is None:
    raise RuntimeError("模型没有返回内容")

print("模型原始输出:")
print(raw_output)

result = IntentResult.model_validate_json(raw_output)

print("解析结果:")
print(result)

这里最关键的是:

extra_body={
    "structured_outputs": {
        "json": json_schema
    }
}

structured_outputs 不是标准 OpenAI API 参数,因此需要通过 OpenAI Python 客户端的 extra_body 发送给 vLLM。vLLM Server 收到请求后,会把它转换成内部的结构化输出配置。


六、SamplingParams 和 extra_body 到底有什么区别

两者的目标相同,区别只在调用入口。

对比项离线推理在线服务
调用方式直接调用 vLLM Python API通过 HTTP 调用
结构化参数SamplingParamsextra_body
模型加载位置当前 Python 进程独立 vLLM Server
是否需要启动服务不需要需要
适合场景实验、评测、批处理生产服务、多人调用
调试成本较低需要处理服务和网络
系统解耦较弱较强

可以把它理解为:

离线推理:

StructuredOutputsParams
        ↓
SamplingParams
        ↓
vLLM Engine
在线服务:

extra_body
        ↓
HTTP 请求
        ↓
vLLM Server
        ↓
内部结构化输出参数

它们最终都会进入 vLLM 的约束解码流程,并不是两套完全不同的实现。


七、该选择哪一种方式

如果正在做模型实验、数据生产或者效果评测,优先使用离线推理。

例如:

  • 批量生成训练数据
  • 测试不同 Prompt
  • 比较不同模型
  • 做信息抽取离线评测
  • 在单机脚本中完成推理

这种场景直接使用:

LLM + SamplingParams

代码更简单,也更方便调试。

如果需要给业务系统提供统一接口,则更适合在线服务。

例如:

  • 后端业务调用
  • 多用户并发请求
  • Agent 服务
  • 工具调用
  • 多个应用共享同一个模型
  • 需要统一监控、限流和扩缩容

这种场景使用:

vllm serve + OpenAI Client + extra_body

会更加合理。

我的建议是:开发阶段先使用离线推理验证效果,逻辑稳定之后再部署成在线服务。


八、实际使用时的几个注意点

1. 使用新接口

vLLM 0.12.0 已经移除了 guided_json、guided_regex 和 guided_choice 等旧字段。

现在应该使用:

StructuredOutputsParams(json=json_schema)

或者:

extra_body={
    "structured_outputs": {
        "json": json_schema
    }
}

不要再使用:

extra_body={
    "guided_json": json_schema
}

2. Prompt 仍然要写清楚字段含义

Schema 只能告诉模型字段是什么类型,不能完整表达业务含义。

例如:

confidence: float

只能约束它是数字。

Prompt 里仍然需要说明:

confidence 表示模型对意图判断的置信度,范围为 0 到 1。

3. 输出后继续做 Pydantic 校验

即使使用结构化输出,也建议保留:

IntentResult.model_validate_json(raw_output)

这样可以把字符串转换成明确的 Python 对象,同时在接口升级、Schema 调整或服务异常时及时发现问题。

4. max_tokens 不要设置得太小

结构化输出仍然需要足够的生成长度。

如果 JSON 字段较多,而 max_tokens 设置过小,模型可能在结构完成前达到长度限制。

5. 结构正确不等于内容正确

结构化输出可以保证:

{
  "confidence": 0.9
}

符合 Schema。

但不能保证 0.9 一定准确。

因此业务中仍然需要:

  • 枚举限制
  • 数值范围限制
  • 业务规则校验
  • 必要时进行重试或降级

总结

vLLM 的结构化输出解决了一个很实际的问题:让大模型输出从“给人看的文本”变成“可以直接被程序使用的数据”。

它的核心原理并不复杂:

JSON Schema
    ↓
语法约束
    ↓
计算合法 Token
    ↓
屏蔽非法 Token
    ↓
生成合法 JSON

在使用方式上:

  • 离线推理通过 StructuredOutputsParams 和 SamplingParams 配置;
  • 在线服务通过 OpenAI API 的 extra_body 传递 structured_outputs;
  • 两者底层使用的是同一类约束解码机制;
  • 实验和批处理适合离线模式,正式业务部署更适合在线服务。

对于信息抽取、意图识别、Agent 工具调用和自动化工作流来说,结构化输出已经不只是一个锦上添花的功能,而是让大模型真正进入工程系统的重要基础能力。

更多推荐