驯服 LLM:如何让大模型稳定输出 JSON

驯服 LLM:如何让大模型稳定输出 JSON

作为 AI 应用开发者,我们每天都在在这个核心矛盾搏斗: 大语言模型 (LLM) 是概率性的,而软件工程是确定性的。

LLM 天生是文本补全引擎 (Text Completion Engine)。它的训练数据是莎士比亚的诗、GitHub 的代码和维基百科。它的本能是生成流畅的自然语言。

但我们的后端接口 (API) 需要的是 JSON

你一定遇到过这种情况:千辛万苦设计了 Prompt 只要 JSON,结果模型心情好的时候给你返回标准 JSON,心情不好的时候给你来一句: "Sure! Here is the JSON you requested: { ... }" 或者更惨,它生成的 JSON 缺了个括号,导致你的 JSON.parse() 抛出异常,整个程序崩溃。

今天我们深入探讨让 LLM 稳定输出结构化数据的三个阶段:从入门的 JSON Mode,到进阶的 Tool Calling,再到终极的 Zod Self-Correction。

第一阶段:Prompt Engineering & JSON Mode

在 OpenAI 推出官方支持之前,我们只能靠 Prompt 恳求模型。

1. 基础 Prompt 技巧

  • 给例子 (Few-Shot):明确展示 Input 和 Output。
  • 强约束:System Prompt 里强调 “Do not output any explanation. Output valid JSON only.”
  • Pre-filling:这是 Anthropic Claude 的绝活。在 Assistant Message 的开头预填一个 {,诱导模型接着补全 JSON 的剩余部分。

2. OpenAI JSON Mode

OpenAI 官方推出了 response_format: { type: "json_object" } 参数。 这基本上解决了“括号不匹配”的问题。模型被强制限制在生成的 Token 必须符合 JSON 语法树。

⚠️ 关键坑点: 即使开了 JSON Mode,你也 必须 在 System Prompt 里显式包含 “JSON” 这个词。否则 API 会报错 400 Bad Request

const completion = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [
    { 
      role: "system", 
      content: "You are a helpful assistant designed to output JSON." // 必须带 JSON 这个词
    },
    { role: "user", content: "Who won the world series in 2020?" }
  ],
  response_format: { type: "json_object" },
});

局限性:它只保证语法正确,不保证 Schema 正确。它可能返回 { "winner": "Dodgers" },也可能返回 { "team": "Dodgers", "year": 2020 }。你还得自己做类型检查。

第二阶段:Function Calling (Tool Use)

这是目前工业界最稳健的方法。利用模型微调过的 Tool Calling 能力来生成 JSON。

原理是:我不告诉模型“请生成 JSON”,而是通过 API 定义“我有一个函数 save_user_info(name: string, age: number),请帮我调用它”。

为了调用这个函数,模型被迫——基于它训练时的微调目标——生成严格符合 Schema 的 JSON 参数。

const completion = await openai.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "My name is Alice and I am 30." }],
  tools: [{
    type: "function",
    function: {
      name: "extract_info",
      parameters: {
        type: "object",
        properties: {
          name: { type: "string" },
          age: { type: "number" }
        },
        required: ["name", "age"]
      }
    }
  }],
  tool_choice: { type: "function", function: { name: "extract_info" } } // 强制调用
});

const json = JSON.parse(completion.choices[0].message.tool_calls[0].function.arguments);

这种方法的稳定性远高于 JSON Mode,因为它不仅约束了语法,还约束了字段名和类型。

第三阶段:Auto-Correction with Instructor & Zod (自我修正)

如果你是 TypeScript 用户,你一定知道 Zod。如果你是 Python 用户,你见过 Pydantic。 能不能把这些 Schema Validation 库和 LLM 结合起来?

Instructor 库(及其类似理念的实现)就是做这个的。它的核心逻辑是:验证 + 重试 (Validation + Retry)

我们可以构建这样一个循环:

  1. LLM 生成 JSON。
  2. 代码用 Zod 解析 (schema.safeParse())。
  3. 分支 A:如果解析成功,直接返回数据。Happy Path!
  4. 分支 B:如果解析失败(比如 Zod 报错 Expected string, received number),我们捕获这个错误。
  5. 反馈循环:我们构造一个新的 User Message,包含 LLM 刚才生成的错误 JSON + Zod 抛出的具体 Error Message,把它“喂”回给 LLM。

    “The JSON you just generated has errors. Field ‘age’ should be a number, but you gave a string. Please fix it.”

  6. LLM 收到反馈,进行自我修正 (Self-Correction),重新生成。

代码实现演示 (伪代码)

import { z } from "zod";
import { OpenAI } from "openai";

const UserSchema = z.object({
  name: z.string(),
  skills: z.array(z.string()).max(3, "Max 3 skills only") // LLM 经常忽略数量限制
});

async function generateStructuredData(prompt: string, retries = 3) {
  let history = [{ role: "user", content: prompt }];

  while (retries > 0) {
    const response = await openai.chat.completions.create({ messages: history, ... });
    const content = response.choices[0].message.content;
    const json = JSON.parse(content);

    const parsed = UserSchema.safeParse(json);

    if (parsed.success) {
      return parsed.data; // 成功!
    }

    // 失败,构造错误反馈
    console.warn(`Attempt failed: ${parsed.error.message}. Retrying...`);
    
    history.push({ role: "assistant", content: content });
    history.push({ 
      role: "user", 
      content: `Validation Error: ${parsed.error.message}. Please fix the JSON.` 
    });

    retries--;
  }
}

这种模式极其强大。它允许我们在 Schema 中定义复杂的业务逻辑(比如 .min(), .regex(), .refine()),并利用 LLM 的推理能力来满足这些要求。

总结

LLM 的本质是文本补全,它天生不懂结构。 结构化输出是连接 AI(概率世界)和 传统软件(确定性世界)的桥梁。

  • 简单场景:用 JSON Mode + Prompt。
  • 中等场景:用 Function Calling。
  • 复杂场景(需要严格校验):用 Instructor / Zod Loop。

掌握了这项技术,你才能真正把 AI 嵌入到自动化的业务流中,而不仅仅是做一个聊天机器人。