让AI返回一段可直接解析的JSON,看起来是最简单的需求,实际做起来却经常翻车。明明提示词里写了只输出JSON,模型还是会在前面加上一段说明文字;明明给了字段清单,返回结果里却多出几个自作主张的新字段;或者数组明明该有十个元素,模型只输出了三个就开始收尾。这些问题的根源在于,自然语言模型默认的目标是生成通顺的话,而不是生成严格符合语法约束的数据。想要稳定的JSON输出,就得在提示词里把模糊的自然语言要求,换成明确的、可执行的规则。下面从原理、写法和容错三个层面,把这件事讲透。

为什么AI输出的JSON总是不稳定
要解决问题,先得理解问题的来源。大模型的生成过程是逐个token预测下一个token,它本质上是一个概率系统,而不是一个按规则执行的解析器。当你在提示词里写“请以JSON格式返回”时,模型理解的是一种倾向性建议,而不是硬性约束。输出JSON的稳定性,取决于提示词把这种倾向性强化到什么程度。
常见的不稳定表现有五类。第一类是夹带说明文字,比如返回“好的,以下是您要的JSON:”再接数据,直接导致json.loads抛出异常。第二类是字段名漂移,这次叫name,下次叫product_name,程序按固定key取值时直接拿到空。第三类是数据类型不稳定,价格这次是数字59.9,下次变成字符串"59.9元",入库或者计算时出错。第四类是截断,字段一多或者数据一长,模型在没写完的情况下就开始输出结尾的说明。第五类是嵌套结构随意变动,该是数组的地方返回了对象,多层的结构一变,解析代码全盘失效。
还有一个容易被忽视的原因是模型对“格式”的理解是训练出来的统计规律,而不是语法校验。如果提示词里描述格式用的语言太口语化,模型会按照自己见过的类似文本去猜结构,猜对了是运气,猜错了是常态。所以稳定输出的核心思路就一句话:不要让模型猜,把结构完整地喂给它。
一套可直接套用的提示词写法
经过大量实践验证,一套稳定的JSON输出提示词通常包含五个组成部分:角色与任务说明、完整的Schema定义、一个完整示例、禁止事项、以及对模糊输入的处理规则。下面给一个可以直接复用的模板。
你是一个数据提取助手。你的唯一任务是提取文本中的商品信息,
并以JSON格式输出。除JSON外,不要输出任何其他内容。
输出Schema(严格遵守):
{
"name": string, // 商品名称
"price": number, // 价格,纯数字,不带货币符号和单位
"category": string, // 只能是以下之一:"食品"、"数码"、"服饰"、"家居"
"tags": string[], // 标签数组,1到5个,每个不超过10个字
"in_stock": boolean // 是否有货
}
示例:
输入:苹果笔记本,售价8999元,数码类,现货,轻薄便携
输出:{"name":"苹果笔记本","price":8999,"category":"数码","tags":["轻薄","便携"],"in_stock":true}
规则:
1. 如果原文没有提到某字段,string填null,数组填[],boolean填false
2. price必须是数字类型,不要加引号,不要带单位
3. 直接输出JSON本体,不要用markdown代码块包裹,不要加任何解释
待处理文本:{用户输入}这个模板里有几个关键细节值得展开。首先是Schema里的注释,用行内注释说明每个字段的含义和约束,模型会参照这些约束生成,比单独一段文字描述有效得多。其次是示例必须完整且只有一个,示例过多时模型容易混合不同示例的特征,反而增加变数。再者是禁止事项要具体,“不要用markdown代码块包裹”这一条尤其重要,很多模型习惯输出三个反引号包裹的代码块,这三个反引号会让直接取响应文本做解析的代码报错。
关于数组长度的约束也要写清楚。如果你要求“列出所有标签”,模型可能给三个也可能给十个,上限不受控。写成“1到5个”这种区间式约束,配合“每个不超过10个字”这样的量化限制,输出会收敛很多。数字字段一定要强调“纯数字,不带单位”,这是类型漂移的重灾区,特别是价格、百分比、数量这类字段。
另外提一下转义问题。如果输出内容可能包含双引号、换行符,要在提示词中要求模型正确转义,或者干脆在Schema里规定这些字段不能包含双引号。JSON字符串中的换行必须是\n而不是真实换行,这一点模型偶尔会犯错,对内容可控性要求高的场景,最好在源头就限制住。
解析端容错与稳定性兜底方案
提示词再严谨,也不能保证百分之百的输出合规,工程上的成熟做法是提示词约束加解析容错双保险。第一步是清洗,把响应里可能混入的前后缀文字剥掉。常用的技巧是从第一个左花括号截取到最后一个右花括号,这一步能解决大部分夹带说明文字的问题。
import json, re
def extract_json(text):
# 去掉markdown代码块标记
text = text.replace("```json", "").replace("```", "")
# 截取第一个{到最后一个}之间的内容
match = re.search(r"\{.*\}", text, re.DOTALL)
if not match:
raise ValueError("响应中未找到JSON内容")
return json.loads(match.group())
def safe_extract(text, schema_keys):
try:
data = extract_json(text)
except (json.JSONDecodeError, ValueError):
return None
# 校验必需字段是否存在
for key in schema_keys:
if key not in data:
return None
return data第二步是字段校验与类型修正。拿到JSON对象后,不要直接信任它的类型,用类似上面的safe_extract做一层校验,必需字段缺失就判为失败。对price这类字段,可以再做一次float()尝试转换,转换失败就按错误处理。很多团队会用Pydantic这类库定义数据模型来校验,比手写校验逻辑更省事,字段缺失、类型错误都能一次性暴露出来。
第三步是失败重试策略。解析失败时不要直接放弃,可以把失败原因拼进重试提示词再请求一次,比如“你上次输出的JSON无法解析,缺少price字段,请严格按Schema重新输出”。模型看到具体的错误描述,修正成功率远高于盲目重试。一般设置两到三次重试上限,超过就降级到人工处理或默认值兜底,避免无限循环消耗token。
最后还有一个结构性建议:能拆就拆。与其让模型一次输出一个几十个字段的巨型JSON,不如拆成多次小任务,每次输出五到十个字段的小对象再在程序侧合并。输出结构越简单,模型出错概率越低,单次失败重试的成本也越小。这是一个用流程设计换稳定性的典型取舍,实践下来往往比死磕一条超长提示词更划算。
总结一下,让AI稳定输出JSON的要点是三层:用完整的Schema加示例消除模型的猜测空间,用明确的禁止事项堵住夹带文本和代码块的漏洞,用解析端的清洗校验和重试机制兜住最后的意外。三层都做到位,JSON输出的稳定性可以达到工程可用的水平,放心接入自动化流程。