大模型如何可靠返回合格的 JSON:从提示词到 Structured Outputs

摘要: 本文系统梳理大模型生成 JSON 的四种方案,并给出一套生产级处理流程。内容覆盖提示词 JSON、JSON Mode、JSON Schema、Function Call、流式拼接、Schema 校验、业务校验、失败重试与供应商兼容,可作为结构化输出功能的设计和排查参考。

关键词: 大模型、JSON、Structured Outputs、JSON Schema、Function Call、流式输出


前言

在业务系统中,“让大模型返回 JSON”看起来只是加一句提示词,真正上线后却经常遇到以下问题:

  • JSON前后夹带解释文字;
  • 使用 Markdown代码围栏包裹;
  • 字段缺失或字段名变化;
  • 数字被返回成字符串;
  • 枚举值不符合约定;
  • 嵌套结构不完整;
  • 流式响应尚未结束就开始解析;
  • 达到输出 token上限,JSON在中途被截断;
  • 模型返回合法 JSON,但业务含义错误;
  • 模型或中转服务不支持 response_format

本文从协议和工程两个层面说明如何获得可靠的 JSON输出,并给出一套可以直接落地的生产流程。

适合哪些读者

  • 正在对接大模型 API 的后端或全栈开发者;
  • 需要从文本中抽取稳定结构化数据的开发者;
  • 正在实现 Agent、Function Call 或 MCP 工具调用的开发者;
  • 遇到 JSON 截断、字段漂移或模型不遵循格式问题的开发者。

阅读后可以获得什么

  • 理解“合法 JSON”“符合 Schema”和“业务正确”的区别;
  • 知道不同模型能力下应该选择哪一种 JSON 返回方案;
  • 掌握完整请求、响应、校验、重试与降级流程;
  • 避免把截断 JSON、工具参数或模型幻觉直接写入业务系统。

1. 先给结论

按可靠性从低到高,可以把方案分为四级:

级别 方案 格式可靠性 适用场景
1 提示词要求 JSON 临时脚本、兼容不支持结构化输出的模型
2 response_format: json_object 只要求合法 JSON,不强制固定字段
3 response_format: json_schema + strict API返回、数据抽取、稳定业务结构
4 Function Call / Tool Call 模型需要触发程序动作或调用工具

生产环境推荐:

优先使用 json_schema + strict
→ 不支持时降级到 json_object
→ 仍不支持时使用提示词 JSON + 本地 Schema校验
→ 涉及执行动作时使用 Function Call

无论使用哪种方式,客户端都必须执行:

检查响应状态
→ 检查 finish_reason
→ 拼接完整流式内容
→ JSON解析
→ Schema校验
→ 业务语义校验
→ 失败时有限重试或降级

response_format 能提高结构可靠性,但不能突破上下文窗口、单次输出上限,也不能保证数据事实正确。

核心原则: 模型负责生成候选结构,程序负责验证。任何模型返回的 JSON 在进入数据库、调用工具或触发业务动作前,都必须经过确定性代码校验。

2. 什么叫“合格的 JSON”

一个可用于生产业务的 JSON结果至少要通过四层检查。

2.1 语法合法

下面是合法 JSON:

{
  "name": "张三",
  "age": 28,
  "active": true,
  "tags": ["开发", "AI"],
  "remark": null
}

常见的非法情况:

{name: "张三"}              字段名没有双引号
{'name': '张三'}            使用了Python单引号
{"name": "张三",}          末尾多余逗号
```json ... ```           外层带Markdown围栏
以下是结果:{"name":"张三"} 前面夹带说明文字

2.2 结构符合 Schema

语法合法不代表结构正确。例如业务要求 age 是整数,模型却返回:

{
  "name": "张三",
  "age": "二十八"
}

它是合法 JSON,但不符合业务 Schema。

2.3 业务语义正确

即使结构正确,也可能不符合业务规则:

{
  "name": "张三",
  "age": -20
}

因此还要校验年龄范围、时间先后关系、订单金额、状态迁移等业务约束。

2.4 内容完整

如果模型达到输出上限,可能返回:

{"name":"张三","projects":[{"name":"项目A"},{"name":"项目B"

此时不仅 JSON非法,而且响应通常伴随:

finish_reason: length

客户端不能把这种内容当作普通 JSON解析失败,而应先识别为输出截断。

3. 方案一:只用提示词要求 JSON

这是兼容性最好、可靠性最低的方案。

3.1 请求示例

{
  "model": "example-model",
  "messages": [
    {
      "role": "system",
      "content": "你是数据抽取助手。只返回一个合法JSON对象,不要使用Markdown代码围栏,不要添加解释文字。字段必须为name、age和skills。"
    },
    {
      "role": "user",
      "content": "张三今年28岁,擅长Python和SQL。"
    }
  ],
  "max_tokens": 1000,
  "temperature": 0,
  "stream": false
}

理想响应:

{
  "id": "chatcmpl-json-001",
  "object": "chat.completion",
  "model": "example-model",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"name\":\"张三\",\"age\":28,\"skills\":[\"Python\",\"SQL\"]}"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 77,
    "completion_tokens": 29,
    "total_tokens": 106
  }
}

注意,message.content 是一个字符串,客户端仍需要调用 JSON解析器:

import json

raw_content = response["choices"][0]["message"]["content"]
data = json.loads(raw_content)

3.2 更可靠的提示词模板

你是结构化数据生成器。

输出规则:
1. 只返回一个JSON对象。
2. 不要使用Markdown代码围栏。
3. 不要添加解释、标题、前缀或后缀。
4. 字段名称必须严格使用:name、age、skills。
5. age必须是整数,未知时返回null,不要猜测。
6. skills必须是字符串数组,没有内容时返回空数组。
7. 不允许输出未定义字段。

目标结构:
{"name":"string","age":"integer|null","skills":["string"]}

提示词能提高成功率,但不是协议级约束。模型仍可能返回错误格式,所以必须在客户端验证。

4. 方案二:JSON Mode

支持 JSON Mode的 OpenAI兼容接口通常使用:

{
  "response_format": {
    "type": "json_object"
  }
}

4.1 完整请求

{
  "model": "example-model",
  "messages": [
    {
      "role": "system",
      "content": "返回JSON对象,字段为name、age和skills。不要添加解释。"
    },
    {
      "role": "user",
      "content": "张三今年28岁,擅长Python和SQL。"
    }
  ],
  "response_format": {
    "type": "json_object"
  },
  "max_tokens": 1000,
  "temperature": 0,
  "top_p": 1,
  "n": 1,
  "stream": false
}

4.2 响应

{
  "id": "chatcmpl-json-002",
  "object": "chat.completion",
  "model": "example-model",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"name\":\"张三\",\"age\":28,\"skills\":[\"Python\",\"SQL\"]}"
      },
      "finish_reason": "stop",
      "logprobs": null
    }
  ],
  "usage": {
    "prompt_tokens": 61,
    "completion_tokens": 29,
    "total_tokens": 90
  }
}

JSON Mode一般保证结果可以解析为 JSON对象,但不一定保证:

  • 必须包含所有字段;
  • 字段类型正确;
  • 没有额外字段;
  • 枚举值符合要求;
  • 数值位于合理范围;
  • 数据事实正确。

因此JSON Mode仍然需要本地 Schema校验。

5. 方案三:JSON Schema Structured Outputs

如果模型支持 Structured Outputs,应优先使用 json_schema 和严格模式。

5.1 完整请求

{
  "model": "example-model",
  "messages": [
    {
      "role": "system",
      "content": "从用户文本中提取人员信息。未知字段返回null,不要推测。"
    },
    {
      "role": "user",
      "content": "张三今年28岁,擅长Python和SQL。"
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "person_information",
      "description": "从文本中提取的人员信息。",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "人员姓名。"
          },
          "age": {
            "anyOf": [
              {"type": "integer"},
              {"type": "null"}
            ],
            "description": "人员年龄,未知时为null。"
          },
          "skills": {
            "type": "array",
            "description": "明确提到的技能。",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["name", "age", "skills"],
        "additionalProperties": false
      }
    }
  },
  "max_tokens": 1000,
  "temperature": 0,
  "top_p": 1,
  "n": 1,
  "stream": false
}

5.2 成功响应

{
  "id": "chatcmpl-json-003",
  "object": "chat.completion",
  "model": "example-model",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "{\"name\":\"张三\",\"age\":28,\"skills\":[\"Python\",\"SQL\"]}",
        "refusal": null
      },
      "finish_reason": "stop",
      "logprobs": null
    }
  ],
  "usage": {
    "prompt_tokens": 198,
    "completion_tokens": 29,
    "total_tokens": 227
  }
}

5.3 为什么还要本地校验

即使服务端声称支持严格 Schema,也仍应本地校验,原因包括:

  • 中转网关可能忽略或改写 response_format
  • 实际模型可能不支持严格模式;
  • 模型供应商可能只支持 JSON Schema子集;
  • 流式拼接、网络中断或SDK兼容问题可能破坏内容;
  • Schema约束不了所有业务语义。

6. JSON Schema可以描述什么

常见基础类型:

类型 示例
object {"name":"张三"}
array ["Python","SQL"]
string "张三"
number 18.5
integer 18
boolean true
null null

常用结构关键字:

properties
required
additionalProperties
items
enum
const
anyOf
$defs
$ref

标准 JSON Schema还定义了大量约束:

minimum / maximum
minLength / maxLength
pattern / format
minItems / maxItems / uniqueItems
oneOf / allOf / not

但是模型服务通常只支持 JSON Schema的一个子集。接入时应以具体供应商文档和实际测试为准。

6.1 可空字段

在严格模式中,常见做法是字段仍然必填,但允许值为 null

{
  "anyOf": [
    {"type": "string"},
    {"type": "null"}
  ]
}

这比完全省略字段更适合稳定的数据接口:调用方始终知道字段存在,只需要判断值是否为 null

6.2 枚举

{
  "type": "string",
  "enum": ["pending", "running", "completed", "failed"]
}

枚举适合状态、分类和固定选项,可以减少模型自由发挥。

6.3 嵌套对象数组

{
  "type": "object",
  "properties": {
    "order_id": {"type": "string"},
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "sku": {"type": "string"},
          "quantity": {"type": "integer"},
          "price": {"type": "number"}
        },
        "required": ["sku", "quantity", "price"],
        "additionalProperties": false
      }
    }
  },
  "required": ["order_id", "items"],
  "additionalProperties": false
}

Schema越复杂,输入 token成本越高,模型生成错误和供应商不兼容的概率也越高。能用简单结构解决时,不要设计过深的嵌套。

7. 方案四:Function Call

如果JSON用于描述“应用接下来要执行什么动作”,应使用 Function Call,而不是要求模型把命令放进普通 content

7.1 请求

{
  "model": "example-model",
  "messages": [
    {
      "role": "system",
      "content": "你是订单查询助手。需要查询订单时使用工具。"
    },
    {
      "role": "user",
      "content": "查询订单 H20260718001。"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_order",
        "description": "根据订单号查询订单。",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {
              "type": "string",
              "description": "完整订单号。"
            }
          },
          "required": ["order_id"],
          "additionalProperties": false
        }
      }
    }
  ],
  "tool_choice": "auto",
  "parallel_tool_calls": true,
  "max_tokens": 1000,
  "temperature": 0,
  "stream": false
}

7.2 响应

{
  "id": "chatcmpl-tool-json-001",
  "object": "chat.completion",
  "model": "example-model",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_order_001",
            "type": "function",
            "function": {
              "name": "get_order",
              "arguments": "{\"order_id\":\"H20260718001\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ],
  "usage": {
    "prompt_tokens": 128,
    "completion_tokens": 28,
    "total_tokens": 156
  }
}

function.arguments 通常仍是 JSON字符串,应用必须解析、验证、审批,再执行工具。Function Call提高了参数结构可靠性,但不代表模型有执行权限。

8. 一套生产级处理流程

业务请求
   │
   ▼
探测模型能力
   │
   ▼
选择 json_schema / json_object / 提示词 JSON
   │
   ▼
计算输入与输出 token 预算
   │
   ▼
发送模型请求
   │
   ├─ 请求失败 ──> 分类网络、鉴权、限流和参数错误
   │
   ▼
检查 finish_reason
   │
   ├─ length ─────────> 判定截断,禁止直接修补半截 JSON
   ├─ content_filter ─> 处理拒绝或安全拦截
   ├─ tool_calls ─────> 解析并校验工具参数
   └─ stop
        │
        ▼
   提取完整 content
        │
        ▼
   JSON 解析
        │
        ▼
   JSON Schema 校验
        │
        ├─ 失败 ──> 有限重试或能力降级
        │
        ▼
   业务语义校验
        │
        ├─ 失败 ──> 有限重试或返回业务错误
        │
        ▼
   返回可信业务对象

8.1 能力探测

不要假设所有文本模型都支持 response_format。可以为每个模型维护能力信息:

{
  "example-model": {
    "supports_json_object": true,
    "supports_json_schema": true,
    "supports_strict_schema": true,
    "supports_tools": true,
    "max_output_tokens": 16384,
    "context_window": 131072
  }
}

如果缺少官方能力接口,应通过小请求测试并缓存结果。不能在每个业务请求中先故意触发一次 400 来探测能力。

8.2 输出预算

结构化输出仍受 token限制:

输入 token + 输出 token <= 上下文窗口
输出 token <= 模型单次输出上限
输出 token <= 请求指定上限

对于可能返回大量数组的数据,优先使用分页、限制条数或先返回摘要。不要让模型一次生成数万条记录。

8.3 检查停止原因

在解析 content 前先检查:

choice = response["choices"][0]
finish_reason = choice.get("finish_reason")

if finish_reason == "length":
    raise RuntimeError("模型输出达到长度限制,JSON可能不完整")

if finish_reason == "content_filter":
    raise RuntimeError("模型输出被安全策略终止")

if finish_reason == "tool_calls":
    raise RuntimeError("当前响应是工具调用,不是普通JSON内容")

if finish_reason != "stop":
    raise RuntimeError(f"未知结束原因: {finish_reason}")

不同服务商可能使用 max_tokensMAX_TOKENSincomplete 或其他原始值,客户端适配层应先归一化。

8.4 JSON解析

import json

content = choice["message"].get("content")
if not isinstance(content, str) or not content.strip():
    raise ValueError("模型没有返回JSON文本")

try:
    data = json.loads(content)
except json.JSONDecodeError as exc:
    raise ValueError(
        f"JSON解析失败: line={exc.lineno}, column={exc.colno}, message={exc.msg}"
    ) from exc

不建议默认使用正则截取第一个 { 到最后一个 }。这种“修复”可能掩盖协议错误、误截嵌套对象,甚至错误接受被截断的数据。

8.5 Schema校验

使用 Python jsonschema

from jsonschema import Draft202012Validator

PERSON_SCHEMA = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": ["integer", "null"]},
        "skills": {
            "type": "array",
            "items": {"type": "string"},
        },
    },
    "required": ["name", "age", "skills"],
    "additionalProperties": False,
}

validator = Draft202012Validator(PERSON_SCHEMA)
errors = sorted(validator.iter_errors(data), key=lambda error: list(error.path))

if errors:
    details = [
        {
            "path": ".".join(str(part) for part in error.path) or "$",
            "message": error.message,
        }
        for error in errors
    ]
    raise ValueError(f"JSON Schema校验失败: {details}")

也可以使用 Pydantic,将解析、类型检查和业务模型结合:

from pydantic import BaseModel, ConfigDict, Field


class PersonInformation(BaseModel):
    model_config = ConfigDict(extra="forbid")

    name: str = Field(min_length=1)
    age: int | None = Field(default=None, ge=0, le=150)
    skills: list[str]


person = PersonInformation.model_validate(data)

8.6 业务语义校验

Schema只能验证结构,不能证明内容真实。例如订单总额应等于明细之和:

calculated_total = sum(item["quantity"] * item["price"] for item in data["items"])

if abs(calculated_total - data["total_amount"]) > 0.01:
    raise ValueError("订单总额与明细计算结果不一致")

对身份证号、库存、订单状态和权限信息,应以数据库或确定性代码为准,不能仅相信模型生成值。

9. 可直接复用的 Python封装

下面的代码展示核心校验流程。模型SDK调用部分以函数参数传入,便于替换不同供应商。

import json
from collections.abc import Callable
from typing import Any

from jsonschema import Draft202012Validator


class StructuredOutputError(RuntimeError):
    pass


class OutputTruncatedError(StructuredOutputError):
    pass


class SchemaValidationError(StructuredOutputError):
    pass


def parse_structured_response(
    response: dict[str, Any],
    schema: dict[str, Any],
) -> dict[str, Any]:
    choices = response.get("choices")
    if not isinstance(choices, list) or not choices:
        raise StructuredOutputError("响应中没有choices")

    choice = choices[0]
    finish_reason = choice.get("finish_reason")

    if finish_reason == "length":
        raise OutputTruncatedError("模型达到输出长度限制")
    if finish_reason == "content_filter":
        raise StructuredOutputError("输出被安全策略终止")
    if finish_reason == "tool_calls":
        raise StructuredOutputError("收到工具调用,而不是结构化content")
    if finish_reason != "stop":
        raise StructuredOutputError(f"不支持的finish_reason: {finish_reason!r}")

    message = choice.get("message") or {}
    refusal = message.get("refusal")
    if refusal:
        raise StructuredOutputError(f"模型拒绝请求: {refusal}")

    content = message.get("content")
    if not isinstance(content, str) or not content.strip():
        raise StructuredOutputError("响应content为空")

    try:
        data = json.loads(content)
    except json.JSONDecodeError as exc:
        raise StructuredOutputError(
            f"JSON语法错误: line={exc.lineno}, column={exc.colno}, {exc.msg}"
        ) from exc

    validator = Draft202012Validator(schema)
    errors = sorted(validator.iter_errors(data), key=lambda error: list(error.path))
    if errors:
        details = "; ".join(
            f"{'.'.join(str(part) for part in error.path) or '$'}: {error.message}"
            for error in errors
        )
        raise SchemaValidationError(details)

    return data


def request_structured_data(
    send_request: Callable[[dict[str, Any]], dict[str, Any]],
    request_body: dict[str, Any],
    schema: dict[str, Any],
    max_attempts: int = 2,
) -> dict[str, Any]:
    last_error: Exception | None = None

    for attempt in range(1, max_attempts + 1):
        try:
            response = send_request(request_body)
            return parse_structured_response(response, schema)
        except OutputTruncatedError:
            raise
        except (StructuredOutputError, SchemaValidationError) as exc:
            last_error = exc
            if attempt == max_attempts:
                break

            request_body = {
                **request_body,
                "messages": [
                    *request_body["messages"],
                    {
                        "role": "user",
                        "content": (
                            "上一次输出未通过结构校验。请重新生成完整JSON,"
                            "严格遵守Schema,不要添加解释文字。"
                        ),
                    },
                ],
            }

    raise StructuredOutputError(
        f"结构化输出在{max_attempts}次尝试后仍失败: {last_error}"
    )

重要原则:

  • 截断错误不应把半截 JSON交给模型“猜着修复”;
  • 重试次数必须有限;
  • 涉及费用或副作用的调用要保证幂等;
  • 日志记录错误类型、模型、请求ID和 token用量,但不要记录密钥和敏感原文。

10. 流式 JSON如何处理

流式返回时,每个数据块只是字符串片段:

data: {"choices":[{"index":0,"delta":{"content":"{\"name\":"}}]}

data: {"choices":[{"index":0,"delta":{"content":"\"张三\",\"age\":"}}]}

data: {"choices":[{"index":0,"delta":{"content":"28}"}}]}

data: {"choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]

正确方式:

parts: list[str] = []
finish_reason: str | None = None

for event in stream:
    choice = event["choices"][0]
    delta = choice.get("delta") or {}

    if isinstance(delta.get("content"), str):
        parts.append(delta["content"])

    if choice.get("finish_reason") is not None:
        finish_reason = choice["finish_reason"]

if finish_reason != "stop":
    raise RuntimeError(f"流式响应未正常结束: {finish_reason}")

data = json.loads("".join(parts))

不要对每个增量片段分别调用 json.loads()。在最后一个片段到达前,整个字符串通常都不是完整 JSON。

如果需要边生成边展示,可以展示原始文本进度,但只有完成拼接和校验后,才能把结果提交给业务系统。

11. 失败重试应该怎么做

11.1 可以重试的情况

  • 短暂网络错误;
  • 服务端 4295xx
  • JSON语法错误;
  • Schema字段缺失或类型错误;
  • 输出包含额外说明文字;
  • 模型偶发没有遵循结构要求。

11.2 不应盲目重试的情况

  • API Key错误;
  • 模型明确不支持 response_format
  • 输入已经超过上下文窗口;
  • Schema本身不被服务商支持;
  • 输出达到固定 token上限且请求内容没有缩小;
  • 安全策略明确拒绝;
  • 同一个有副作用工具已经成功执行。

11.3 分层降级

尝试 json_schema strict
→ 收到 unsupported response_format
→ 降级 json_object + 本地Schema校验
→ 仍不支持
→ 提示词JSON + 本地Schema校验
→ 连续失败
→ 返回可诊断错误,不无限重试

降级决策应依据明确的错误码或能力配置,不要对所有异常都静默降级,否则真实的鉴权、网络或业务错误会被掩盖。

12. 常见错误和改进方式

错误一:只写“请返回JSON”

问题:字段、类型、未知值和额外内容都没有约束。

改进:给出字段定义、类型、未知值策略,并使用 json_schema 或本地 Schema校验。

错误二:直接删除 Markdown围栏

问题:可能把本应判定为协议失败的内容伪装成成功,而且无法处理前后解释和多个 JSON对象。

改进:优先使用结构化输出;兼容旧模型时可以进行受控清理,但清理后仍必须严格解析和校验,并记录发生过降级。

错误三:JSON解析成功就写数据库

问题:合法 JSON可能字段错误、金额错误、状态非法或包含模型虚构数据。

改进:执行 Schema校验、业务校验和权限校验,再进入持久化流程。

错误四:忽略 finish_reason

问题:被截断的 JSON最终表现为普通解析错误,无法判断是模型格式问题还是输出预算不足。

改进:先检查结束原因,再解析内容。

错误五:把 max_tokens 设置得越大越好

问题:会增加延迟和成本,也可能与剩余上下文空间冲突;模型自身仍有单次输出上限。

改进:根据 Schema和预期数据量设置合理预算,大结果采用分页或分块。

错误六:相信 strict 等于事实正确

问题:strict 约束结构,不验证事实来源。

改进:关键事实通过数据库、检索结果和确定性程序验证。

13. 供应商兼容性

不是所有文本模型都支持 response_format。常见能力组合:

能力 可能情况
普通文本 基本都支持
提示词 JSON 基本可尝试,但可靠性因模型而异
json_object 部分 OpenAI兼容模型支持
json_schema 只有部分模型和网关支持
strict 支持范围更小,且可能只支持 Schema子集
Function Call 部分模型原生支持,部分网关模拟支持

可能遇到:

400 unsupported parameter: response_format
400 invalid schema
response_format被网关静默忽略
模型名称支持但当前部署版本不支持
同一聚合平台上的不同上游模型能力不同

因此,能力应绑定到“供应商 + API模式 + 实际模型 + 模型版本”,不能只根据模型展示名称判断。

14. 日志与监控

建议记录:

request_id
provider
actual_model
api_mode
response_format类型
schema名称和版本
finish_reason
prompt_tokens
completion_tokens
解析是否成功
Schema是否成功
重试次数
错误分类
响应延迟

不要记录:

API Key
Authorization Header
用户密码
完整个人敏感数据
不必要的完整提示词和模型输出

Schema本身也应该有版本,例如:

{
  "schema_name": "person_information",
  "schema_version": "1.2.0"
}

当字段发生变化时,可以区分是模型失败,还是客户端仍按旧 Schema解析。

15. 最终推荐架构

┌──────────┐    ┌──────────────┐    ┌────────────────┐
│ 业务请求 │ -> │ 模型能力注册表 │ -> │ Schema与提示词构建 │
└──────────┘    └──────────────┘    └────────────────┘
                                               │
                                               ▼
┌──────────┐    ┌──────────────┐    ┌────────────────┐
│ 业务对象 │ <- │ 业务语义校验   │ <- │ JSON Schema校验 │
└──────────┘    └──────────────┘    └────────────────┘
                                               ▲
                                               │
┌──────────┐    ┌──────────────┐    ┌────────────────┐
│ 模型 API │ -> │ 响应状态检查   │ -> │ JSON解析        │
└──────────┘    └──────────────┘    └────────────────┘
      ▲                 │                    │
      │                 └──────┬─────────────┘
      │                        ▼
      │              ┌──────────────────┐
      └──────────────│ 有限重试或能力降级 │
                     └──────────────────┘

生产级最佳实践可以总结为:

  1. 优先选择支持 json_schema + strict 的模型;
  2. Schema保持简单、封闭,并设置 additionalProperties: false
  3. 未知值使用 null,避免模型猜测;
  4. 在解析前检查 finish_reason 和拒绝状态;
  5. 流式响应必须完整拼接后再解析;
  6. 服务端约束之后仍执行本地 Schema校验;
  7. 再增加业务语义和权限校验;
  8. 对不支持结构化输出的模型按能力明确降级;
  9. 失败只做有限重试,并保留可诊断日志;
  10. 大结果采用分页、分块或工具查询,不依赖无限提高输出 token。

16. 总结

让大模型“返回一段看起来像 JSON的文本”很容易;让它稳定返回可被业务系统信任的结构化数据,则需要协议约束和客户端验证共同完成。

最稳妥的路径是:使用 json_schema + strict 约束输出结构,检查响应结束状态,完成 JSON解析、本地 Schema校验和业务语义校验;不支持 Structured Outputs时,降级到 json_object 或提示词 JSON,但不降低本地验证标准。涉及程序执行时,则应使用 Function Call,并把模型输出视为待校验的调用请求,而不是已经获得授权的操作命令。

最后提醒: “模型成功返回”不等于“业务数据可信”。只有完成响应状态检查、JSON解析、Schema校验、语义校验和权限校验之后,结构化输出才能进入真实业务流程。


如果本文对你有帮助,可以将这套流程整理成项目中的统一结构化输出组件,避免每个业务模块重复实现解析、校验、重试和降级逻辑。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐