模型输出能解析,还需过类型与业务校验
文章以数量字段类型错误导致下游订单异常为例,区分 JSON 可解析、模式合规与业务正确三个层次。建议保存原始响应并回放验证,覆盖类型漂移、金额不一致、非法状态和跨租户标识等问题。
AI 深度解读
模型返回了合法 JSON,订单却照样出了错:作者的一次故障中,quantity 被写成字符串 '3',下游最终发出了 12 笔数量为零的订单。原文没有交代字符串如何变成零,但问题很明确:能解析,不等于能使用。
作者给出的办法是把输出依次送过解析、Schema、业务规则三道关,再保存原始响应做回放。这篇值得读的地方,是把「模型应该听懂了」变成可检查的契约;需要保留的疑问,则是作者没有公布失败率和对照数据。
合法 JSON 到底保证了什么
JSON mode 保证的是输出能被当作 JSON 解析,不保证字段符合业务要求。{"quantity": "3"} 是合法 JSON,但如果数量必须是整数,它就没有履行契约。
作者建议做一个小实验:保存 50 条原始响应,先用 json.loads 统计解析成功数,再交给真正的 Schema 校验器,比较两次通过数量。他预期两者会有差异,但没有公布这组实验的结果,不能把它当成已经证明的普遍比例。
三道关分别回答不同的问题:
- Gate A 检查能不能解析,拦住语法错误。
- Gate B 检查类型、必填字段、范围和枚举,拦住结构错误。
- Gate C 检查跨字段关系和业务规则,拦住结构正确但业务不成立的数据。
解析通过,只说明数据进得了门,不说明它有资格执行后续操作。
例如,订单金额可能算错,状态可能违反状态机,订单 ID 可能属于另一个租户。这些问题不能只靠字段类型判断。尤其是租户归属,还需要结合请求身份或数据库记录,单独检查返回对象并不够。
Schema 管字段,业务代码管关系
作者的模板使用 jsonschema 库里的 Draft202012Validator,对应 JSON Schema draft 2020-12。订单必须包含 order_id、quantity、unit_cents、total_cents 和 status,并通过 additionalProperties: False 拒绝额外字段。
规则也写得很具体:order_id 匹配 ^ord_[0-9a-f]{8}$;quantity 是 1 到 999 的整数;两个金额字段是非负整数;status 只能是 new、paid、shipped。
这些限制仍然不能证明总金额正确。下面这段原始代码负责补上金额之间的关系:
def gate_c(obj):
if obj['total_cents'] != obj['quantity'] * obj['unit_cents']:
COUNT['gateC:arithmetic'] += 1
return None, 'gateC:total-mismatch'
return obj, None
要看的是乘法比较那一行:它假设订单总额就是数量乘单价。如果业务存在折扣、税费或运费,这条规则就得调整。Gate C 必须来自自己的领域规则,不能直接复制成通用答案。
作者随后又提出一个更省事的选择:只让模型返回 quantity 和 unit_cents,由代码计算总额。前面的检查适合验证已有输出,后面的设计则减少了模型需要负责的事情。
Strict mode 也不能让本地校验器退场。作者提醒,不同提供方对类型、正则、范围、跨字段运算的支持不同,但没有给出产品和版本对照,不能据此判断某个具体服务的能力。
还有两个容易漏掉的边界:字段缺失与 null 是否被区分,布尔值是否会被误认为整数。Python 中 isinstance(True, int) 为真,手写类型检查时尤其需要留意。
重试能救一次,回放才能看见问题
作者反对的是用重试掩盖失败。同一个提示词再请求一次,可能得到合格结果,却没有解释第一次为什么失败。
文中的「Gate B 失败率为 4%」是举例,不是作者公布的实测结果。它想说明:即使重试让最终请求成功,首次失败仍应被记录,否则提示词退化可能被成功率遮住。
作者的回放方法不依赖在线模型。先把原始输出存成 JSONL,每行包含 content,再由 replay.py 依次调用 gate_a、gate_b、gate_c;任一关失败就跳过,全部通过才增加 kept,最后打印通过数和 COUNT。
准备好原文的 gate.py、replay.py 与响应文件后,执行命令如下:
python -m venv .venv && . .venv/bin/activate
pip install jsonschema
python replay.py saved_responses.jsonl
最后一行使用的是已保存的响应,因此修改校验规则后可以重复检查同一批数据,不必重新消耗模型请求。
作者还建议在拒绝之前做类型转换,例如把 '3' 转成 3,同时记录转换次数。这里存在一个实现缺口:原文模板没有包含转换逻辑,照抄代码会直接在 Gate B 拒绝字符串。转换规则需要自己补,并限定适用字段。
Schema 也应和提示词一起进入版本控制。每次拒绝都记录 Schema 版本,才能判断失败来自模型输出,还是契约发生了变化。
我的判断
这套方法最有价值的是把失败分开统计,让团队知道问题发生在哪里。它适合接收模型结构化结果、还要继续写库或调用业务接口的流程。
但校验器只能证明「满足已写下的规则」。一个格式正确、确实存在的订单 ID,仍可能不是用户想查询的订单。作者也明确承认,三道关不能消除幻觉,且没有公开自己的回放数据。因此,这是一套可实践的工程经验,还不是可靠性提升的量化证明。
部署建议也要分开看。作者主张让校验与模型调用处于同一进程,避免额外网络跳转;随后推荐 MonkeyCode 的免费服务器方案,并披露文章属于产品推广。文中提到的额度是据转述的 1000 万 tokens,同时说明配额会变化。这不能当成当前承诺,更不能支持 p99 延迟结论,作者自己也反对这样推断。
对于已经端到端强制执行类型契约的 SDK,我倾向于复用已有校验入口,再补业务规则和观测。原文建议这类场景跳过三关设计,但类型契约是否覆盖跨字段规则,仍需确认。
能拿来做什么
- 给前端或 Node 中接收 AI 结构化结果的位置补一份明确契约,先检查必填字段、数字类型和额外字段,再决定是否交给下游。
- 保存首批 50 条原始响应作为回放样本,分别记录解析、Schema 和业务规则的失败数;允许重试时,也保留首次失败记录。
- 从一个确定的业务关系开始写 Gate C,例如数量与单价的计算关系;能由代码推导的字段,优先交给代码计算。
- 把提示词和 Schema 一起管理版本,明确哪些字段允许类型转换,并记录转换率,避免一次提示词修改悄悄改变接口行为。