「模型输出结构化 JSON」这件事已经不难了,难的是接着那一步:把它写进数据库。真出事的地方往往不是模型答得不合理,而是一条格式完全正确、却没经过任何检查的数据被直接写了进去——金额多一个零、日期填到了明年、关联的订单号根本不存在。

这篇给一个能直接套用的骨架:两道闸 + 一个兜底。第一道管「长得对不对」,第二道管「说得对不对」,兜底管「出错了别把线索丢了」。

第一道闸:结构校验,先保证类型和必填对

拿 JSON Schema 描述你期望的结构,让校验器来判断,不要自己写一串 if 去查字段。这样做的额外好处是:schema 本身就是给模型看的说明书——同一个 schema 喂给模型的结构化输出功能,出错率会明显下降。

{
  "type": "object",
  "required": ["orderId", "amount", "currency", "reason"],
  "properties": {
    "orderId": { "type": "string", "pattern": "^ORD-[0-9]{8}$" },
    "amount": { "type": "number", "exclusiveMinimum": 0 },
    "currency": { "enum": ["CNY", "USD"] },
    "reason": { "type": "string", "minLength": 2, "maxLength": 200 }
  },
  "additionalProperties": false
}

注意最后一行 additionalProperties: false。它挡住的是模型自作主张多吐出来的字段——那些字段没有经过任何检查,却会顺着你的代码一路流到业务逻辑里,是最容易被忽略的一类问题。

第二道闸:业务规则,格式对不等于该写

结构校验只说明「这句话语法通顺」,不说明「这句话是真的」。第二道闸要回答的是几个业务问题:这个金额在允许范围内吗?这个日期合理吗?它引用的实体真的存在吗?这条记录是不是重复的?

# 结构校验过了,再做业务规则校验
rules = [
    (lambda d: d["amount"] <= 50000,        "金额超过单笔上限"),
    (lambda d: d["currency"] in allowed,     "不支持的币种"),
    (lambda d: order_exists(d["orderId"]),   "订单号不存在"),
    (lambda d: not is_future(d["date"]),     "日期在未来"),
]
for ok, msg in rules:
    if not ok(d):
        reject(d, msg)   # 拒绝写库,记录原因,交人工或退回模型重试

这一步的关键是把「拒绝」当成正常路径,而不是异常。模型输出被拒之后,你有三种选择:退回让它重试、转人工复核、或者干脆放弃这条——具体选哪个取决于这条数据有多要紧。全都不选、直接写进去,才是真正的风险。

写库这一步单独处理:幂等与并发

两道闸都过了,还有一个坑:同一条输出可能被处理两次。网络重试、队列重投、用户连点两下,都会让同一笔操作进来两遍。所以写库要用一个从输入里稳定算出来的幂等键,落库时靠唯一约束去重,而不是在代码里「先查有没有」——并发下那个查询和写入之间有窗口。

-- 结构示意:幂等键 + 唯一约束,让数据库替你去重
INSERT INTO operations (idem_key, order_id, amount, reason)
VALUES (:idem_key, :order_id, :amount, :reason)
ON CONFLICT (idem_key) DO NOTHING;

兜底:出错了别丢原始输出

被拒绝的输出不要直接扔掉。它是最有信息量的数据——把「原始输出 + 命中了哪条规则 + 当时的输入」存成一张独立的表,攒上一两周,你会看清模型到底在哪类输入上最容易变形,也就知道该去改提示词、改 schema、还是补一条规则。丢掉它,就等于每次都从零开始猜。

上线前对着这几条核一遍

  1. 结构校验里有没有限制「多余字段」,而不只是检查必填项?
  2. 业务规则是不是独立于提示词——也就是换了模型、改了提示词后规则照样生效?
  3. 写库是不是靠唯一约束保证幂等,而不是靠应用层先查后写?
  4. 被拒绝的输出有没有落库留档,包含原始文本与拒绝原因?
  5. 有没有一条「批量回放」的路径,能把历史被拒输出重新跑一遍验证新规则?
别指望用提示词把模型的输出约束到万无一失;提示词是减少出错率的,真正兜底的是写入之前那几行代码。

最后提醒一句:JSON Schema 的实现、各语言的校验库与数据库的冲突处理语法都各有版本差异,动手前对着当前文档核一遍;文里的代码是结构示意,重点是「先校验、后写库,拒绝是正常路径」这套顺序,别照抄参数。