这个问题问的是怎么保证大模型稳定输出合法JSON,本质上是结构化输出的可靠性问题。你不能指望模型每次都听话,所以我的思路是搭三层防御,从软到硬再到兜底,而不是只靠一层。
先讲第一层,提示词工程。系统消息里明确说“你是一个严格的JSON生成器”,再给两三个[[Few-shot]]例子,最后加一句“只输出JSON,不要markdown代码块,不要解释”。这套做法零成本,迭代快,适合原型验证或者准确率要求不高的场景。但缺点也很明显,模型该[[幻觉]]还是幻觉,格式说飘就飘。说白了,这就是个软引导,靠不住。
所以真正落地一定要上第二层,约束解码。这里有几个主流方案。一种是基于文法的,比如llama.cpp的GBNF,逐token过滤非法路径,适合边缘部署和高稳定性场景。另一种是JSON Schema约束,像OpenAI的response_format,把Schema编译成自动机,动态约束token概率,适合云端API快速迭代。还有Outlines这类库,预编译索引加动态mask,支持复杂嵌套,适合自研模型深度定制。
这里有个关键权衡:约束越强,推理延迟越高。因为DFA状态跳转有开销。我的做法是把Schema预编译好,再缓存常见结构模板,这样能降低延迟。另外,不要同时叠加多层强约束,比如Grammar加Schema一起上,状态空间会爆炸,解码慢到没法用。
但就算有约束,模型还是可能犯错,所以第三层后处理修复是兜底。我一般分三级。第一级是语法修复,用正则把markdown标记删掉,补全缺失的括号。第二级是字段校验,用Pydantic验证,缺失字段就用默认值填充或者重试。第三级是LLM自修复,把错误JSON和错误信息再喂给模型,让它修正,但重试次数上限只设一次。
这里有个成本意识:L3重试的触发率必须控制在2%以下,超过这个数就说明前面两层没做好,得回头优化提示词或者约束。
举个例子,在实时[[Agent]]工具调用场景里,比如一个客服系统要调用退款接口,延迟敏感,偶尔重试可以接受,我就会用Schema约束加轻量后处理,这样速度快。如果是批量数据生成,比如生成一万条商品描述,准确率优先,可以并行,那就用Grammar-based加完整后处理。要是多租户SaaS,每个用户自定义结构,那就得动态Schema加沙箱校验,隔离不同用户的结构。
所以整体看,我会把这三层看成递进关系,不是非此即彼。实际选型就是根据场景在成本、延迟和准确率之间做取舍。
这里有个延伸点我没细说,就是约束解码对推理性能的具体影响。比如Grammar-based方案里,状态机的复杂度直接影响生成速度,我在上线前会特别关注这个,用benchmark测试不同Schema的吞吐量,防止因为约束太强拖慢整个服务。
最后总结一句,我不会迷信任何一层,而是把提示、约束、后处理当成一个整体来设计,根据业务场景动态调整权重。