LLM 稳定输出json 格式的几种方式

5 阅读3分钟

为了让你一目了然,我把三种主流流派(OpenAI天花板、国产平替、Claude特立独行)最核心的 Request(请求体)Response(响应体) 的 JSON 结构扒出来。

我们统一设定一个任务:让模型从“张三今年25岁”这句话中,提取出姓名和年龄。

场景一:OpenAI(Structured Outputs 严格模式)

这是目前最稳的,它在请求里直接塞入了一张完整的“图纸”(JSON Schema),响应直接就是纯 JSON。 【Request 请求示例】

{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    {"role": "user", "content": "张三今年25岁"}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "user_info",
      "strict": true, 
      "schema": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "age": { "type": "integer" }
        },
        "required": ["name", "age"],
        "additionalProperties": false
      }
    }
  }
}

💡 关键点:strict: trueadditionalProperties: false 是灵魂,代表“必须且只能”按这个格式来。 【Response 响应示例】

{
  "id": "chatcmpl-xxx",
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "{"name":"张三","age":25}" 
      }
    }
  ]
}

💡 关键点:content 里面干干净净,就是一串合法的 JSON 字符串,你直接 json.loads() 就行,绝对报错不了。

场景二:国产模型(以 阿里 Qwen / 智谱 GLM 为例)

国产模型大部分走的是 OpenAI 早期的“简易开关”路线。它不让你传复杂的 Schema 图纸,只给你一个开关,具体长什么样靠你在 Prompt 里“嘴遁”。 【Request 请求示例】

{
  "model": "qwen-plus", 
  "messages": [
    {
      "role": "system", 
      "content": "你是一个数据提取助手。请务必提取用户的name(字符串)和age(整数),并以纯JSON格式返回,不要任何多余文字。格式如:{"name":"xx","age":xx}"
    },
    {"role": "user", "content": "张三今年25岁"}
  ],
  "response_format": { "type": "json_object" } 
}

💡 关键点:只有一句 "type": "json_object"。注意阿里要求 Prompt 里必须出现“JSON”字样。 【Response 响应示例】

{
  "id": "chatcmpl-xxx",
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "{"name":"张三","age":25}"
      }
    }
  ]
}

💡 关键点:返回格式和 OpenAI 一样,content 是纯 JSON 字符串。区别在于,它只保证语法合法(比如不会少个括号),但不保证字段一定叫 name/age(如果你 Prompt 写得不清楚,它可能返回 {"姓名":"张三","年龄":25}),所以需要配合好 Prompt。

场景三:Anthropic Claude(Tool Use 借尸还魂)

Claude 没有上面的 response_format 开关,它的做法是:假装系统里挂了一个名叫“提取工具”的函数,逼着 Claude 按照这个函数的参数格式来输出。 【Request 请求示例】

{
  "model": "claude-sonnet-4-20250514",
  "messages": [
    {"role": "user", "content": "张三今年25岁"}
  ],
  "tools": [
    {
      "name": "extract_user_info",
      "description": "提取用户信息",
      "strict": true, 
      "input_schema": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "姓名" },
          "age": { "type": "integer", "description": "年龄" }
        },
        "required": ["name", "age"],
        "additionalProperties": false
      }
    }
  ]
}

💡 关键点:没有 response_format,而是把你的 JSON 结构定义在了 tools[0].input_schema 里。 【Response 响应示例】

{
  "id": "msg_xxx",
  "content": [
    {
      "type": "text",
      "text": "好的,我来为您提取信息。"
    },
    {
      "type": "tool_use",
      "id": "toolu_xxx",
      "name": "extract_user_info",
      "input": {
        "name": "张三",
        "age": 25
      }
    }
  ]
}

💡 关键点:Claude 的响应结构变了!它返回的是一个 content 数组。你要找里面 type"tool_use" 的那个块,然后从它的 input 字段里把 JSON 拿出来。这个 input 是被 Claude 底层严格约束过的,100% 符合你写的 input_schema

🌟 总结对照表(写代码时怎么取值)

厂商流派开启方式的灵魂参数返回 JSON 藏在哪里?你的 Python 取值代码
OpenAI (严格)response_format.json_schema.strict: truemessage.contentdata = json.loads(response.choices[0].message.content)
国产 Qwen/GLMresponse_format.type: "json_object"message.contentdata = json.loads(response.choices[0].message.content)
Claudetools[0].strict: true + input_schemacontent[?].input (需遍历找 tool_use)data = next(block.input for block in response.content if block.type == "tool_use")

有了这三个对照,你在写代码封装统一调用层的时候,就知道针对不同模型该怎么发请求、怎么抠数据了。