适合人群:在程序中调用大模型,需要把结果交给后续代码处理,却经常遇到"模型多说了一句话导致 JSON 解析失败"的开发新手。
一、背景
X 上有中文开发者分享:让大模型"吐 JSON",模型要一个 token 一个 token 地生成,既慢又容易出格式问题。在实际开发中,这个问题非常普遍:模型返回的内容前面多了一句"好的,以下是结果",或者用 Markdown 代码块包裹,或者漏了一个引号,程序就直接报错。
本文整理三种由浅入深的方法,让模型输出稳定可用的 JSON,并用 Python 做校验和兜底。
二、为什么模型会输出"坏 JSON"
大模型的本质是逐个预测下一个 token,它并不"知道"自己在写 JSON,只是根据训练经验模仿格式。因此:
- 提示词不够明确时,它会按聊天习惯加上开场白;
- 输出很长时,容易在中途出现格式错误;
- 字段名、类型可能和你期望的不一致。
解决思路有两类:在提示词层面引导 ,以及在解码层面约束。
三、操作步骤
方法一:提示词约束 + 代码兜底(适用于所有模型)
步骤 1:在提示词中给出明确格式
请从下面的商品评论中提取信息,只输出 JSON,不要输出任何其他文字。
格式:
{"sentiment": "positive|negative|neutral", "topics": ["字符串"], "score": 1到5的整数}
评论:物流很快,但包装有点破损,客服处理还不错。
要点:写清字段名、类型和取值范围,并明确"只输出 JSON"。
步骤 2:解析时做清理
python
import json, re
def parse_json(text):
text = text.strip()
m = re.search(r"```(?:json)?\s*(.*?)```", text, re.S)
if m:
text = m.group(1)
start, end = text.find("{"), text.rfind("}")
return json.loads(text[start:end + 1])
这段代码会去掉 Markdown 代码块,并截取第一个 { 到最后一个 } 之间的内容。
方法二:使用 API 的 JSON 模式
许多模型服务提供了专门的结构化输出参数。以 OpenAI 兼容接口为例,可以设置 response_format:
python
from openai import OpenAI
client = OpenAI() # 密钥从环境变量读取
resp = client.chat.completions.create(
model="你使用的模型名称",
messages=[
{"role": "system", "content": "你只输出 JSON。"},
{"role": "user", "content": "提取评论信息......"},
],
response_format={"type": "json_object"},
)
print(resp.choices[0].message.content)
JSON 模式能保证输出是合法 JSON,但不保证字段符合你的预期。部分服务还支持通过 JSON Schema 指定精确结构(Structured Outputs),能进一步约束字段和类型。具体参数名称和支持情况,以所用服务的官方文档为准。
本地模型服务也有类似能力,例如 Ollama 的接口支持 format 参数,可以传入 "json" 或一个 JSON Schema。
方法三:用 Pydantic 定义结构并校验
无论使用哪种方法,都建议在代码里做一次结构校验:
python
from pydantic import BaseModel, Field, ValidationError
from typing import Literal
class Review(BaseModel):
sentiment: Literal["positive", "negative", "neutral"]
topics: list[str]
score: int = Field(ge=1, le=5)
try:
data = Review.model_validate(parse_json(model_output))
except (ValidationError, json.JSONDecodeError) as e:
print("格式不符合要求:", e)
还可以用 Review.model_json_schema() 生成 JSON Schema,直接放进提示词或传给支持 Schema 的接口,做到"一处定义、两处使用"。
步骤补充:失败重试
校验失败时,可以把错误信息反馈给模型,让它修正:
python
def ask_with_retry(prompt, max_retry=2):
msg = prompt
for _ in range(max_retry + 1):
out = call_llm(msg) # 你自己的模型调用函数
try:
return Review.model_validate(parse_json(out))
except Exception as e:
msg = f"{prompt}\n\n上一次的输出不符合格式,错误:{e}\n请只输出修正后的 JSON。"
raise RuntimeError("多次重试后仍然失败")
设置重试上限,避免无限调用。
四、方法对比与选择
| 方法 | 优点 | 局限 |
|---|---|---|
| 提示词 + 清理 | 通用,任何模型可用 | 无法完全保证格式 |
| JSON 模式 / Schema | 格式可靠性高 | 依赖服务支持 |
| Pydantic 校验 + 重试 | 保证进入业务代码的数据正确 | 失败时需要额外调用 |
实际项目中建议组合使用:能用 Schema 约束就用,再加 Pydantic 校验和有限次重试。
另外,如果任务只是从几个固定选项中选一个(例如情感分类),其实不必生成 JSON,直接比较候选选项的概率会更快、更稳定。
五、常见问题 FAQ
Q1:设置了 JSON 模式还是报错?
有的服务要求在提示词中明确出现"JSON"字样,否则会报错或表现异常。另外检查输出是否因长度限制被截断。
Q2:中文被转成了 \u 编码?
这是 json.dumps 的默认行为,加上 ensure_ascii=False 即可正常显示中文。
Q3:字段偶尔缺失怎么办?
在 Pydantic 模型中区分必填和可选字段,可选字段设置默认值;必填字段缺失时触发重试。
Q4:输出很长的列表时容易出错?
把任务拆小,每次只处理少量数据,再在代码中合并结果。
Q5:小模型适合做结构化输出吗?
小模型遵循复杂格式的能力较弱,建议结构尽量扁平、字段尽量少,并配合解码约束和校验。
六、实用技巧
- 字段名用英文:减少模型自由发挥的空间,也方便代码处理;
- 枚举值写全 :用
Literal或在提示词中列出所有允许的取值; - 温度设低:结构化任务通常不需要创造性,较低的温度更稳定;
- 记录失败样本:分析失败原因,持续改进提示词;
- 不要信任未经校验的数据:模型输出进入数据库前,必须经过校验。
七、总结
让大模型稳定输出 JSON 的关键是"三层保障":提示词写清格式 → 接口层使用 JSON 模式或 Schema 约束 → 代码层用 Pydantic 校验并有限次重试。做好这三步,模型输出就能可靠地接入你的程序。
参考资料
- OpenAI 官方文档:Structured Outputs 章节
- Pydantic 官方文档:模型与校验章节
- Ollama 官方文档:结构化输出说明
- X 用户 @ataosky 关于大模型输出 JSON 与小模型限定选项的分享