如何让 LLM 稳定地返回结构化输出
前言
目前,LLM 的输出都是纯文本,但是有的时候我们希望大模型能够输出结构化的结果,特别是 JSON 结果,方便我们在后续的步骤中使用.
这里,我们介绍一下 vllm 的支持,以及 openai 接口的支持.
vllm 的支持
vllm 支持以下配置:
guided_choice
:模型输出为指定选项之一.guided_regex
:模型输出满足指定的正则表达式.guided_json
:模型输出满足指定的 JSON Schema.guided_grammar
:模型的输出满足指定的语法规则.这个我们暂时不用.structural_tag
:暂时不用.有兴趣查询官方文档.
以上的这些配置都不在 openai 接口的参数里,一般都要通过 extra_body
参数来设置.
注意,你必须确定模型是用 vllm 服务启动的,才可以使用这些配置.此外,如果你的请求经过 new-api 等类似的 API 管理服务的中转,extra_body
参数可能不会被正确传递.
代码示例
下面我们用代码来举例,使用模型为:Qwen/Qwen2.5-32B-Instruct
,基础代码如下:
from enum import Enum
from typing import List
from openai import OpenAI
from openai.lib._pydantic import to_strict_json_schema
from pydantic import BaseModel
model = "Qwen/Qwen2.5-32B-Instruct"
base_url = "your-base-url"
api_key = "sk-your-key"
client = OpenAI(api_key=api_key, base_url=base_url)
guided_choice
guided_choice
测试,也就是要求模型输出的结果仅限于给定的选择.
completion = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "李白是唐代诗人吗?"}],
extra_body={"guided_choice": ["是", "否"]},
)
print(completion.choices[0].message.content)
如果一切正常,模型应该会输出"是".如果你无法控制你的请求经过哪些中间层的转发处理,甚至也无法确定模型是不是用 vllm 部署,则可能会有额外的输出内容.例如,我们把模型改成调用硅基流动的 deepseek-v3,则可能输出更多的信息,模型会对结果进行详细的解释.
guided_regex
也就是让模型输出满足正则表达式的结果.
completion = client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "帮我随机生成一个电子邮件地址.例如: [email protected]\n",
}
],
extra_body={"guided_regex": r"\w+@\w+\.com\n"},
)
print(completion.choices[0].message.content)
guided_json
这里是要求模型输出的内容要符合指定的 JSON Schema.其中 JSON Schema 可以自己全部手写,也可以从 pydantic.BaseModel
生成.这里我们演示的是从 pydantic.BaseModel
生成 JSON Schema 的方式.这通常是比较简单直接的做法.
class Step(BaseModel):
explanation: str
output: str
class MathResponse(BaseModel):
steps: list[Step]
final_answer: str
# pydantic basemodel 转换为 json schema
# 推荐使用后者
json_schema = MathResponse.model_json_schema()
# json_schema = to_strict_json_schema(MathResponse)
completion = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一个聪明的数学助手."},
{"role": "user", "content": "求解方程 8x + 31 = 2."},
],
extra_body=dict(guided_json=json_schema),
)
content = completion.choices[0].message.content
answer = json.loads(content)
这里输出的结果可以直接用 json.loads
反序列化为一个对象来使用.实际上这里等价于设置 response_format
为 json_schema
.等价代码如下:
class Step(BaseModel):
explanation: str
output: str
class MathResponse(BaseModel):
steps: list[Step]
final_answer: str
json_schema = to_strict_json_schema(MathResponse)
response_format = {
"type": "json_schema",
"json_schema": {
"schema": to_strict_json_schema(MathResponse),
"name": MathResponse.__name__,
"strict": True,
},
}
completion = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "你是一个聪明的数学助手."},
{"role": "user", "content": "求解方程 8x + 31 = 2."},
],
response_format=response_format,
)
content = completion.choices[0].message.content
answer = json.loads(content)
这样子写起来有些麻烦,openai-python 新增了一个接口,方便我们直接传入 pydantic.BaseModel
,他自己帮我们做好 JSON Schema 的生成,返回的结果也是一个对象,不用再自己做 JSON 的反序列化.等价代码如下:
class Step(BaseModel):
explanation: str
output: str
class MathResponse(BaseModel):
steps: list[Step]
final_answer: str
completion = client.beta.chat.completions.parse(
model=model,
messages=[
{"role": "system", "content": "你是一个聪明的数学助手."},
{"role": "user", "content": "求解方程 8x + 31 = 2."},
],
response_format=MathResponse,
)
# completion.choices[0].message.parsed 是一个 MathResponse 对象,可以直接使用
print(completion.choices[0].message.parsed)
这里返回的结果就直接是一个 MathResponse
,非常方便使用.
JSON mode
有的模型并不支持 JSON Schema,而是支持 JSON mode.例如 gpt-3.5-turbo 支持的就是 JSON mode.JSON mode 只是保证返回的内容为有效的 JSON 字符串,但是不保证它遵循预定的 JSON Schema.当然,你也可以在提示词中详细地描述需要生成的 JSON 数据的信息,确保得到更加可靠的 JSON 结果.
此外,你还要必须在提示词中显式要求模型返回 JSON 输出.
completion = client.chat.completions.create(
model=model,
messages=[
{
"role": "user",
"content": "介绍一下李白的生平,以JSON格式返回结果",
}
],
response_format={"type": "json_object"},
)
print(completion.choices[0].message.content)
例如上述例子,我们指定了 response_format={"type": "json_object"}
,返回的输出会是有效的 JSON 输出,但是并不确保其遵循特定的 JSON Schema.
好在现在多数模型都是支持的 JSON Schema,仅支持 JSON mode 的也较少使用了.
大一统
实际上,vllm 的 guided_choice
, guided_regex
, guided_json
都可以统一到 response_format
为 json_schema
中来.特别提一下,JSON Schema 里是可以写正则表达式的.openai 接口支持的 JSON Schema 规范可以参考 Supported schemas.
因此,考虑到中间的转发服务可能丢弃 extra_body
参数,最终的提供模型服务的也可能不是 vllm
,我们建议优先使用 response_format
为 json_schema
的方式来获得 LLM 的结构化输出.
总之,我们建议使用 response_format
为 json_schema
.
此外,尽管不是必须的,我们在提示词里明确要求模型返回 JSON 输出,并且描述 JSON 的各个字段如何填充数据,或者直接给出示例输出,往往能够得到更加稳定的结果.
总结
要让支持 JSON Schema 的 LLM 稳定地输出结构化的结果,特别是 JSON 输出,我们可以:
- 编写合适的提示词,要求模型返回 JSON 输出,并且描述输出的各个字段如何填充或者给出示例。
- 创建合适的
pydantic.BaseModel
,优先使用completion = client.beta.chat.completions.parse
来发起请求,注意设置response_format
为你的pydantic.BaseModel
,然后completion.choices[0].message.parsed
就是返回的满足pydantic.BaseModel
的对象。 - 处理异常情况,有的模型不能正确返回 JSON 输出,以上调用就会抛出异常。这个时候就需要我们捕获并且处理。