structured-output-across-vendors · ZH · 2026-10-19

一把 key 上逼六个后端吐 JSON:谁真认 response_format,谁在悄悄忽略

用同一段带 schema 的提示词逐个探测聚合站背后的 Claude、GPT、DeepSeek、Qwen、GLM、Kimi,记录各自对 JSON 输出的真实行为差异,并给出在行为不一致时退回提示词层面约束的工程做法。

为什么值得先探测再写代码

很多项目在接入 LLM 时假设“模型都支持 JSON 输出”,上线后才发现有的后端返回干净 JSON,有的把 JSON 裹在解释性散文里,有的在 JSON 前后加了代码围栏。聚合站让你用一把 API key 调多个后端,好处是切换成本低,代价是行为差异被藏在一个统一接口后面。

探测的目标不是给模型排名,而是搞清楚:在你的提示词下,每个后端到底吐出什么形状的字符串,以及解析失败时该退到哪一层。

探测方法:固定提示词,只换模型

要点是控制变量。同一段提示词、同一个 schema、同一个温度设置(建议先用低温以便复现),只改模型名。

  • 提示词里显式给出字段名、类型、是否必填,并给一个小例子。
  • 明确要求“只输出 JSON,不要解释、不要代码围栏”。
  • 请求体里如果接口支持 responseformat(如 {"type": "jsonobject"} 或 JSON Schema 形式),先带上;再跑一轮不带,对比差异。
  • 每个后端至少跑若干次,观察稳定性而非单次结果。

记录三件事:原始返回字符串、能否直接 JSON.parse、失败时的具体形态(前缀散文、后缀散文、围栏、字段缺失、类型错、多返回了 schema 外的键)。

常见的四种返回形态

探测下来,返回大致落在几类:

  1. 严格 JSON:整段字符串就是一个合法 JSON 对象,直接解析即可。
  2. 围栏包裹: `json ... ` ,剥掉围栏后可用。
  3. 散文夹 JSON:前面有“好的,以下是结果:”,后面有总结。需要定位第一个 { 和最后一个 }。
  4. 结构漂移:是 JSON,但字段名、嵌套层级或类型与 schema 不一致,甚至 schema 外的键混进来。

注意:声明支持 response_format 不等于一定严格执行。有的后端只保证“输出是合法 JSON”,不保证符合你的 schema;有的在复杂嵌套或长输出时退化。所以声明和实测要分开对待。

解析层怎么写才不脆

不要假设一种形态。建议做一条容错链,逐级降级并记录命中哪一级:

  • 第一级:直接 JSON.parse。
  • 第二级:去掉首尾空白和代码围栏后重试。
  • 第三级:截取第一个 {/[ 到最后一个 }/] 之间的子串再解析。
  • 第四级:仍失败则视为格式错误,走重试或换后端。

同时对解析成功的对象做 schema 校验(字段存在性、类型、枚举值),把“解析成功但不合 schema”单独计数——这类问题最容易被忽略,却会在下游引发难以定位的 bug。

什么时候该退回提示词约束

如果某个后端在带 response_format 时仍频繁漂移,或者你的输出结构复杂到后端难以稳定遵守,就不要继续在接口参数上纠缠,回到提示词层面控制:

  • 把 schema 写进提示词,用简短的字段说明加示例,而不是长篇描述。
  • 要求“第一个字符是 {,最后一个字符是 }”,对抑制前后散文有明显作用。
  • 把复杂结构拆成两步:先让模型输出精简的中间结构,再由代码补全其余字段。
  • 输出很长时,让模型分块返回,降低中途结构崩坏的概率。

提示词约束不是万能的,但它是所有后端都认的那一层,适合作为兜底。

一把 key 多后端的工程建议

  • 按后端记录能力画像:把“是否支持 response_format”“是否严格遵守 schema”“典型失败形态”写进配置,而不是散落在代码里。
  • 解析链统一,重试策略分后端:格式错误的重试次数、是否换后端,可以按画像配置。
  • 不要把 JSON 模式当唯一保障:无论哪个后端,解析后都要校验,校验失败要有可观测的日志。
  • 回归探测:后端行为可能随版本变化,把探测脚本保留下来,定期重跑,避免配置过期。

聚合站的价值在于用一把 key 横向对比和切换,但这也意味着你不能把某个后端的行为当默认。先探测、再容错、最后才谈优化,是更稳的顺序。