要让大语言模型稳定输出JSON,很多开发者容易掉进一个陷阱:完全依赖提示工程,比如简单地在提示词里写"输出JSON"或"不要添加额外文字"。但在生产环境中,这种方式并不可靠。要构建一个健壮的解决方案,我们需要一个多层策略——结合提示工程、模型原生能力、底层约束和回退机制。下面用实战步骤和代码示例逐一讲解每一层。
1. 带少样本学习的提示工程
第一层是使用精心设计的提示词来引导LLM,特别是利用少样本学习。不要使用模糊的指令,而是提供输入和预期JSON输出的具体示例,让模型照猫画虎。
例如,如果你想从聊天消息中提取用户的姓名和年龄,可以这样构建提示词:
任务:从以下聊天消息中提取姓名和年龄,以JSON格式输出。
输入示例:小明今年十八岁
输出示例:{"name": "小明", "age": 18}
当前输入:你好,我是张三,今年25岁
预期输出:
通过向模型展示清晰的输入输出对,它就会学习模仿所需的JSON结构,大大减少输出无关文本或格式出错的概率。
2. 利用原生LLM能力
现代LLM大多内置了强制执行结构化输出的功能。两种最实用的方法是JSON模式和函数调用。
JSON模式
许多LLM API(比如OpenAI的API)提供了 response_format 参数。你只需要把它设为 { "type": "json_object" },模型就会优先考虑输出JSON格式。
下面是使用Python和OpenAI API的完整示例:
import openai
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "从'李四今年30岁'中提取姓名和年龄,返回JSON格式。"}
],
response_format={"type": "json_object"}
)
print(response.choices[0].message.content)
函数调用
函数调用则更进一步:你可以为JSON输出定义一个明确的模式(Schema),LLM生成的JSON会严格遵循这个模式,不会跑偏。
下面是一个提取用户信息的函数模式定义:
functions = [
{
"name": "extract_user_info",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}
}
]
response = openai.ChatCompletion.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "我叫王五,今年40岁"}
],
functions=functions,
function_call={"name": "extract_user_info"}
)
# 解析函数调用响应
function_response = response.choices[0].message.function_call.arguments
print(function_response)
3. 本地模型的底层约束
如果你是在本地运行LLM,还可以在Token生成这一底层做文章。通过监控模型逐个输出的Token,实时拦截那些会破坏JSON语法的Token。
用Python的 transformers 库,你可以实现一个Token级别的过滤器:
from transformers import AutoTokenizer, AutoModelForCausalLM
import re
tokenizer = AutoTokenizer.from_pretrained("your-local-model")
model = AutoModelForCausalLM.from_pretrained("your-local-model")
def is_valid_json_token(token):
# 检查该Token是否为有效JSON语法的一部分
json_patterns = [r'^\{.*', r'^\}.*', r'^".*"', r'^[0-9].*', r'^,.*', r'^:.*', r'^\[.*', r'^\].*']
for pattern in json_patterns:
if re.match(pattern, token):
return True
return False
# 使用Token过滤生成文本
input_ids = tokenizer("从'赵六今年28岁'中提取姓名和年龄,输出JSON格式:", return_tensors="pt")
output = model.generate(
input_ids,
max_length=100,
pad_token_id=tokenizer.eos_token_id,
bad_words_ids=[[tokenizer.encode(token)[0]] for token in tokenizer.vocab if not is_valid_json_token(tokenizer.decode([token]))]
)
print(tokenizer.decode(output[0], skip_special_tokens=True))
这段代码的核心思路是:只允许那些能构成有效JSON的Token通过,其余的全部拦截,从源头上保证输出格式正确。
4. 错误处理的回退机制
即使前面三层都用上了,错误还是有可能出现。这时候就需要一个兜底的回退机制:先验证输出是不是合法JSON,如果不是,就让LLM自己修正。
import json
def validate_and_retry(response_text, model, tokenizer):
try:
json.loads(response_text)
return response_text
except json.JSONDecodeError as e:
# 将错误返回给LLM进行修正
error_message = f"无效JSON:{str(e)}。请修复JSON并重试。"
correction_response = openai.ChatCompletion.create(
model="gpt-4o",
messages=[
{"role": "user", "content": error_message + "
原始输出:" + response_text}
],
response_format={"type": "json_object"}
)
return correction_response.choices[0].message.content
这个函数先用 json.loads 尝试解析。如果抛异常了,就把错误信息塞回给LLM,让它基于原来的输出重新生成一个合法的JSON。大多数情况下,LLM看到自己的错误后都能正确修正。
结论
通过结合这四层——带少样本示例的提示工程、JSON模式和函数调用等原生LLM特性、本地模型的底层Token约束以及错误处理的回退机制——你可以在任何环境下确保LLM稳定输出JSON。这种多层方法不仅对生产环境非常实用,也能体现出扎实的工程思维,在技术面试和实际开发中都是加分项。
对于使用云端LLM API的开发者来说,第三层(本地模型的底层约束)用不了,因为你无法控制服务端Token的生成过程。这也意味着前面三层更加关键,必须做得足够扎实才能保证可靠性。