大模型结构化输出实战:3招让LLM稳定返回JSON,告别解析地狱(附完整代码)

大模型结构化输出实战:3 招让 LLM 稳定返回 JSON,告别解析地狱(附完整代码)

导语:你是不是也遇到过这种情况------写好的 Prompt 让大模型返回 JSON,测试时好好的,一上线就开始抽风:多一个逗号、少一个引号、字段名拼错、甚至直接甩给你一句"好的,以下是结果:"外加一段废话。然后你的 json.loads() 抛异常,线上告警疯狂响起。

这不是你的 Prompt 写得不够好,而是你用错了方法。大模型本质上是"概率采样生成文本",让它天然保证 JSON 格式,就像让一个自由发挥的作家严格填写表格------你需要的不是更凶的叮嘱,而是**结构化输出(Structured Output)**的正确姿势。


一、为什么大模型输出 JSON 总是不稳定?

先搞清楚问题的根源。大模型(LLM)生成文本的本质,是逐 token 采样:

  1. 模型根据上下文,为下一个 token 计算出一个概率分布;
  2. 按这个分布随机采样出一个 token;
  3. 重复直到生成结束。

这里有两个关键因素在捣乱:

  • 温度(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 的三大优势:

  1. 参数强类型校验 :模型在生成参数时会遵循你的 JSON Schema,拿到手的 arguments 一定是合法 JSON。
  2. 意图路由:一个对话可以挂多个工具,模型自动判断该调哪个,适合做"智能体(Agent)"。
  3. 动作闭环:从"生成文本"升级为"调用真实函数",是构建 AI 应用的核心枢纽。

五、生产落地避坑清单

结合踩过的坑,给你几条实战建议:

  1. 抽取/分类/落库类任务,一律开 strict 或走 Function Calling,别再靠 Prompt 兜底。
  2. 能用枚举就不要用自由文本 。比如"状态"字段定义成 enum: ["pending", "paid", "shipped"],比让模型自由发挥可靠得多。
  3. 数字类型想清楚 。价格用 number,数量用 integer,避免混用导致下游类型错误。
  4. 仍然要防御 。即便模型承诺 100% 符合,也建议在 json.loads 外层包一层异常捕获并记录原始输出,方便事后排查(模型偶发异常时至少能定位)。
  5. 记录 Schema 版本。Schema 一旦上线,业务侧改动要谨慎,保持向后兼容,否则老数据解析会挂。

六、总结

回到开头的问题:为什么你的 JSON 输出总是崩?因为你在用"劝告"对抗"概率采样"。正确做法是从机制上约束:

方法 稳定性 成本 适用场景
Prompt 约束 低 零 原型、一次性脚本
JSON Schema 结构化输出 高 低 抽取、分类、数据落库
Function Calling 高 低 意图路由、Agent、动作执行

一句话记住:想让大模型稳定输出 JSON,别问它"能不能",要给它"一个必须遵守的约束"。

如果你正在把大模型接进生产系统,强烈建议把上面的结构化输出或 Function Calling 用起来,你会发现------那些曾经让你半夜爬起来改 Bug 的解析异常,从此再也不会出现了。


本文为原创技术分享,若对你有帮助,欢迎点赞、收藏、转发。有问题也欢迎在评论区交流,一起把大模型用得又稳又顺。

相关推荐
Java后端的Ai之路几秒前
CAS并发控制-从CPU指令到企业级乐观锁
vue.js·人工智能·python·cas·乐观锁
情 九5 分钟前
MCP 实战 06:把 HTTP 接口接成工具,让 AI 帮你查数据
人工智能·网络协议·http·mcp
码农小旋风40 分钟前
Ray分布式计算引擎介绍
人工智能·架构
火山引擎和TA的超级拍档们43 分钟前
产品情报局 08 | AI 内容助手:全网热点驱动,一站式打通内容生产全链路
人工智能
Binovate缤江数智1 小时前
水泵远程开关之后,还需要管什么?数智开关在无人值守泵站中的监测、保护与能耗分析
人工智能·物联网
素男1 小时前
两个月不是六十天
人工智能·agent·self-becoming·ai长期记忆·ai自我介绍
byte轻骑兵1 小时前
【LMP】规范精讲[10]: 语音专线开线记——SCO 固定占座与 eSCO 限时重传
人工智能·人机交互·lmp·蓝牙链路管理·蓝牙外设
龙亘川1 小时前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化
迅易科技1 小时前
Copilot Studio 更新思考:Agent、Workflow、App 如何重新划分企业流程分工
人工智能·微软