大模型结构化输出实战:3 招让 LLM 稳定返回 JSON,告别解析地狱(附完整代码)
导语:你是不是也遇到过这种情况------写好的 Prompt 让大模型返回 JSON,测试时好好的,一上线就开始抽风:多一个逗号、少一个引号、字段名拼错、甚至直接甩给你一句"好的,以下是结果:"外加一段废话。然后你的
json.loads()抛异常,线上告警疯狂响起。这不是你的 Prompt 写得不够好,而是你用错了方法。大模型本质上是"概率采样生成文本",让它天然保证 JSON 格式,就像让一个自由发挥的作家严格填写表格------你需要的不是更凶的叮嘱,而是**结构化输出(Structured Output)**的正确姿势。
一、为什么大模型输出 JSON 总是不稳定?
先搞清楚问题的根源。大模型(LLM)生成文本的本质,是逐 token 采样:
- 模型根据上下文,为下一个 token 计算出一个概率分布;
- 按这个分布随机采样出一个 token;
- 重复直到生成结束。
这里有两个关键因素在捣乱:
- 温度(temperature):温度越高,采样的随机性越大,输出越"放飞"。默认值通常大于 0,这正是输出不稳定的一大来源。
- 无约束生成:模型"说人话"是它的强项,但严格遵循某个 JSON Schema 并不是它的天性。你给的自由度越大,它偏离格式的概率就越高。
所以,靠堆 Prompt 里的"请务必返回合法 JSON,不要输出任何其他内容"这类道德劝说,永远只能是"大概率有效",无法做到"确定性有效"。
要根治,得从机制上约束输出。下面按"成本从低到高、稳定性从低到高"的顺序,介绍三种方法。
二、方法一:Prompt 约束(零成本,但不保底)
最朴素的做法,就是明确告诉模型输出格式,甚至给出一个示例(Few-shot):
text
你是一个订单信息抽取助手。请严格从用户文本中提取以下字段,并以 JSON 格式返回,
不要输出任何其他文字。
字段:product(商品名)、quantity(数量)、unit_price(单价)、customer(客户名)
输出示例:
{"product": "iPhone 15", "quantity": 3, "unit_price": 5999, "customer": "张三"}
优点 :无需额外配置,任何模型都适用。 缺点 :模型仍可能输出带 Markdown 代码块的 JSON(```````json````)、前后缀废话、或格式错误。你需要写一堆容错解析代码去"兜底",比如正则提取 {...}、ast.literal_eval 尝试等,既丑陋又脆弱。
结论 :适合快速原型和一次性脚本,不适合生产环境。
三、方法二:JSON Schema 结构化输出(主流推荐)
目前主流大模型 API(OpenAI、DeepSeek、通义千问 Qwen 等)都已支持结构化输出 能力,即:你把目标 JSON Schema 传给模型,模型保证返回的 JSON 100% 符合该 Schema。
核心做法是通过 response_format 指定 json_schema,并开启 strict 模式:
python
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY", "sk-xxx"),
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"),
)
# 1. 定义目标 JSON Schema
json_schema = {
"name": "extract_order",
"strict": True, # 开启严格模式,保证返回 100% 符合 Schema
"schema": {
"type": "object",
"properties": {
"product": {"type": "string"},
"quantity": {"type": "integer"},
"unit_price": {"type": "number"},
"customer": {"type": "string"},
},
"required": ["product", "quantity", "unit_price", "customer"],
"additionalProperties": False, # 禁止多余字段
},
}
# 2. 调用时指定结构化输出
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是订单信息抽取助手,从用户文本中抽取结构化字段。"},
{"role": "user", "content": "张三买了 3 台 iPhone 15,单价 5999 元。"},
],
response_format={"type": "json_schema", "json_schema": json_schema},
temperature=0, # 结构化输出建议低温,进一步降低随机性
)
# 3. 直接 json.loads,无需任何容错解析
result = json.loads(resp.choices[0].message.content)
print(json.dumps(result, ensure_ascii=False, indent=2))
输出:
json
{
"product": "iPhone 15",
"quantity": 3,
"unit_price": 5999,
"customer": "张三"
}
关键点:
strict: true是灵魂,它让模型在受约束的语法下生成,从而保证字段类型、必填项、禁止多余字段全部满足。temperature=0进一步降低随机性,让抽取类任务更稳定。- 生成后直接
json.loads,不必再写正则和try/except容错。
局限 :并非所有模型/版本都支持 strict 模式,接入前请确认你所用的模型文档。
四、方法三:Function Calling / 工具调用(最灵活的生产方案)
如果说方法二是"让模型填一张固定表格",那么 Function Calling 就是"让模型决定调用哪个工具、传什么参数 "。它的强大之处在于:参数本身就是结构化的,天然适合做抽取、路由、动作执行。
下面给一个无需 API Key 也能完整跑通的演示脚本:先用 Mock 模拟模型输出,展示完整流程;再附上真实 API 调用方式。
python
import json
from typing import Any
# ---------- 工具(函数)定义 ----------
TOOLS = [
{
"type": "function",
"function": {
"name": "create_order",
"description": "根据抽取出的订单信息创建订单",
"parameters": {
"type": "object",
"properties": {
"product": {"type": "string", "description": "商品名"},
"quantity": {"type": "integer", "description": "数量"},
"unit_price": {"type": "number", "description": "单价"},
},
"required": ["product", "quantity", "unit_price"],
},
},
}
]
# ---------- 真实业务函数 ----------
def create_order(product: str, quantity: int, unit_price: float) -> dict:
"""真正落库/下单的业务逻辑(示例仅打印)"""
return {
"status": "ok",
"product": product,
"quantity": quantity,
"total": round(quantity * unit_price, 2),
}
# ---------- 模拟 LLM 返回的工具调用(MOCK) ----------
# 真实场景下,这一步是调用大模型 API,返回的 message 里带 tool_calls。
MOCK_LLM_TOOL_CALL = {
"name": "create_order",
"arguments": '{"product": "iPhone 15", "quantity": 3, "unit_price": 5999}',
}
def handle_tool_call(tool_call: dict[str, Any]) -> dict:
"""解析并执行模型发起的工具调用"""
fn_name = tool_call["name"]
args = json.loads(tool_call["arguments"]) # 参数一定是合法 JSON
if fn_name == "create_order":
return create_order(**args)
raise ValueError(f"未知工具: {fn_name}")
if __name__ == "__main__":
result = handle_tool_call(MOCK_LLM_TOOL_CALL)
print(json.dumps(result, ensure_ascii=False, indent=2))
真实 API 调用(替换 Mock 部分):
python
from openai import OpenAI
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", "sk-xxx"))
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是订单助手,请调用合适的工具处理用户需求。"},
{"role": "user", "content": "帮我下单:iPhone 15 三台,单价 5999。"},
],
tools=TOOLS,
tool_choice="auto", # 模型自行决定是否调用工具
)
msg = resp.choices[0].message
if msg.tool_calls:
result = handle_tool_call({
"name": msg.tool_calls[0].function.name,
"arguments": msg.tool_calls[0].function.arguments,
})
print(json.dumps(result, ensure_ascii=False, indent=2))
Function Calling 的三大优势:
- 参数强类型校验 :模型在生成参数时会遵循你的 JSON Schema,拿到手的
arguments一定是合法 JSON。 - 意图路由:一个对话可以挂多个工具,模型自动判断该调哪个,适合做"智能体(Agent)"。
- 动作闭环:从"生成文本"升级为"调用真实函数",是构建 AI 应用的核心枢纽。
五、生产落地避坑清单
结合踩过的坑,给你几条实战建议:
- 抽取/分类/落库类任务,一律开
strict或走 Function Calling,别再靠 Prompt 兜底。 - 能用枚举就不要用自由文本 。比如"状态"字段定义成
enum: ["pending", "paid", "shipped"],比让模型自由发挥可靠得多。 - 数字类型想清楚 。价格用
number,数量用integer,避免混用导致下游类型错误。 - 仍然要防御 。即便模型承诺 100% 符合,也建议在
json.loads外层包一层异常捕获并记录原始输出,方便事后排查(模型偶发异常时至少能定位)。 - 记录 Schema 版本。Schema 一旦上线,业务侧改动要谨慎,保持向后兼容,否则老数据解析会挂。
六、总结
回到开头的问题:为什么你的 JSON 输出总是崩?因为你在用"劝告"对抗"概率采样"。正确做法是从机制上约束:
| 方法 | 稳定性 | 成本 | 适用场景 |
|---|---|---|---|
| Prompt 约束 | 低 | 零 | 原型、一次性脚本 |
| JSON Schema 结构化输出 | 高 | 低 | 抽取、分类、数据落库 |
| Function Calling | 高 | 低 | 意图路由、Agent、动作执行 |
一句话记住:想让大模型稳定输出 JSON,别问它"能不能",要给它"一个必须遵守的约束"。
如果你正在把大模型接进生产系统,强烈建议把上面的结构化输出或 Function Calling 用起来,你会发现------那些曾经让你半夜爬起来改 Bug 的解析异常,从此再也不会出现了。
本文为原创技术分享,若对你有帮助,欢迎点赞、收藏、转发。有问题也欢迎在评论区交流,一起把大模型用得又稳又顺。