跳到主要内容

结构化输出

在实际应用中,我们经常需要模型输出特定格式的数据,例如 JSON 对象、特定格式的字符串或符合某种语法规则的内容。vLLM 提供了强大的结构化输出功能,可以约束模型的输出符合预定义的格式,确保输出数据的可靠性和可用性。

为什么需要结构化输出

传统的大模型输出是自由文本,存在以下问题:

格式不确定:同样的请求可能产生不同格式的输出,难以解析和处理

解析失败:期望 JSON 输出时,可能得到不合法的 JSON,导致程序崩溃

字段缺失:期望包含特定字段的输出,实际可能缺少某些字段

类型错误:期望数值类型,实际可能得到字符串

结构化输出通过约束模型的采样过程,确保输出始终符合指定的格式规范。

支持的输出类型

vLLM 支持 xgrammar 和 guidance 作为结构化输出的后端,提供以下几种约束类型:

类型说明适用场景
choice从预定义选项中选择一个分类任务、选择题
regex符合正则表达式模式特定格式的字符串(邮箱、电话等)
json符合 JSON Schema结构化数据、API 响应
grammar符合上下文无关语法复杂语言结构(SQL、代码片段等)
structural_tag在指定标签内遵循 JSON Schema混合格式输出

在线服务中使用结构化输出

Choice:选项选择

最简单的结构化输出是从预定义的选项中选择一个:

from openai import OpenAI

client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed"
)

# 情感分类:输出只能是 "positive" 或 "negative"
response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{"role": "user", "content": "请对以下文本进行情感分类:这个产品太棒了,强烈推荐!"}
],
extra_body={
"structured_outputs": {
"choice": ["positive", "negative", "neutral"]
}
}
)

print(response.choices[0].message.content)
# 输出一定是 "positive"、"negative" 或 "neutral" 之一

这个功能特别适合分类任务,确保输出始终是有效的类别标签。

Regex:正则表达式约束

使用正则表达式约束输出格式:

# 生成符合邮箱格式的输出
response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{
"role": "user",
"content": "为张三生成一个示例邮箱地址,公司是 example。"
}
],
extra_body={
"structured_outputs": {
"regex": r"\w+@example\.com"
}
}
)

print(response.choices[0].message.content)
# 输出类似:[email protected]

正则表达式语法注意:不同的后端支持的正则语法略有不同。xgrammar 和 guidance 使用 Rust 风格正则,而 lm-format-enforcer 使用 Python 的 re 模块语法。

JSON Schema:JSON 格式约束

JSON Schema 是最常用的结构化输出方式,特别适合生成结构化数据。有两种定义方式:

方式一:使用 response_format 参数

from openai import OpenAI

client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed"
)

# 定义 JSON Schema
response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{
"role": "user",
"content": "生成一个关于经典汽车的信息,包含品牌、型号和类型。"
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "car-info",
"schema": {
"type": "object",
"properties": {
"brand": {"type": "string", "description": "汽车品牌"},
"model": {"type": "string", "description": "汽车型号"},
"car_type": {
"type": "string",
"enum": ["sedan", "SUV", "truck", "coupe"],
"description": "汽车类型"
}
},
"required": ["brand", "model", "car_type"]
}
}
}
)

import json
result = json.loads(response.choices[0].message.content)
print(f"品牌: {result['brand']}")
print(f"型号: {result['model']}")
print(f"类型: {result['car_type']}")

方式二:使用 Pydantic 模型(推荐)

Pydantic 是 Python 中定义数据模型的流行库,使用它定义 Schema 更加直观:

from openai import OpenAI
from pydantic import BaseModel
from enum import Enum
import json

# 定义枚举类型
class CarType(str, Enum):
sedan = "sedan"
suv = "SUV"
truck = "truck"
coupe = "coupe"

# 使用 Pydantic 定义数据模型
class CarDescription(BaseModel):
brand: str
model: str
car_type: CarType
year: int

client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed"
)

# 直接使用 Pydantic 模型的 JSON Schema
response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{
"role": "user",
"content": "生成一辆 90 年代最具代表性的汽车信息。"
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "car-description",
"schema": CarDescription.model_json_schema()
}
}
)

# 解析结果
car = CarDescription.model_validate_json(response.choices[0].message.content)
print(f"品牌: {car.brand}")
print(f"型号: {car.model}")
print(f"类型: {car.car_type.value}")
print(f"年份: {car.year}")

复杂嵌套结构

Pydantic 支持嵌套模型,可以定义复杂的数据结构:

from pydantic import BaseModel
from typing import List
from openai import OpenAI

# 定义嵌套的数据结构
class Step(BaseModel):
"""数学解题步骤"""
explanation: str
output: str

class MathSolution(BaseModel):
"""数学问题的完整解答"""
steps: List[Step]
final_answer: str

client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed"
)

response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{
"role": "system",
"content": "你是一个专业的数学老师,请逐步解答问题。"
},
{
"role": "user",
"content": "解方程:2x + 5 = 13"
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "math-solution",
"schema": MathSolution.model_json_schema()
}
}
)

solution = MathSolution.model_validate_json(response.choices[0].message.content)

print("解题步骤:")
for i, step in enumerate(solution.steps, 1):
print(f"步骤 {i}: {step.explanation}")
print(f" {step.output}")
print(f"最终答案: {solution.final_answer}")

Grammar:上下文无关语法

对于更复杂的格式需求,可以使用上下文无关语法(EBNF 格式):

# 定义简化的 SQL 查询语法
sql_grammar = """
root ::= select_statement
select_statement ::= "SELECT " column " FROM " table " WHERE " condition
column ::= "name " | "age " | "email "
table ::= "users " | "customers "
condition ::= column operator value
operator ::= "= " | "> " | "< "
value ::= "'Alice' " | "'Bob' " | "25 " | "30 "
"""

response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{
"role": "user",
"content": "生成一个查询用户表中年龄大于 25 岁的 SQL 语句。"
}
],
extra_body={
"structured_outputs": {
"grammar": sql_grammar
}
}
)

print(response.choices[0].message.content)
# 输出类似:SELECT name FROM users WHERE age > 25

语法规则说明:

  • root 是起始规则
  • ::= 定义规则
  • | 表示选择
  • 引号内的内容是字面量

离线推理中使用结构化输出

离线推理同样支持结构化输出,使用 StructuredOutputsParams 类配置:

from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams
from pydantic import BaseModel
from typing import List

# 定义数据模型
class Person(BaseModel):
name: str
age: int
hobbies: List[str]

# 初始化模型
llm = LLM(model="meta-llama/Llama-2-7b-chat-hf")

# 配置结构化输出
structured_outputs = StructuredOutputsParams(
json=Person.model_json_schema()
)

sampling_params = SamplingParams(
structured_outputs=structured_outputs,
max_tokens=200
)

# 生成
outputs = llm.generate(
["生成一个虚拟人物的信息"],
sampling_params
)

import json
result = json.loads(outputs[0].outputs[0].text)
print(f"姓名: {result['name']}")
print(f"年龄: {result['age']}")
print(f"爱好: {', '.join(result['hobbies'])}")

使用 Choice 约束

from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams

llm = LLM(model="meta-llama/Llama-2-7b-chat-hf")

# 配置选项约束
structured_outputs = StructuredOutputsParams(
choice=["正面", "负面", "中性"]
)

sampling_params = SamplingParams(
structured_outputs=structured_outputs,
max_tokens=10
)

prompts = [
"情感分析:这个产品质量很好,物流也快!",
"情感分析:太失望了,和描述完全不符。",
"情感分析:收到货了,还没拆。"
]

outputs = llm.generate(prompts, sampling_params)

for prompt, output in zip(prompts, outputs):
sentiment = output.outputs[0].text.strip()
print(f"{prompt} -> {sentiment}")

自动解析(实验性功能)

OpenAI 客户端提供了 beta.chat.completions.parse() 方法,可以自动将响应解析为 Pydantic 模型:

from openai import OpenAI
from pydantic import BaseModel

class PersonInfo(BaseModel):
name: str
age: int
occupation: str

client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="not-needed"
)

# 使用 parse 方法,自动解析为 Pydantic 模型
completion = client.beta.chat.completions.parse(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{"role": "user", "content": "我叫张三,今年28岁,是一名软件工程师。请提取我的个人信息。"}
],
response_format=PersonInfo
)

# 直接访问解析后的对象
message = completion.choices[0].message
if message.parsed:
print(f"姓名: {message.parsed.name}")
print(f"年龄: {message.parsed.age}")
print(f"职业: {message.parsed.occupation}")

这个方法简化了 JSON 解析的过程,但需要注意这是 OpenAI 客户端的 beta 功能。

配置后端

vLLM 支持多种结构化输出后端:

# 启动服务时指定后端
vllm serve meta-llama/Llama-2-7b-chat-hf \
--structured-outputs-config.backend xgrammar

# 可选后端:auto(默认)、xgrammar、guidance、outlines、lm-format-enforcer

后端对比

后端特点适用场景
auto自动选择合适的后端默认选择
xgrammar性能好,支持 Rust 正则推荐
guidance灵活,支持复杂约束高级场景
outlines简单易用基础场景
lm-format-enforcer兼容 Python re 模块需要精确 Python 正则

最佳实践

提示词中包含 Schema 信息

虽然不是必须的,但在提示词中说明期望的输出格式可以显著提高结果质量:

response = client.chat.completions.create(
model="meta-llama/Llama-2-7b-chat-hf",
messages=[
{
"role": "user",
"content": """
请提取以下文本中的人物信息,返回 JSON 格式,包含以下字段:
- name: 姓名(字符串)
- age: 年龄(整数)
- occupation: 职业(字符串)

文本:李明今年32岁,是一名医生,在北京工作。
"""
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "person",
"schema": PersonInfo.model_json_schema()
}
}
)

处理解析错误

虽然结构化输出能大幅减少格式错误,但仍建议添加错误处理:

import json
from pydantic import ValidationError

try:
result = PersonInfo.model_validate_json(response.choices[0].message.content)
except json.JSONDecodeError as e:
print(f"JSON 解析失败: {e}")
except ValidationError as e:
print(f"数据验证失败: {e}")

合理设置 max_tokens

结构化输出可能需要更多 token 来完成格式化,建议适当增加 max_tokens

sampling_params = SamplingParams(
structured_outputs=structured_outputs,
max_tokens=500 # 给足够的生成空间
)

注意事项

性能影响:结构化输出会约束采样空间,可能略微影响生成速度。

复杂度限制:过于复杂的 JSON Schema 或语法规则可能导致处理时间增加。

字段说明:在 Schema 中添加 description 字段可以帮助模型理解字段含义。

必需字段:确保 Schema 中的 required 字段正确设置,否则可能生成缺少字段的输出。

小结

结构化输出是 vLLM 的重要功能,可以帮助开发者:

  1. 保证输出格式:确保输出符合预期的数据结构
  2. 简化后处理:减少解析和验证代码
  3. 提高可靠性:避免因格式错误导致的程序崩溃
  4. 增强可控性:精确控制输出的内容和格式

在实际应用中,推荐优先使用 Pydantic 模型定义数据结构,结合 response_format 参数实现结构化输出。

参考资料