JSON 输出总翻车?先补这份结构化避坑清单

17 min read

凌晨 1:17,Discord 里有人 @我:“脚本没报错,但工作流停了。”

我顺着日志往下看,模型本来该返回一段 JSON,结果它先客气了一句“当然可以”,后面又包了个 ```json,再往下还有一个多余的尾逗号。人眼一看就知道它想表达什么,解析器可不讲人情,直接卡死。

最烦的地方就在这儿:这类故障看起来像小概率。真把 AI 接进提取脚本、工作流节点、飞书机器人或者表单处理之后,你很快会发现,它不是“会不会发生”,而是“今晚发生还是下周发生”。

这两天我专门整理这个坑时,刚好撞上一则 Reddit 话题:原帖标题说,作者统计了本地模型在 288 次调用里各种 JSON 翻车方式。可惜帖子现在是 403,我没法替你复述它的原始分布,所以这里就不装“二手搬运”了。这篇直接讲更有用的东西:AI 生成 JSON 时,到底坏在哪;哪些能靠提示词压住;哪些必须在程序侧兜底。

前阵子我帮老大查一份结构化提取链路的问题,刚开始还以为是节点配置写错了。结果翻到最后才发现,真正把流程拖住的,不是模型“不会答”,而是它差一点答对:多了一句废话,少了一个括号,或者字段名偷偷变了。那种感觉很真实,你盯着日志半天,会开始怀疑是不是自己眼花了。

先把结论摆前面:让 AI 稳定输出 JSON,不是一个 prompt 问题,而是一个接口设计问题。 提示词只能减少事故,不能替你兜底。


为什么本地模型在 JSON 上更容易翻车?

短答:因为你面对的不只是模型本身,而是整条生成链路。

本地模型JSON翻车链路图 很多人第一反应是把锅全甩给“模型不够聪明”,这话只说对了一半。本地模型更容易在结构化输出上出问题,通常是下面这几层一起叠出来的:

环节会怎么把 JSON 弄坏典型表现
聊天模板系统提示被包装得不稳定明明要求只返回 JSON,前面还是多出解释文字
采样参数温度、top_p 太放飞key 名漂移、字段顺序乱、值类型不稳
上下文长度输出被截断少了结尾 }、数组没闭合
停止词设置stop token 切得太早或太晚JSON 半截断掉,或后面拖出废话
模型习惯爱“讨好式回答”自动加 Markdown 代码块、加说明、加注释
量化/运行时差异小误差放大格式问题同样提示词,A 环境能过,B 环境就炸

这里有个很多人容易混过去的点:“返回 JSON”其实同时包含两件事——语法正确,和结构正确。
前者是能不能被 parse;后者是字段、类型、必填项是不是都对。多数人只盯着第一层,所以脚本偶尔能跑,业务却时不时歪掉。

还有一个事,本地模型的问题往往不够“稳定地坏”。它不是每次都犯同一种错,所以你很难靠一次修补一劳永逸。今天是代码块,明天是尾逗号,后天可能是字段名突然换了个同义词。这也是为什么,单靠提示词经常会让人产生一种虚假的安全感。


AI 生成 JSON 时,到底常坏在哪?

短答:最常见的不是“完全胡说八道”,而是 差一点就对 的半结构化垃圾。

结构化输出四层防线流程图 我把最容易遇到的坑,按“提示词能不能压住”分成一张表。你做脚本、工作流还是小工具,基本都绕不开这些:

翻车类型具体表现提示词能缓解吗程序侧要不要兜底
包代码块json ... 能,通常有效要,先剥壳
前后废话“下面是结果:” / “希望对你有帮助”能,但不稳要,截取 JSON 主体
非法语法尾逗号、单引号、漏引号有时能要,repair
截断少半个对象或数组结尾很难要,重试或回退
类型漂移age: "18" 本该是数字部分能要,Schema 校验
字段漂移full_name 写成 name部分能要,字段映射或重试
额外字段多吐一堆你没要的内容能,效果一般要,白名单过滤
注释/伪 JSON// note/* comment */能压一点要,清洗
重复 key同一个字段写两次很难要,按规则取舍
转义问题引号、换行、反斜杠炸掉有时能要,repair + validate

如果你只让模型“尽量按格式返回”,那它一旦翻车,后面的脚本就得陪葬。说白了,结构化输出最怕的不是错,而是“看起来差不多,所以你没防”。

如果你想把 AI 输出直接接进脚本、工作流节点或数据库,别把“请返回 JSON”当成安全带。真正稳的做法是四层:提示词只负责缩小错误范围,Schema 校验负责拦截结构问题,repair 负责修补语法层小伤,失败回退负责保住流程不断。少一层,自动化就只是“偶尔能跑”,还谈不上可交付。

我见过最坑的一种情况,不是解析失败,而是解析成功。字段名拼错了一个字母,程序照样吞进去,后面统计、分流、入库全悄悄偏掉。你第二天看报表,只会觉得“怎么数据怪怪的”,很少第一时间怀疑是 AI 那个 JSON 偷偷拐弯了。

说真的,这类问题最烦的不是修,而是排查。因为日志看上去“没红”,任务状态也可能显示成功,最后是业务结果先出问题。等你回头倒查,才发现一切都从那个能 parse、但不该通过的 JSON 开始。


哪些错误,真的可以靠提示词先压下去?

短答:能压住“礼貌”和“装饰”,压不住“失误”和“截断”。

提示词可控与失控边界图 提示词最适合解决三类事:

  1. 不要解释
  2. 不要 Markdown 代码块
  3. 不要超出指定字段

你可以直接用下面这份模板。它不神,但够实用。

text你是一个只输出 JSON 的信息提取器。

规则:
1. 只返回一个 JSON 对象
2. 不要输出 Markdown,不要使用 ```json 代码块
3. 不要添加任何解释、前缀、后缀、注释
4. 如果信息缺失,使用 null,不要编造
5. 字段名必须严格使用下面这些:
   ["title", "summary", "tags", "risk_level"]
6. risk_level 只能是: "low" | "medium" | "high"
7. tags 必须是字符串数组
8. 输出必须符合标准 JSON 语法

返回格式示例:
{"title":"...","summary":"...","tags":["..."],"risk_level":"low"}

这类提示词为什么有效?因为它在做两件事:

  • 把模型的“表达欲”往回按
  • 把你允许它活动的范围缩小

但边界也很清楚。只要问题进入“语法损坏”“输出截断”“类型跑偏”这些层面,提示词就开始不靠谱了。 尤其是本地模型,环境稍微一变,昨天听话,今天就未必。

一个小建议:如果你的任务只是提取字段,宁可把输出结构做窄一点,也别一上来塞十几个可选字段。字段越多,模型越容易在 key 名、类型和缺失值处理上失控。

还有个很现实的经验:别让模型自己“猜默认值”。缺了就写 null,比它热心帮你补一个看似合理的字符串安全得多。后者最容易污染数据,而且不容易被发现。


哪些坑必须在程序侧兜底?

短答:凡是会让解析器、数据库、分支逻辑出错的,都别交给模型自觉。

LLM输出四道关与回退流程图 这里建议你直接接受一件事:repair 不是锦上添花,是必需层
尤其你一旦要把输出接进自动化流程,程序侧至少要有这四道关:

1)预清洗:先把“外壳”扒掉

这一步解决的是代码块、前后废话、空白字符、奇怪前缀。

你不用太聪明,反而要保守:
只清掉最常见的包装,不要写一堆激进规则把正常内容误伤。

2)语法修复:把“差一点就对”的 JSON 扶正

目标是处理:

  • 尾逗号
  • 单引号
  • 漏闭合括号
  • 非法转义
  • 注释残留

这一层你可以自己写,也可以接一个 JSON repair 类库。无论哪种,原则都一样:只修语法,不替业务做决定。
比如字段缺失、类型错误,这不是 repair 该瞎补的。

3)Schema 校验:别让“能 parse”冒充“能用”

第一次提到 JSON Schema(描述 JSON 结构和类型的标准),它就是干这个的。
你要明确哪些字段必填、类型是什么、枚举值有哪些、数组里装什么。

很多人把 repair 做完就收工,结果 JSON 的确能 parse 了,但 score 本来该是数字,模型给了 "high"tags 本来该是数组,它给你一整段字符串。解析没报错,业务照样翻车。

4)失败回退:别让一条坏输出拖垮整条流程

这一步最值钱,也最容易被省掉。

你的回退策略至少要有一个:

  • 带错误信息重试一次
  • 换更保守的提示词重试
  • 切到更稳的模型
  • 返回空结构并标记人工复核
  • 暂存原文,不继续执行下游动作
最危险的设计:解析失败就直接抛异常结束。 这在 demo 里看着干净,放进生产流程里,基本等于把半夜报警写进日历。

我自己现在更偏向把“失败回退”当成主流程的一部分,而不是异常分支。因为只要你真的跑过几轮,就会知道坏输出不是事故,它更像天气:有时大,有时小,但总会来。至于你的 repair 要写到多聪明、Schema 要卡到多严,这件事得看场景,我也不敢给一个放之四海而皆准的阈值。


一套能直接塞进工作流的通用流程,怎么搭?

短答:提示词约束 → 预清洗 → repair → Schema 校验 → 定向重试 → 回退。

LLM 输出到回退的通用漏斗流程图 你可以把它理解成一个漏斗。越往后,越少让模型“自由发挥”,越多让程序按规则办事。

通用处理顺序

  1. 调用模型
    用窄结构提示词,限制字段和类型。

  2. 预清洗
    去掉代码块、前后说明、无关空白。

  3. 尝试解析
    如果能直接 parse,继续下一步;不行就进 repair。

  4. 执行 repair
    只修语法层问题,不脑补业务字段。

  5. Schema 校验
    检查必填字段、类型、枚举值、数组结构。

  6. 定向重试
    把失败原因喂回去,比如:“risk_level 必须是 low/medium/high 之一,请只返回修正后的 JSON。”

  7. 失败回退
    仍不通过,就别继续下游写库、发消息、触发付款之类的动作。

你要是懒得把它抽象成框架,哪怕先按下面这个伪流程接起来,都比裸跑强很多:

textmodel_output
  -> strip_wrapper
  -> parse?
      yes -> schema_validate?
               yes -> accept
               no  -> targeted_retry -> schema_validate -> fallback
      no  -> repair
            -> parse?
                 yes -> schema_validate -> accept / retry / fallback
                 no  -> fallback

这套流程真正解决的,不是“让模型永远不犯错”,而是“让错误停在可控范围内”。
对自动化来说,后者比前者重要得多。模型不是数据库,别拿数据库那种“输入合法性”幻想去要求它,但你完全可以用工程手段把它驯到够用。

顺手说个容易被忽略的小坑:如果你的工作流平台会自动把 AI 输出再包一层字符串,记得先确认自己是在修“模型输出”,还是在修“平台转义之后的字符串”。不然你会对着错误方向抡锤子,越修越乱。这个学费,不少人都交过。


这套方法适合用在哪些场景?

只要你要“把结果接下去”,它就适合。

比如这些地方最常中招:

场景裸跑时最容易出的问题补上这套流程后的变化
网页信息提取个别页面一变,整批任务中断单条失败可隔离,不拖全局
工单/表单分类字段偶尔漂移,统计口径乱类型和字段更稳,便于入库
飞书/Slack 机器人回答看得懂,程序吃不下聊天输出和流程输出分离
AI 小工具后端demo 能跑,上线后偶发报错出错有回退,日志能定位
自动化工作流节点一处解析炸掉,全链路停摆错误停在节点,不放大

这也是我最想提醒技术人的地方:别把结构化输出当成“模型能力展示”,要把它当成接口稳定性工程。
你只要换了这个视角,很多看似玄学的问题,处理方式一下就落地了。


今天就能动手改的最小版本

如果你手上已经有一个会吐 JSON 的脚本,我建议你今天先补三件事:

  1. 把提示词改成“只允许固定字段 + 缺失写 null”
  2. 在解析前加一层“剥代码块 / 剥废话”
  3. parse 后补一层 Schema 校验

先别追求完美 repair 库,也别急着抽象框架。
你只要先把“能跑但脆”改成“偶尔出错但不会拖垮流程”,这条线就已经从玩具跨到工具了。


FAQ

Q: 我用的是云模型,不是本地模型,也要做 repair 吗?
A: 要。云模型通常更稳,但只要输出要接程序,就不能把正确性押在“它今天刚好听话”上。

Q: 有了 JSON Schema,是不是就不需要提示词约束了?
A: 不是。提示词负责减少垃圾输出,Schema 负责拦截坏结构。两层职责不同,少一层都容易多出重试成本。

Q: 什么时候该直接放弃 repair,改成重试?
A: 遇到截断、字段大量缺失、核心类型全错时,直接重试更稳。repair 适合补小伤,不适合替模型重写答案。

Q: 如果我现在只能补一层,先补哪层?
A: 如果你连校验都没有,先补 Schema 校验;如果你已经常被代码块和废话绊住,就先补预清洗。真要我只选一个优先级最高的,还是 Schema 校验 + 明确回退。

今晚你就可以拿一条真实工作流试一下:别先改模型,先看看自己现在有没有“清洗、校验、回退”这三道门。很多时候,问题不在模型多笨,而在于你是不是默认它会一直乖。