一把 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 外的键)。
常见的四种返回形态
探测下来,返回大致落在几类:
- 严格 JSON:整段字符串就是一个合法 JSON 对象,直接解析即可。
- 围栏包裹:
`json ...`,剥掉围栏后可用。 - 散文夹 JSON:前面有“好的,以下是结果:”,后面有总结。需要定位第一个
{和最后一个}。 - 结构漂移:是 JSON,但字段名、嵌套层级或类型与 schema 不一致,甚至 schema 外的键混进来。
注意:声明支持 response_format 不等于一定严格执行。有的后端只保证“输出是合法 JSON”,不保证符合你的 schema;有的在复杂嵌套或长输出时退化。所以声明和实测要分开对待。
解析层怎么写才不脆
不要假设一种形态。建议做一条容错链,逐级降级并记录命中哪一级:
- 第一级:直接
JSON.parse。 - 第二级:去掉首尾空白和代码围栏后重试。
- 第三级:截取第一个
{/[到最后一个}/]之间的子串再解析。 - 第四级:仍失败则视为格式错误,走重试或换后端。
同时对解析成功的对象做 schema 校验(字段存在性、类型、枚举值),把“解析成功但不合 schema”单独计数——这类问题最容易被忽略,却会在下游引发难以定位的 bug。
什么时候该退回提示词约束
如果某个后端在带 response_format 时仍频繁漂移,或者你的输出结构复杂到后端难以稳定遵守,就不要继续在接口参数上纠缠,回到提示词层面控制:
- 把 schema 写进提示词,用简短的字段说明加示例,而不是长篇描述。
- 要求“第一个字符是
{,最后一个字符是}”,对抑制前后散文有明显作用。 - 把复杂结构拆成两步:先让模型输出精简的中间结构,再由代码补全其余字段。
- 输出很长时,让模型分块返回,降低中途结构崩坏的概率。
提示词约束不是万能的,但它是所有后端都认的那一层,适合作为兜底。
一把 key 多后端的工程建议
- 按后端记录能力画像:把“是否支持 response_format”“是否严格遵守 schema”“典型失败形态”写进配置,而不是散落在代码里。
- 解析链统一,重试策略分后端:格式错误的重试次数、是否换后端,可以按画像配置。
- 不要把 JSON 模式当唯一保障:无论哪个后端,解析后都要校验,校验失败要有可观测的日志。
- 回归探测:后端行为可能随版本变化,把探测脚本保留下来,定期重跑,避免配置过期。
聚合站的价值在于用一把 key 横向对比和切换,但这也意味着你不能把某个后端的行为当默认。先探测、再容错、最后才谈优化,是更稳的顺序。