十万个why:为什么让大模型输出 JSON 不难,但是稳定输出JSON这么难?
大家好,我是小富~
《十万个why》系列持续更新~
其实让大模型输出一个 JSON 很简单,Prompt 里写一句"请以 JSON 格式输出",大多数模型就能给你吐出来。
但你真正在项目里用过就知道,能输出和稳定输出,完全不是一个难度级别的事。
我刚开始做 LLM 相关开发的时候,第一反应就是疯狂改 Prompt,各种花式叮嘱:
严格按照 JSON 输出。
不要输出任何解释。
不要使用 Markdown。
这些写法有没有用?有用。但本质上你只是在引导模型,并不是在约束模型。模型仍然随时可能给你整出花活:
- 前面加一句"好的,下面是结果:"
- 用
```json ```包一层 Markdown 代码块 - 少字段、多字段
- 字段类型不对,该给数字给了字符串
- JSON 格式本身就有语法错误,直接解析失败
测试环境跑 50 条数据全部完美,上线后结果生产环境每隔几个小时就蹦一个 JSONParseException。
Prompt 可以提高成功率,但永远不能保证稳定性!!!
要控制模型稳定输出,必须多管齐下~
Prompt 把输出结构描述清楚
太多人写 Prompt 的时候太随意了,就丢一句:
返回姓名、年龄、地址。
这种描述模糊得一批,模型看到以后全靠自由发挥。它可能返回 {"姓名": "张三"},也可能返回 {"name": "张三"},你根本无法预期。
更好的做法是直接在 Prompt 里给出目标 JSON 结构,例如:
{
"name": "",
"age": 0,
"address": ""
}
或者用更清晰的字段描述:
name: string,用户姓名
age: integer,用户年龄
address: string,用户地址
模型有了明确的参考模板,输出通常会稳定不少。我一般还会补一句狠话:
只输出纯 JSON 文本,不要输出任何解释文字、Markdown 标记或代码块。
但是说实话,Prompt 写得再严格,该翻车还是会翻车。这就好比你跟同事交代工作,哪怕说了三遍"别加注释",他还是可能顺手写两行注释上去。毕竟大模型的底层是概率生成,不是指令执行。
优先使用模型原生的结构化输出能力
如果项目要投入生产,千万不要只依赖 Prompt。
现在主流的大模型基本都提供了结构化输出的能力,常见的有三种:
- Structured Output(结构化输出)
- JSON Schema
- Function Calling(也叫 Tool Calling)
这三种方案的共同点是:不是在语义层面上"劝"模型遵守格式,而是在生成阶段就按照你指定的 Schema 进行硬约束。
举个例子,你定义了这样一个 Schema:
{
"score": {
"type": "integer"
}
}
在 Structured Output 模式下,模型一般不会返回:
{
"score": "90"
}
因为字段类型在底层就被锁死了,它想输出字符串都输出不了。
Structured Output 和 Function Calling 怎么选?
其实很简单,看你的场景:
如果你只是想让模型返回固定的数据结构,比如从一段文本里提取几个字段,Structured Output 通常是首选。你给它一个 JSON Schema,它严格按照这个 Schema 生成,干净利落。
如果后续还要调用数据库、搜索接口、天气 API 等外部能力,那 Function Calling 更合适。因为它不仅规范了参数格式,还能直接驱动工具调用。模型在 Function Calling 模式下会意识到自己是在"填写参数表",而不是在跟人聊天,输出的规范性会好很多。
以 OpenAI 的接口为例,用 Function Calling 大概长这样:
tools = [{
"type": "function",
"function": {
"name": "extract_user_info",
"description": "从文本中提取用户信息",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "用户姓名"},
"age": {"type": "integer", "description": "用户年龄"},
"address": {"type": "string", "description": "用户地址"}
},
"required": ["name", "age", "address"]
}
}
}]
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "张三,28岁,住在北京市朝阳区"}],
tools=tools,
tool_choice={"type": "function", "function": {"name": "extract_user_info"}}
)
后端拿到的直接就是一个标准的函数调用对象,不是一段带有 Markdown 包裹的文本,用 Java 或者 Go 直接反序列化就完事了。
能用模型原生能力约束的,就别靠 Prompt 去求模型。,这条原则在 AI 工程化落地中怎么强调都不为过。
不要让模型一次干太多活
这是一个特别容易被忽略的坑。
很多人写 Prompt 的时候,恨不得把所有事情一股脑塞进去:
阅读下面这篇文章 → 总结核心观点 → 提取关键词 → 判断情绪倾向 → 翻译成英文 → 最后输出 JSON。
任务链越长,模型越容易在中间某个环节跑偏,到最后输出的 JSON 就开始变形。要么丢字段,要么格式错乱,甚至直接变成一段自然语言的"总结报告"。
模型不是流水线工人,它是一个概率生成器。你给它的任务越复杂,它需要同时兼顾的约束就越多,出错概率就越高。
实际开发中,我更倾向于拆成多个步骤:
第一步:内容理解
→ 输入原始文章,让模型做摘要和关键词提取,输出自然语言即可
第二步:结构化输出
→ 把第一步的结果喂给模型,让它严格按照 JSON Schema 输出结构化数据
虽然多调用了一次模型,多花了几分钱 Token 费,但整体稳定性通常会显著提高,而且出了问题也更容易定位——是第一步理解错了,还是第二步格式化出了问题,一目了然。
这跟写代码是一个道理:一个函数只干一件事,永远比一个函数干五件事更可靠。
程序一定要做校验和兜底
永远不要假设模型一定会返回合法 JSON!!!
哪怕你已经用了 Structured Output,哪怕你已经用了 Function Calling,程序也必须做好兜底。原因很简单:线上环境什么妖蛾子都可能出——模型版本升级、接口超时返回了半截响应、网络抖动导致 JSON 被截断……
一个成熟的 AI 应用,对模型的输出至少要做以下校验:
import json
def validate_model_output(raw_output: str, required_fields: list) -> dict:
# 1. JSON 是否能解析
try:
data = json.loads(raw_output)
except json.JSONDecodeError:
# 尝试修复常见问题:去掉 Markdown 代码块包裹
cleaned = raw_output.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[-1].rsplit("```", 1)[0]
try:
data = json.loads(cleaned)
except json.JSONDecodeError:
raise ValueError("模型输出无法解析为 JSON")
# 2. 必填字段是否缺失
for field in required_fields:
if field not in data:
raise ValueError(f"缺少必填字段: {field}")
# 3. 字段类型是否正确(根据业务定义校验)
# 4. 枚举值是否合法
# 5. 是否需要填充默认值
return data
除了校验,还有一个非常重要的机制:自动重试。
模型输出有一定的随机性,这次格式不对,重新调一次可能就对了。给关键链路加一个 2~3 次的重试机制,配合指数退避,能覆盖掉绝大多数偶发性的格式异常。
import time
def call_with_retry(func, max_retries=3):
for attempt in range(max_retries):
try:
result = func()
return validate_model_output(result, ["name", "age"])
except (ValueError, KeyError) as e:
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt) # 指数退避:1s, 2s, 4s
想做一个能跑在线上的 AI 应用,不能假想模型特别可靠,必须保证应用能处理各种异常情况。 这跟你调第三方接口是一个道理。你不会裸调支付宝的接口不加任何异常处理吧?对待模型的输出,也该有同样的防御心态。
说在最后
总结一下,目前比较成熟的一套做法其实就是四层防线:
Prompt 明确描述输出结构,减少模型的理解偏差。
优先使用 Structured Output 或 Function Calling,在生成层面做硬约束,而不是只靠 Prompt 软约束。
拆分复杂任务,让模型一次只专注一件事,降低出错概率。
程序侧做校验和兜底,对解析失败、字段异常等情况做好重试和容错。
一般刚接触大模型开发的人,会把大量时间花在反复修改 Prompt 上,希望把成功率从 90% 提高到 99%。但真做过线上应用就会发现,Prompt 只是第一步。
真正决定稳定性的,是模型原生的结构化输出能力,加上程序侧的校验和异常处理。只有这种配合起来,才能把 JSON 输出真正做到生产级别的稳定可靠。
做 AI 应用和做传统后端最大的区别就是:传统代码的输出是确定性的,而模型的输出永远是概率性的。
接受这个现实,用工程手段去兜住概率的不确定性,这是现在 AI 应用开发的现实与常态。
