凌晨 1:17,Discord 里有人 @我:“脚本没报错,但工作流停了。”
我顺着日志往下看,模型本来该返回一段 JSON,结果它先客气了一句“当然可以”,后面又包了个 ```json,再往下还有一个多余的尾逗号。人眼一看就知道它想表达什么,解析器可不讲人情,直接卡死。
最烦的地方就在这儿:这类故障看起来像小概率。真把 AI 接进提取脚本、工作流节点、飞书机器人或者表单处理之后,你很快会发现,它不是“会不会发生”,而是“今晚发生还是下周发生”。
这两天我专门整理这个坑时,刚好撞上一则 Reddit 话题:原帖标题说,作者统计了本地模型在 288 次调用里各种 JSON 翻车方式。可惜帖子现在是 403,我没法替你复述它的原始分布,所以这里就不装“二手搬运”了。这篇直接讲更有用的东西:AI 生成 JSON 时,到底坏在哪;哪些能靠提示词压住;哪些必须在程序侧兜底。
前阵子我帮老大查一份结构化提取链路的问题,刚开始还以为是节点配置写错了。结果翻到最后才发现,真正把流程拖住的,不是模型“不会答”,而是它差一点答对:多了一句废话,少了一个括号,或者字段名偷偷变了。那种感觉很真实,你盯着日志半天,会开始怀疑是不是自己眼花了。
先把结论摆前面:让 AI 稳定输出 JSON,不是一个 prompt 问题,而是一个接口设计问题。 提示词只能减少事故,不能替你兜底。
为什么本地模型在 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 开始。
哪些错误,真的可以靠提示词先压下去?
短答:能压住“礼貌”和“装饰”,压不住“失误”和“截断”。
提示词最适合解决三类事:
- 不要解释
- 不要 Markdown 代码块
- 不要超出指定字段
你可以直接用下面这份模板。它不神,但够实用。
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"}
这类提示词为什么有效?因为它在做两件事:
- 把模型的“表达欲”往回按
- 把你允许它活动的范围缩小
但边界也很清楚。只要问题进入“语法损坏”“输出截断”“类型跑偏”这些层面,提示词就开始不靠谱了。 尤其是本地模型,环境稍微一变,昨天听话,今天就未必。
还有个很现实的经验:别让模型自己“猜默认值”。缺了就写 null,比它热心帮你补一个看似合理的字符串安全得多。后者最容易污染数据,而且不容易被发现。
哪些坑必须在程序侧兜底?
短答:凡是会让解析器、数据库、分支逻辑出错的,都别交给模型自觉。
这里建议你直接接受一件事:repair 不是锦上添花,是必需层。
尤其你一旦要把输出接进自动化流程,程序侧至少要有这四道关:
1)预清洗:先把“外壳”扒掉
这一步解决的是代码块、前后废话、空白字符、奇怪前缀。
你不用太聪明,反而要保守:
只清掉最常见的包装,不要写一堆激进规则把正常内容误伤。
2)语法修复:把“差一点就对”的 JSON 扶正
目标是处理:
- 尾逗号
- 单引号
- 漏闭合括号
- 非法转义
- 注释残留
这一层你可以自己写,也可以接一个 JSON repair 类库。无论哪种,原则都一样:只修语法,不替业务做决定。
比如字段缺失、类型错误,这不是 repair 该瞎补的。
3)Schema 校验:别让“能 parse”冒充“能用”
第一次提到 JSON Schema(描述 JSON 结构和类型的标准),它就是干这个的。
你要明确哪些字段必填、类型是什么、枚举值有哪些、数组里装什么。
很多人把 repair 做完就收工,结果 JSON 的确能 parse 了,但 score 本来该是数字,模型给了 "high";tags 本来该是数组,它给你一整段字符串。解析没报错,业务照样翻车。
4)失败回退:别让一条坏输出拖垮整条流程
这一步最值钱,也最容易被省掉。
你的回退策略至少要有一个:
- 带错误信息重试一次
- 换更保守的提示词重试
- 切到更稳的模型
- 返回空结构并标记人工复核
- 暂存原文,不继续执行下游动作
我自己现在更偏向把“失败回退”当成主流程的一部分,而不是异常分支。因为只要你真的跑过几轮,就会知道坏输出不是事故,它更像天气:有时大,有时小,但总会来。至于你的 repair 要写到多聪明、Schema 要卡到多严,这件事得看场景,我也不敢给一个放之四海而皆准的阈值。
一套能直接塞进工作流的通用流程,怎么搭?
短答:提示词约束 → 预清洗 → repair → Schema 校验 → 定向重试 → 回退。
你可以把它理解成一个漏斗。越往后,越少让模型“自由发挥”,越多让程序按规则办事。
通用处理顺序
-
调用模型
用窄结构提示词,限制字段和类型。 -
预清洗
去掉代码块、前后说明、无关空白。 -
尝试解析
如果能直接 parse,继续下一步;不行就进 repair。 -
执行 repair
只修语法层问题,不脑补业务字段。 -
Schema 校验
检查必填字段、类型、枚举值、数组结构。 -
定向重试
把失败原因喂回去,比如:“risk_level必须是 low/medium/high 之一,请只返回修正后的 JSON。” -
失败回退
仍不通过,就别继续下游写库、发消息、触发付款之类的动作。
你要是懒得把它抽象成框架,哪怕先按下面这个伪流程接起来,都比裸跑强很多:
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 的脚本,我建议你今天先补三件事:
- 把提示词改成“只允许固定字段 + 缺失写 null”
- 在解析前加一层“剥代码块 / 剥废话”
- 在
parse后补一层 Schema 校验
先别追求完美 repair 库,也别急着抽象框架。
你只要先把“能跑但脆”改成“偶尔出错但不会拖垮流程”,这条线就已经从玩具跨到工具了。
FAQ
Q: 我用的是云模型,不是本地模型,也要做 repair 吗?
A: 要。云模型通常更稳,但只要输出要接程序,就不能把正确性押在“它今天刚好听话”上。
Q: 有了 JSON Schema,是不是就不需要提示词约束了?
A: 不是。提示词负责减少垃圾输出,Schema 负责拦截坏结构。两层职责不同,少一层都容易多出重试成本。
Q: 什么时候该直接放弃 repair,改成重试?
A: 遇到截断、字段大量缺失、核心类型全错时,直接重试更稳。repair 适合补小伤,不适合替模型重写答案。
Q: 如果我现在只能补一层,先补哪层?
A: 如果你连校验都没有,先补 Schema 校验;如果你已经常被代码块和废话绊住,就先补预清洗。真要我只选一个优先级最高的,还是 Schema 校验 + 明确回退。
今晚你就可以拿一条真实工作流试一下:别先改模型,先看看自己现在有没有“清洗、校验、回退”这三道门。很多时候,问题不在模型多笨,而在于你是不是默认它会一直乖。