大数跨境

模型吐的 json 五个错法,我在出口挂了一道闸

模型吐的 json 五个错法,我在出口挂了一道闸 小兵的AI视界
2026-09-04
8
导读:模型吐的 json 不能只靠 prompt 里写“return JSON”碰运气。厂商 structured output 是提高合规概率、不是保证,出口校验闸才是物理拦截。这篇给了四条路线判断链 +

图片

我是小兵,一个动手派AI架构师。「AI工程化实战」系列第 7 篇。

上线第四周,数据组来拍桌子

客服 AI 念身份证的事过去一周,脱敏闸挂上了,泄露是不泄露了。可数据组的新工单比泄露还难缠:模型把“金额”吐成字符串 “¥1,234”(schema 要的是 number),“是否退款”整个字段丢了,还自作主张加了 schema 里根本没有的“运费”。下游 json.loads 直接崩,pandas 读出来一片 NaN。

上篇说“输出侧先脱敏、再锁格式”——脱敏闸挂了,格式闸没挂。这篇把输出侧最后一道闸挂上:JSON Schema 结构校验,不合规就重试、兜底、降级一条龙。

先弄清:prompt 里写 return JSON 为什么不保险

让模型“按这个 schema 输出”,和上篇“别泄露”是一回事——都是措辞。模型按 token 概率吐,不按 JSON 语法吐。键名拼错、字段少一个、类型不对,它自己不知道,也没人拦。我把线上一个月的输出拉出来,烂 json 就这五类:

  1. 字段缺失——required 里的键没吐出来,比如“是否退款”整个丢了;
  2. 类型错——“金额”吐成字符串,要 number 给 string;
  3. 多余字段(幻觉)——多出个“运费”,schema 里根本没有;
  4. 嵌套错——收货地址的“城市”吐成数字;
  5. 值不在枚举内——状态字段给个枚举外的字符串。

烂 json 进了下游,三个后果一个比一个贵:json.loads 直接崩,调用方 500;pandas 读出一片 NaN,报表静默错——比崩了更可怕,你看见数字不对才知道出事;最危险的是 Agent 工具调用传错参,把字符串 “¥1,234” 当 number 传给扣款工具,动作是对的,参数是错的,钱就扣错了。

结论先放这儿:格式对不对和泄露不泄露一样,是代码闸的事,不是措辞的事

四条路线,收敛成一条判断链

市面上管模型输出结构就四条路。先记住一条判断链,看完你会选,不是只记住四个名词。

  • ① schema 校验 + 重试:出口端用 JSON Schema 校验,不过就把错误列表喂回模型重试。纯代码闸,不绑厂商不绑后端。代价是重试烧 token——重试 1 次长 json = 2 倍输出 token,而且不保证收敛,所以必须有 budget 上限 + 降级兜底,这就是开篇《四大支柱》说的“避免无限重试推高成本”。
  • ② 厂商 structured output:OpenAI / Anthropic / Bedrock 到 2026 年都做成了正式能力,厂商在服务端尽量锁。但这里有个我们踩过的大坑:JSON mode 不等于 structured output。OpenAI 的 JSON mode 只保证“吐出来是合法 JSON”,不保证“符合你的 schema”,字段照样能少、类型照样能错;strict 模式才保证。老模型还不支持,schema 也有限制(strict 下全部字段 required、additionalProperties 强制 false、根级不能有 oneOf、嵌套深度 ≤5)。
  • ③ 语法约束采样:outlines / guidance / llama.cpp GBNF,采样层把非法 token 的 logits 置为 -inf,模型想吐错都吐不出来,物理层级的强约束。代价是绑模型绑后端、要自托管、工程重,还有 8%~28% 的解码开销。
  • ④ json_repair 类兜底:修 markdown fence、尾逗号、单引号,救急好使。但修语法不修语义:“¥1,234” 修完还是字符串,不会自动变 number。

一条判断链怎么走?先过一道前置过滤:数据出不出得去?出不去(合规/隐私),厂商 API 直接出局,只剩自托管过了这关才轮到选择:厂商 API 优先 ② structured output + ① 出口校验兜底(含 ④ repair 救急旁路 + 重试 + 降级);自托管走 ③ grammar-constrained + ① 出口校验兜底。不管哪条,SchemaGate 都要挂。

厂商都帮你锁了,出口校验闸还挂吗

挂,而且必须。structured output 是在“生成端”提高合规概率,不是保证;出口校验是在“交付端”物理拦截。这层闸删了,就只能赌模型不出错。五条理由全是真碰到的:

  1. JSON mode ≠ strict,response_format 选错,上线第一周就被“字段对不上”打脸;
  2. 老模型不支持,灰度切旧型号参数被静默忽略,输出照样乱;
  3. schema 限制逼你改业务 schema(全部 required、根级无 oneOf、深度 ≤5),放宽了就不够硬;
  4. SSE 流式中途断流,厂商的“保证”断在半路,下游拿到半个 json;
  5. 网关把请求路由到另一家厂商(第 5 篇《LLM 网关选型》的活),那家能力不一样。

降级兜底长什么样?重试耗尽仍不合规,返回结构化 error object(error.code + error.detail + 原文本裁剪),不是 500。下游接得住、能告警、能进 Trace——这也是给第 8 篇《OTel + LangFuse 全链路 Trace》埋好的观测点。

一段最短代码:SchemaGate 长什么样

完整实现(可离线跑的 json_schema_gate.py + 5 个测试断言)在 CSDN 原文,这里只留核心思路和骨架。

挂载顺序必须先讲清楚:先脱敏、再锁格式。SchemaGate 挂在第 6 篇《OWASP LLM 安全护栏》的 OutputGuard 之后、同一出口端,入参是已脱敏文本。这里有个串篇的坑要提前点破:OutputGuard 会把“金额”这类敏感字段打成 “[金额已脱敏]” 占位串,而 schema 要求金额是 number——脱敏在前、锁格式在后,金额先被打成字符串,SchemaGate 再一查就是类型错。生产上二选一:金额这类“既要脱敏又要当输出”的字段,要么豁免脱敏,要么 schema 对脱敏占位符开白名单。

出口链就是一条线:

model -> OutputGuard.mask -> SchemaGate.guard -> 下游(json.loads / pandas / Agent 工具调用)

核心编排,几个关键取舍全在注释里:

for attempt in range(max_retries + 1): # max_retries 就是 budget 上限    report = validate_text(text, schema) # 解析 + 校验:一次报全,不 raise    if report.ok: 放行    repaired = repair_json_text(text) # repair 救急旁路    if repaired 且回校通过: 放行(repaired=True    if attempt >= max_retries: break # 超限停手,绝不无限重试    text = retry_with_feedback(report.errors) # 错误列表喂回 prompt,带反馈重试# 重试耗尽 -> 降级:结构化 error object(code + detail + sample),不是 500

生产把 ModelMock 换成真实 SDK——厂商 API 优先 strict,自托管用 outlines / SGLang,换哪条路,SchemaGate 出口校验一行不改。5 个断言,一条锁一道取舍:合规放行 / 一次报全 / repair 救急 / 重试收敛 / 重试不收敛降级。

两个付过费的坑

坑一:盲重烧钱。schema 不过就原样重跑,一个长 json 重试 3 次,token 花了 4 倍,还没收敛。翻日志发现每次重试的 prompt 一模一样——模型不知道错在哪,同样的错再犯一遍。

修复:带校验反馈重试(错误列表喂回 prompt),并设 budget 上限,超限走降级。长 json 重试成本记清楚:重试 1 次 = 2 倍输出 token,重试 3 次 = 4 倍。

坑二:把 JSON mode 当 structured output。上线第一周 response_format 配了 json,字段还是对不上——少字段、类型错照旧。排查才发现 JSON mode 只保证“合法”、不保证“合规”——以为厂商帮你锁了结构,其实没有。

修复:改 strict 模式(确认模型支持),出口校验兜底双保险。再强的 structured output,出口这道闸也得留着。

选型结论,直接抄作业

四条路线一张表,维度对齐“锁在哪一层 / 厂商依赖 / 解码开销 / 维护成本 / 适用”:

路线
锁在哪一层
解码开销
适用
① schema 校验 + 重试
出口端,纯代码闸
任何链路兜底,必须带 budget
② 厂商 structured output
生成端,厂商服务
厂商 API 链路首选
③ 语法约束采样
采样层,解码时
8%~28%
自托管 + 要硬保证
④ json_repair 兜底
出口端,语法修补
救急:fence / 尾逗号 / 截断

各厂商 structured output 支持度,关键记三行:

  • OpenAI:strict 模式,但限制多——全部字段 required、additionalProperties 强制 false、根级无 oneOf、嵌套深度 ≤5;JSON mode 只保证合法不保证合规;
  • Anthropic:Structured Outputs,2026-01 GA,Sonnet 4.5 / Opus 4.5 / Haiku 4.5 支持,但 refusal / max_tokens 截断仍可能返不合规输出,出口闸不可省
  • Amazon Bedrock / 国产各家:能力差异大,别拿 OpenAI 的文档当圣经。

选型 checklist,5 个“是/否”答完落到方案:数据出不出得去?是否自托管?要不要硬保证?团队养不养得起采样层工程(8%~28% 解码开销、XGrammar 编译期集成)?有没有现成厂商能力(查型号/GA 支持度)?

三句话总结

回到开头数据组那个工单——如果 SchemaGate 在,“金额”变字符串、“是否退款”丢失、多出“运费”,这三条在出口就被校验拦下:要么 repair、要么带反馈重试、要么结构化降级,烂 json 根本出不了出口。

到这儿,输出侧两道闸齐了:第 6 篇的脱敏闸管不泄露,本篇的 SchemaGate 管格式对不对。但“校验不通过”这件事本身必须看得见:重试了几次、哪个字段错的、走没走兜底,都要进 Trace。下一篇《OTel + LangFuse 全链路 Trace》,把第 5 篇网关、第 6 篇护栏、本篇 SchemaGate 全部打点串起来,出问题一眼定位到环节。


这篇是脱水版。完整版约 6000 字,含可离线跑的 json_schema_gate.py 全量代码、test_json_schema_gate.py 5 个断言、五类失败×三个后果对照、四条路线完整选型表、各厂商 structured output 支持度、重试成本推导附录,已同步发布在 CSDN。点文末「阅读原文」看完整代码和推导过程。

评论区聊聊:你们输出结构现在靠什么锁?是 prompt 里写一句 return JSON 碰运气,还是已经挂上物理闸了?

我是小兵,一个动手派AI架构师。这里只写自己跑过、摔过、复盘过的AI工程化案例。如果你想持续收到这类实战内容,点击关注,下篇见。

【声明】内容源于网络
0
0
小兵的AI视界
专注 AI 领域:AI前沿资讯/开源精品/实用工具,大模型应用开发/部署推理/微调实践,助你领航 AI。
内容 481
粉丝 0
小兵的AI视界 专注 AI 领域:AI前沿资讯/开源精品/实用工具,大模型应用开发/部署推理/微调实践,助你领航 AI。
总阅读2.6k
粉丝0
内容481