用 vLLM 部署大模型,并让它稳定输出 JSON
大模型很擅长生成自然语言,但业务系统真正需要的,通常不是一段“看起来正确”的回答,而是一份可以直接被程序消费的数据。
例如做意图识别,我们希望模型返回:
{
"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 调用 |
| 结构化参数 | SamplingParams | extra_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 工具调用和自动化工作流来说,结构化输出已经不只是一个锦上添花的功能,而是让大模型真正进入工程系统的重要基础能力。
更多推荐

所有评论(0)