让模型从一段文字里提取订单信息,返回下面这样的 JSON,程序就能直接创建订单了吗?

{
  "order_id": "DEMO-001",
  "sku": "DEMO-NOTEBOOK",
  "quantity": 2,
  "unit_price_cent": 1990,
  "total_cent": 3000,
  "currency": "CNY"
}

这段 JSON 可以解析,数量和金额也都是整数,但两本笔记本,每本 19.90 元,总额应该是 39.80 元。这里的 30 元对不上。

结构化输出,就是让模型按约定的结构返回数据,方便程序继续处理。 JSON 是常见的载体,但能解析 JSON、字段符合约定、数据满足业务规则,是三个不同的问题。下面用一份订单把它们分开,再写一个本地程序逐层校验。

先约定要提取什么

假设输入是这段文字:

订单 DEMO-001:购买 DEMO-NOTEBOOK 笔记本 2 本,单价人民币 19.90 元,合计 39.80 元。

提取结果约定如下:

字段含义约定
order_id订单号非空字符串
sku商品编码非空字符串,还要能在商品目录里找到
quantity数量大于 0 的整数
unit_price_cent单价,单位为分大于 0 的整数
total_cent总额,单位为分大于 0 的整数,等于数量乘单价
currency币种本例只接受 CNY

金额统一用整数分表示,因此 19.90 元写成 1990,39.80 元写成 3980。

另外,假设本地商品目录记录的单价是 1990 分,这笔订单必须按这个价格购买。真实业务允许合同价或折扣时,需要改用对应订单的定价规则,不能直接拿当前目录价否定历史订单。

三层校验分别管什么

第一层:JSON 语法

如果模型输出:

{"order_id": "DEMO-001", "quantity": 2,}

最后多了一个逗号,JSON 解析会失败。模型如果在 JSON 前加了“好的,结果如下”,或者用 Markdown 代码块包住 JSON,把整段正文传给解析器也会失败。

这一层只解决文本能不能被解析。即使解析成功,得到的也可能是数组、字符串或 null,并不一定是订单对象。

第二层:字段、类型和单字段约束

把数量改成 "2",仍然是合法 JSON,但它是字符串,不符合本例要求的整数类型。缺少 sku、数量为 0、币种写成 USD,也都需要拒绝。

这些约定可以用 JSON Schema 描述。它是一份数据结构说明,例如对象有哪些属性、哪些必填、属性是什么类型、取值有什么限制。

下面只展示数量字段所在的最小 Schema,不是完整订单的 Schema:

{
  "type": "object",
  "properties": {
    "quantity": { "type": "integer", "exclusiveMinimum": 0 }
  },
  "required": ["quantity"],
  "additionalProperties": false
}

properties 描述字段,required 要求字段出现,additionalProperties: false 拒绝未声明的字段。只写 properties 不会自动让字段必填,也不会自动禁止额外字段,具体规则见 JSON Schema 对象说明。

数量大于 0 本身也是业务要求,只是它能直接写进字段约束。这里把单字段约束放在第二层,字段关系和外部数据放在第三层,是为了分开观察错误,不代表 JSON Schema 只能约束单个字段。它也能表达部分字段依赖和条件规则;不过,本例的数量乘单价等于总额,以及查询商品目录,仍由应用代码处理。

第三层:字段之间和外部数据是否一致

开头的订单能通过前两层:字段齐全、类型正确、数值都大于 0,但 2 × 1990 不等于 3000。

再比如,把单价改成 1000,总额改成 2000,乘法也对了,但仍不符合本例商品目录的定价。商品编码是否存在,同样需要查目录,单靠字段类型不能判断。

因此,业务校验还需要拿到程序里的规则和可信数据。字段结构不能证明模型确实理解了原文。

先用接口已有的结构化输出能力

接入模型时,先查看所选模型和接口支持什么,再决定怎样要求它返回订单。

方式做法应用仍需要处理什么
提示词约定在提示词中写“只返回 JSON”,给出字段和例子可能出现解释文字、语法错误、缺字段和类型错误
JSON 模式使用服务提供的 JSON 输出能力合法 JSON 仍可能缺字段,或包含错误类型、错误数值
Schema 约束输出向支持该能力的服务传入 Schema,让服务按其支持的约束生成检查服务实际支持哪些约束,并继续做本地和业务校验

如果接口已经支持所需的 Schema 约束,可以直接使用。只在提示词里粘贴一份 Schema,不等于启用了服务端的约束输出。服务限制了生成格式,本地校验负责检查实际收到的数据,两者可以一起使用。

JSON 模式也要按接口要求配置。例如 DeepSeek JSON Output要求启用对应的响应格式并提供提示词,还说明了截断和空白响应的处理要求。不能仅凭模式名称就省掉完成状态和空内容检查。

例如,Gemini 结构化输出文档提供了使用 JSON Schema,以及通过 Python SDK 使用 Pydantic 定义结构的方式。它支持 Schema 的子集,并要求应用处理结构符合要求但语义错误的结果。文档还列出了一些不由生成约束执行的关键词,例如 minLength;这意味着本例的非空字符串要求仍需本地检查。具体支持范围以接入时的模型和接口文档为准,不能把某个服务的保证推广到所有服务。

下面使用 Pydantic 的内置字段校验,只把订单计算和目录核对留给业务代码。示例固定输出、不请求模型,展示的是收到数据后的检查过程,不是模型提取能力的测试。

用 Python 跑一遍

在空目录执行:

python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install pydantic==2.13.4

将下面的完整代码保存为 order_validation.py:

import json
from typing import Literal
 
from pydantic import BaseModel, ConfigDict, Field, ValidationError
 
 
class Order(BaseModel):
    model_config = ConfigDict(strict=True, extra="forbid")
 
    order_id: str = Field(min_length=1)
    sku: str = Field(min_length=1)
    quantity: int = Field(gt=0)
    unit_price_cent: int = Field(gt=0)
    total_cent: int = Field(gt=0)
    currency: Literal["CNY"]
 
 
CATALOG = {"DEMO-NOTEBOOK": 1990}
 
 
def check_business(order: Order) -> list[str]:
    errors = []
    price = CATALOG.get(order.sku)
    if price is None:
        errors.append("商品不存在")
    elif order.unit_price_cent != price:
        errors.append(f"单价应为 {price} 分")
 
    expected = order.quantity * order.unit_price_cent
    if order.total_cent != expected:
        errors.append(f"总额应为 {expected} 分")
    return errors
 
 
def validate_order(raw: str) -> tuple[str, str]:
    try:
        data = json.loads(raw)
    except json.JSONDecodeError:
        return "语法失败", "无法解析为 JSON"
 
    try:
        order = Order.model_validate(data)
    except ValidationError as exc:
        details = []
        for error in exc.errors():
            path = ".".join(str(part) for part in error["loc"]) or "$"
            details.append(f"{path}:{error['type']}")
        return "结构失败", "; ".join(details)
 
    errors = check_business(order)
    if errors:
        return "业务失败", "; ".join(errors)
    return "通过", f"{order.order_id}, total_cent={order.total_cent}"
 
 
def main():
    valid = {
        "order_id": "DEMO-001",
        "sku": "DEMO-NOTEBOOK",
        "quantity": 2,
        "unit_price_cent": 1990,
        "total_cent": 3980,
        "currency": "CNY",
    }
    cases = [
        ("正常订单", json.dumps(valid)),
        ("多余逗号", '{"quantity": 2,}'),
        ("数量是字符串", json.dumps({**valid, "quantity": "2"})),
        ("总额不一致", json.dumps({**valid, "total_cent": 3000})),
        ("缺少商品编码", json.dumps({k: v for k, v in valid.items() if k != "sku"})),
        ("多出字段", json.dumps({**valid, "approved": True})),
        ("数量为零", json.dumps({**valid, "quantity": 0})),
        ("商品不存在", json.dumps({**valid, "sku": "DEMO-UNKNOWN"})),
        ("单价不一致", json.dumps({**valid, "unit_price_cent": 1000, "total_cent": 2000})),
    ]
    for name, raw in cases:
        status, detail = validate_order(raw)
        print(f"{name} -> {status}: {detail}")
 
 
if __name__ == "__main__":
    main()

运行:

python order_validation.py

输出如下:

正常订单 -> 通过: DEMO-001, total_cent=3980
多余逗号 -> 语法失败: 无法解析为 JSON
数量是字符串 -> 结构失败: quantity:int_type
总额不一致 -> 业务失败: 总额应为 3980 分
缺少商品编码 -> 结构失败: sku:missing
多出字段 -> 结构失败: approved:extra_forbidden
数量为零 -> 结构失败: quantity:greater_than
商品不存在 -> 业务失败: 商品不存在
单价不一致 -> 业务失败: 单价应为 1990 分

json.loads() 先把字符串解析为 Python 数据,Order.model_validate() 再检查结构。只有得到合法的 Order 对象,才会进入 check_business(),因此业务代码不用再处理数量是字符串、商品编码缺失等情况。

这里显式设置了 strict=True,因为 Pydantic 默认可能把 "2" 转成整数 2。是否允许转换取决于接口约定,本例希望发现模型输出的类型错误,所以拒绝这种转换。严格模式也不是对所有类型和输入方式都采用同一规则,本文实际走的是先解析 JSON、再校验 Python 对象的路径,见 Pydantic 严格模式。

extra="forbid" 则让多出的 approved 字段报错。Pydantic 默认会忽略额外字段;如果希望发现模型擅自添加的数据,就需要主动禁止,见 额外字段配置。

程序没有创建订单,也没有写数据库。通过 只表示满足这里写出的规则;

实际接入时可以省掉什么

前面的代码分开调用 json.loads() 和 model_validate(),是为了分别显示语法错误与结构错误。如果不需要把这两步拆开,可以直接使用 Pydantic 内置的 Order.model_validate_json(raw),同时解析 JSON 和校验字段,失败时会抛出 ValidationError;接着再调用 check_business(order)。SDK 如果已经返回校验后的 Order 对象,也不必先转成字符串再解析一次。内置入口见 Pydantic JSON 解析文档。

两种入口对部分类型的严格校验行为不同,例如日期在 JSON 中只能用字符串表示。当前订单只包含字符串和整数,不能据此假定换成日期、元组等类型后行为也完全相同。

另外,JSON Schema 的 integer 按数值是否有小数部分判断,2.0 也能满足这个约束;本例先解析后得到 Python float,Pydantic 的严格 int 字段会拒绝它。因此,“通过 Schema”与“通过这个 Python 模型”也不是完全相同的保证。相关定义见 JSON Schema 数值类型。

本例也没有实现严格 JSON 审计。Python 的默认解析器会接受 NaN 等扩展值,重复键则保留最后一个值;如果业务要求拒绝这些输入,需要单独设置解析规则,不能把 json.loads() 成功等同于完全符合 JSON 标准。具体行为见 Python JSON 兼容性说明。

上面的 Pydantic 模型可以通过 Order.model_json_schema() 生成 Schema,供支持的接口使用,见 Pydantic Schema 生成文档。但本地的严格类型配置和任意 Python 校验逻辑不一定能完整表达在 Schema 中。这里的 check_business() 也不会自动传给模型服务,目录价格和总额关系仍由应用检查。

模型调用还可能被截断、拒绝或执行失败。接入时先按服务协议判断调用是否正常完成,再取完整输出进入校验;如果采用流式,先收齐相关数据,不能把半份 JSON 当作可执行的订单。

通过校验以后,还要对照原始材料

假设模型把“两本”提取成了“三本”,并把总额一起改成 5970 分。数量合法、单价正确、乘法成立,上面的程序会判定通过,但结果与原文不符。

这类问题需要核对提取依据。应用可以保留原始文本,让模型返回字段对应的原文片段,再由程序检查片段是否存在,或交给人工确认。片段存在只能说明能在原文中找到这句话,还不能证明它与该字段对应正确;不能把模型自己生成的一段解释当成事实证据。

原文缺少单价时,也不应该让模型为了填满必填字段而猜一个数。可以把提取阶段的单价设计成允许 null,同时返回待补充原因;等拿到定价依据后,再转换成要求字段完整的订单数据。缺失字段与显式 null 的含义应单独约定,不要用 0 同时表示“免费”和“不知道”。

校验失败时怎样处理

语法或结构错误可以把明确的错误路径反馈给模型,要求重新提取,并设置有限的重试次数。不要只写“结果不对”,也不要靠无限重试等待它碰巧成功。

总额不一致时,要先明确任务:如果是在提取原始订单,就保留原文金额并标记冲突;如果是在创建新订单,就让代码按可信价格计算总额,而不是让模型决定最终收费金额。本例为了演示错误会直接拒绝不一致数据,没有擅自修正。

商品不存在、原文缺信息或价格冲突,需要查询可信数据、让用户补充或进入审核。重新生成一个能通过校验的值,并不等于问题已经解决。

真正创建订单时,还要在写入环节检查权限、重复提交,以及可能已经变化的价格或库存。模型负责把文字整理成候选数据,程序决定这些数据是否足以执行当前操作。