排障

CCSub 报错 invalid_structured_output:JSON 校验失败排查与修复

排障2026-11-02·11 分钟阅读

近 7 天在平台侧记录到的错误类型中,invalid_structured_output 出现 12 次,排在 upstream_auth_or_policy_error、connect_error、upstream_unavailable 之后。它的数量不算最多,但最容易被误判:很多人第一反应是“CCSub 挂了”或“额度出问题”,实际上请求已经成功返回,只是返回的 JSON 没能通过你这边(或 SDK 那边)的结构化校验。本文把它拆开讲清楚,并给出可直接照做的修复步骤。

一、先确认它到底是什么错

invalid_structured_output 的含义是:上游模型确实产出了内容,但内容在解析或校验阶段被判定为非法的结构化输出。典型表现有:

  • 返回文本不是合法 JSON,例如前后带了解释性文字、Markdown 代码围栏,或出现尾逗号。
  • JSON 合法,但字段缺失、类型不对(字符串给了数字)、枚举值不在允许范围内。
  • JSON 合法,但触发 additionalProperties: false 或必填字段校验失败。
  • 流式响应中途截断,最后一个 } 还没到就被解析。

判断方法很简单:看 HTTP 状态码。如果是 4xx 额度类或鉴权类错误,报错不会长这样;如果是 5xx 或连接中断,通常伴随 connect_error、upstream_unavailable。而 invalid_structured_output 往往发生在你收到 200 响应之后,是你的代码在 JSON.parse 或 schema 校验那一步抛出来的。换句话说,问题在契约,不在通道。

二、三类最容易触发的场景

1. 函数调用 / tool use

使用函数调用时,模型输出的是 tool_calls 里的参数字符串,这个字符串同样要能被解析成对象。常见坑是 schema 里写了嵌套数组、联合类型或过深的层级,模型在生成长参数时容易在中途“走神”,输出被截断或改写成自然语言。参数越长、嵌套越深,失败率越高。

2. response_format 严格 JSON 模式

当你用 response_format 指定 JSON 输出时,模型会努力按 schema 生成,但“努力”不等于“保证”。如果 prompt 里同时混入“先分析再给结论”的指令,模型可能把分析过程也写进 JSON,导致结构变形;如果 schema 里存在多个可选字段,模型有时会自由发挥补字段,触发 additionalProperties 校验失败。

3. 批量任务与长上下文拼接

批量任务中一次请求处理多条记录,并要求逐条返回结构化结果时,输出 token 变长,模型在列表后段更容易出现格式崩塌。另一个高频原因是流式返回:多个 chunk 拼接顺序或时机处理不当,或提前把不完整的 buffer 交给解析器。

经验判断:如果同样请求重试一次就成功,基本是生成侧的概率性偏差;如果稳定失败,多半是 schema 本身写得过严或 prompt 与 schema 相互冲突。

三、四步修复法

步骤 1:精简 schema,收敛必填字段

先做减法。把 required 限制在业务真正必需的字段上,其余字段给默认值或允许为空;去掉不必要的嵌套层级;把 anyOf、联合类型改成扁平枚举。一个能稳定通过的简单 schema,比一个漂亮但通过率低的 schema 有用得多。

{
  "type": "object",
  "additionalProperties": false,
  "required": ["intent", "confidence"],
  "properties": {
    "intent": { "type": "string", "enum": ["refund", "logistics", "other"] },
    "confidence": { "type": "number" },
    "note": { "type": "string" }
  }
}

注意 note 不在 required 里,模型可以不返回,解析端要能容忍缺省。

步骤 2:prompt 只做一件事

把“分析”和“输出”拆成两次请求,或者明确要求模型只输出 JSON、不加任何解释。prompt 里出现“请先说明理由”这类话,几乎必然导致结构污染。对于 function calling,把复杂参数改为先返回一个 ID 列表,再按需补查细节,避免单次生成超长嵌套结构。

步骤 3:关闭流式,拼接完整后再解析

排障阶段先关掉流式(stream: false),拿到完整响应再一次性解析,这样能立刻区分是“生成不完整”还是“schema 不匹配”。确认稳定后,如果确实需要流式,请在客户端累积 buffer,直到收到结束标记再交给 JSON 解析器,不要对半截字符串解析。

步骤 4:在 CCSub 上切换模型重试

不同模型对结构约束的遵循程度不同。同一份 schema 和 prompt,可以先用 gpt-5.6-sol 跑一遍,再用 claude-sonnet-4-6 跑一遍做对比:前者在严格 JSON 与函数参数生成上通常更稳,后者在长文本和复杂指令理解上更细腻。用两份结果定位问题究竟在 schema 还是模型倾向,比盲目重试更省 token。

四、验证脚本思路

写一个最小复现脚本,把失败样本固化成测试用例,每次改 schema 后跑一遍回归。核心逻辑:

  1. 准备 10~20 条真实输入,覆盖正常与边界情况。
  2. 对每条输入调用一次接口,记录原始返回文本。
  3. 本地执行 JSON.parse 并跑 schema 校验,统计通过率。
  4. 把失败样本的原始输出保存下来,人工看是截断、多字段还是类型错。
const res = await fetch("https://ccsub.xyz/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer sk-你的CCSub密钥"
  },
  body: JSON.stringify({
    model: "gpt-5.6-sol",
    stream: false,
    response_format: { type: "json_object" },
    messages: [{ role: "user", content: "只输出 JSON,字段见 schema" }]
  })
});
const data = await res.json();
const text = data.choices[0].message.content;
JSON.parse(text); // 校验从这里开始

五、在 CCSub 上如何接入与验证

OpenAI 兼容调用:把 OPENAI_BASE_URL 设为 https://ccsub.xyz/v1,请求 https://ccsub.xyz/v1/chat/completions,密钥用 sk-你的CCSub密钥 形式。注意是 /v1/chat/completions,不要多写或少写路径段。

Codex 原生调用:base 地址用 https://ccsub.xyz/openai,协议选择 wire_api = responses,Codex 会自动请求 /responses 端点。如果你在 Codex 里看到结构校验失败,先确认没有把 base 配成 /v1,否则路径会拼错。

Claude Code 原生调用:ANTHROPIC_BASE_URL 设为 https://ccsub.xyz/api,客户端会自动请求 /v1/messages。用 Claude 系模型做结构化输出时,schema 尽量扁平,并把枚举值写全。

可用模型以接口为准:调用 GET https://ccsub.xyz/v1/models 查询当前可用列表,不要凭记忆写模型名。工具侧支持 Claude Code、Codex、OpenCode、OpenClaw、Cursor、VS Code、Windsurf、CherryStudio,配置方式可参考站内 安装配置 与 API 参数与推理能力 文档。

六、成本提醒与行动建议

结构化输出失败会消耗输入 token 甚至部分输出 token,批量任务下重试成本会被放大。建议在排障阶段用较小的模型做 schema 验证,确认通过后再切到主力模型跑全量。充值前先估算单次请求的 token 量,具体定价可在模型分组页查看;预算有限的场景优先减少重试次数,而不是减少测试样本。

下一步行动:打开你的失败请求日志,确认是解析失败而非通信失败;按本文第三步关流式复现一次;再用 gpt-5.6-sol 与 claude-sonnet-4-6 各跑一次对比结果。若仍然稳定失败,带上原始返回文本到 故障排查 页面按症状核对,通常能在 schema 层找到答案。

CCSub API 调用方法

如果你使用 OpenAI 兼容客户端,把 Base URL 设置为 https://ccsub.xyz/v1,API Key 使用控制台创建的 sk-你的CCSub密钥。

export OPENAI_BASE_URL=https://ccsub.xyz/v1
export OPENAI_API_KEY=sk-你的CCSub密钥

Codex 请使用原生 Responses 专用地址。先设置密钥:

export CCSUB_API_KEY=sk-你的CCSub密钥

再在 ~/.codex/config.toml 配置自定义 provider:

model = "gpt-5.6-sol"
model_provider = "ccsub"
[model_providers.ccsub]
name = "CCSub"
base_url = "https://ccsub.xyz/openai"
env_key = "CCSUB_API_KEY"
wire_api = "responses"

Claude Code 使用专用 Messages Base URL:

export ANTHROPIC_BASE_URL=https://ccsub.xyz/api
export ANTHROPIC_AUTH_TOKEN=sk-你的CCSub密钥

需要确认当前可用模型时,可以请求 GET https://ccsub.xyz/v1/models。正式长任务前,建议先用短请求验证 Key、余额和模型是否正常。

读完想动手试试?3 分钟接入 CCSub。