随笔2026 · 09 / 236 分钟♡ 39评论 6

给模型输出写契约:能解析的 JSON,不等于能用的结果

「让模型输出 JSON」解决的只是格式问题。真正让产线稳下来的,是把模型当成一个不可信的外部服务,给它的每一次输出写契约、做校验、留降级。

想不通就摊开纸画,画到第三版才对

有一段时间我很得意于一件事:产线里所有模型的输出都是 JSON,一行代码就能解析,再也不用写正则从自然语言里抠字段了。

然后线上出了一个很难看的问题。某个环节的模型在大约 2% 的情况下会输出一个格式完全正确、字段齐全、但数值不合理的结果——比如一段音频的时长字段是 0,或者一个本该在 0 到 1 之间的分数是 37。

JSON 解析全部通过。下游拿着这些「合法」的数据往下跑,直到最后一步合成时才炸,而那时已经离真正出错的地方隔了四个环节。

能解析,不等于能用。 这件事让我重新想了「模型输出」到底是什么。

把模型当成一个不可信的外部服务

我最后的心智模型是:模型就是一个你不能读源码、行为会漂移、偶尔会一本正经地胡说的外部服务。你对待一个第三方接口会做的事,对模型输出全都要做,而且要做得更严。

具体就是三件:契约、校验、降级。

契约:不只是字段名,还有值域和关系

契约要写清楚的不只是「有哪些字段、什么类型」,还有:

最后一类最容易漏,也最常出问题。模型对输入的理解偏了,输出在自己的世界里完全自洽,只有拿输入来对照才能发现。

校验:在离模型最近的地方,用代码而不是靠提示词

有了契约,校验的位置很重要——紧贴模型输出的下一行,而不是等下游用到的时候。越早拦住,定位越容易。

另一个原则:用代码校验,不要指望提示词。 我曾经在提示词里写了三遍「分数必须在 0 到 1 之间」,模型在 98% 的时候听话,剩下 2% 依旧我行我素。而这 2% 在批量产线里就是每天几十次。

提示词负责让好结果的概率变高,代码负责让坏结果一个都过不去。两者不能互相替代。

降级:校验不过之后,产线该怎么走

校验失败之后做什么,比校验本身更考验设计。我用的是一个三级策略:

  1. 重试一次,换一个措辞。很多时候只是模型这一次抽风,换个说法它就正常了。
  2. 换一个模型。同一个任务备一个不同来源的模型,主模型连续失败就切过去。两个模型同时抽风的概率很低。
  3. 标记并跳过,不要伪造。两个模型都不行,就把这个任务标记出来,人工看。绝对不要用一个「默认值」把坑填上继续跑——我早期这么干过,结果是产物看起来完整,实际上有一段是空白,直到有人真的看到最后才发现。

第三条我想多说一句。写兜底逻辑的时候,总有一种诱惑是「反正给个合理的默认值,别让流程断掉」。但一个被默认值悄悄填掉的错误,比一个大声报出来的错误贵十倍——前者会被带到很远的下游,混进产物里,直到有人用肉眼看到为止。

一个衡量标准

现在我判断产线里某个模型环节是否「接好了」,就看一件事:如果这个模型今天开始输出纯垃圾,产线会在哪一步停下来?

答案如果是「在它的下一行」,这个环节就接好了。答案如果是「最后合成的时候」或者「用户看到的时候」,那不管它平时表现多好,都还没接好。

这篇里的照片Frames全部相册 →
凌晨两点四十,队列还剩十一条
凌晨两点四十,队列还剩十一条家 · 书房 · Sony A7C II · 35mm f/2.0
← 更早一篇
断点续跑:一条流水线最值钱的不是快,是能从第 137 步接着走

关注公众号「十年追随」

新文章第一时间推给你。留言我都会回,公众号里也能收到回复通知。 不发广告,一周最多两条。

十年追随 公众号二维码
评论Comments6 条 · 匿名也能评,昵称头像会记住
评论服务(Waline)待部署 —— 香港 VPS 起来后填 SERVICES.waline 即生效。
想说话可以先去 留言板,那边我每条都回。
关注公众号「十年追随」更新第一时间推给你 · 留言也能收到我的回复
看二维码